CMake 사용법

C++ 프로젝트가 파일 몇 개를 넘어서면 컴파일 명령이 금방 복잡해진다. 소스 파일이 여러 디렉터리에 흩어지고, 라이브러리를 따로 빌드해서 링크해야 하고, 리눅스에서는 GCC, macOS에서는 Clang, 윈도우에서는 MSVC를 각각 다르게 호출해야 한다. Makefile을 직접 짜서 관리할 수도 있지만 컴파일러·플랫폼이 바뀔 때마다 손을 봐야 하고, IDE(Visual Studio, CLion 등) 연동도 매번 따로 챙겨야 한다. CMake는 이 문제를 “빌드 스크립트를 생성하는 메타 빌드 시스템”으로 해결한다 — CMakeLists.txt 하나로 프로젝트 구조를 선언하면, 실제 빌드에 필요한 Makefile이나 Ninja 파일, Visual Studio 프로젝트 파일을 플랫폼에 맞게 생성해준다. 이 글에서는 실제로 빌드까지 검증한 예제를 통해 CMake의 기본 사용법과 실무에서 자주 쓰는 기능을 정리한다.

프로젝트 구조와 CMakeLists.txt

라이브러리와 실행 파일을 분리한 전형적인 C++ 프로젝트 구조는 다음과 같다.

project-root/
|-- CMakeLists.txt
|-- src/
|   |-- main.cpp
|-- include/
|   |-- mylibrary.h
|-- lib/
|   |-- mylibrary.cpp

include/mylibrary.h는 라이브러리 헤더, lib/mylibrary.cpp는 라이브러리 구현, src/main.cpp는 이 라이브러리를 사용하는 실행 파일이다. 빌드 산출물은 별도의 build/ 디렉터리에 생성되는데, 이 디렉터리는 소스 트리와 분리된 “out-of-source” 빌드 방식이며 뒤에서 이유를 설명한다.

루트에 두는 CMakeLists.txt는 다음과 같이 작성한다.

# 최소 CMake 버전 지정
cmake_minimum_required(VERSION 3.10)

# 프로젝트 이름 및 버전
project(MyProject VERSION 1.0)

# C++ 표준 지정
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 라이브러리 빌드 (정적 라이브러리)
add_library(mylibrary STATIC lib/mylibrary.cpp)
target_include_directories(mylibrary PUBLIC include)

# 실행 파일 빌드 및 라이브러리 링크
add_executable(myapp src/main.cpp)
target_include_directories(myapp PRIVATE include)
target_link_libraries(myapp PRIVATE mylibrary)

target_include_directoriesPUBLIC/PRIVATE 키워드가 핵심이다. mylibraryPUBLIC으로 지정했기 때문에 include/ 경로가 이 라이브러리를 링크하는 다른 타깃(myapp)에도 자동으로 전파된다. 반대로 myappPRIVATE이므로 myapp을 링크하는 타깃이 있더라도 그 경로가 전파되지 않는다. 이런 타깃 단위 전파 덕분에 전역 변수(include_directories() 같은 구식 명령)를 쓰지 않고도 각 타깃의 의존성을 명확히 관리할 수 있다.

빌드하기

CMake는 소스 디렉터리와 빌드 디렉터리를 분리하는 out-of-source 빌드를 권장한다. 소스 트리 안에서 바로 빌드(in-source build)하면 빌드 산출물과 소스 파일이 섞여 .gitignore 관리가 지저분해지고, 빌드 디렉터리를 통째로 지우고 다시 시작하기도 번거롭다. -S(소스 디렉터리)와 -B(빌드 디렉터리) 옵션으로 위치를 명시하면 이 문제를 피할 수 있다.

# 빌드 디렉터리를 생성하며 구성(configure)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release

# 빌드 실행 (병렬 빌드)
cmake --build build --parallel

# 실행
./build/myapp

실행하면 다음과 같은 출력을 볼 수 있다.

Hello, CMake!
Message from MyLibrary

CMAKE_BUILD_TYPEDebug, Release, RelWithDebInfo, MinSizeRel 중 하나를 지정하며, 컴파일러 최적화/디버그 심볼 플래그를 대신 설정해준다. 값을 지정하지 않으면 대부분의 생성기(Generator)에서 최적화 없이 빌드되므로, 배포용 빌드에서는 Release를 명시하는 습관을 들이는 것이 좋다. cmake --build build 대신 빌드 디렉터리로 이동해 makeninja를 직접 실행해도 되지만, cmake --build는 내부적으로 어떤 생성기를 쓰든 동일한 명령으로 빌드할 수 있어 플랫폼에 무관하게 스크립트를 작성할 때 유리하다.

외부 라이브러리 연동 — find_package

시스템에 설치된 라이브러리를 사용하려면 find_package로 찾은 뒤 타깃을 링크한다. 예를 들어 스레드를 사용하는 프로그램이라면 다음과 같이 작성한다.

find_package(Threads REQUIRED)
target_link_libraries(myapp PRIVATE Threads::Threads)

Threads::Threads처럼 네임스페이스가 붙은 이름을 “임포트 타깃(Imported Target)”이라고 부르는데, 최신 CMake에서는 라이브러리 경로나 컴파일 플래그를 직접 지정하는 대신 이런 임포트 타깃을 링크하는 방식을 권장한다. 플랫폼별로 필요한 링크 플래그(예: 리눅스에서 pthread 링크 시 필요한 -pthread)를 CMake와 라이브러리 쪽에서 알아서 처리해주기 때문이다. Boost, OpenSSL, Protobuf 등 CMake 설정 파일을 제공하는 라이브러리는 대부분 같은 패턴(find_package(패키지명 REQUIRED)target_link_libraries(타깃 PRIVATE 패키지명::컴포넌트))으로 연동한다.

주의사항

  • in-source 빌드는 피한다. 소스 루트에서 바로 cmake .을 실행하면 CMakeCache.txt, CMakeFiles/ 등이 소스 트리에 섞여 들어간다. 실수로 이렇게 생성했다면 해당 파일들을 지우고 -S/-B 옵션으로 다시 구성해야 한다.
  • 전역 명령보다 타깃 기반 명령을 쓴다. include_directories(), link_libraries() 같은 디렉터리 전역 명령은 프로젝트가 커질수록 어떤 타깃에 어떤 설정이 적용되는지 추적하기 어렵다. 이 글에서 사용한 target_include_directories, target_link_libraries처럼 타깃 이름을 명시하는 명령을 우선 사용해야 한다.
  • cmake_minimum_required 버전은 실제 사용 중인 기능과 맞춰야 한다. PUBLIC/PRIVATE 전파나 임포트 타깃 같은 기능은 버전에 따라 지원 여부가 다르므로, 지정한 최소 버전보다 최신 문법을 쓰면 다른 환경에서 구성이 실패할 수 있다.
  • 빌드 디렉터리를 지웠는데도 이상 동작하면 캐시부터 의심한다. CMakeCache.txt에 이전 구성 값(컴파일러 경로, 옵션 등)이 남아있으면 새 옵션이 반영되지 않는 경우가 있다. 빌드 디렉터리 전체를 삭제하고 처음부터 다시 구성하는 것이 가장 확실하다.

마무리

CMake는 처음 배울 때는 문법이 낯설게 느껴지지만, 핵심은 결국 “타깃(라이브러리·실행 파일)을 선언하고, 그 타깃에 필요한 include 경로와 링크 대상을 붙이는 것” 하나로 요약된다. 이 글에서 다룬 add_library/add_executable, target_include_directories, target_link_libraries, find_package 네 가지 명령만 능숙하게 다뤄도 대부분의 중소 규모 C++ 프로젝트 빌드는 커버할 수 있다. 프로젝트가 여러 디렉터리로 커지면 각 디렉터리에 별도의 CMakeLists.txt를 두고 add_subdirectory()로 묶는 구조로 확장하면 된다.

참고

답글 남기기