CMake로 C++ 프로젝트 빌드하기: 라이브러리, 헤더 경로, 빌드 타입, 설치 규칙

이 글의 핵심

cmake_minimum_required(VERSION 3.10) project(MyProject).

다른 생태계와 비교

CMake는 메타 빌드 시스템(실제 빌드는 Make·Ninja·IDE가 담당)입니다. Rust의 Cargo·npm·Go 모듈처럼 언어와 패키지 매니저가 한 축에 있는 구조와 달리, C++는 CMake + Conan 또는 vcpkg 조합으로 맞추는 경우가 많습니다. Python pip·uv·Poetry와 의존성 락·재현 빌드를 비교해 보면 팀 표준을 잡기 쉽습니다. Makefile 직접 작성과의 관계는 C++ Makefile·빌드 시스템 비교를 함께 보세요.

기본 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)

add_executable(myapp main.cpp)

빌드:

mkdir build
cd build
cmake ..
make
./myapp

세 줄짜리 파일이지만 각 줄에 이유가 있습니다. cmake_minimum_required는 단순한 버전 검사가 아니라 정책(policy) 기본값을 정합니다. CMake는 버전이 올라가며 동작을 바꿀 때 기존 프로젝트가 깨지지 않도록 “이 프로젝트는 3.10 기준으로 동작해 달라”는 선언을 받아 그 시점의 동작을 유지합니다. 너무 낮은 버전을 적으면 최신 CMake가 “Compatibility with CMake < 3.5 has been removed”(CMake 4.0부터) 같은 에러나 deprecation 경고를 냅니다. project()는 컴파일러를 탐지하는 단계라, 이보다 먼저 컴파일러 관련 변수를 쓰면 값이 비어 있습니다.

cmake ..는 “구성(configure)” 단계로, 컴파일러를 찾고 CMakeCache.txt와 Makefile을 생성합니다. 실제 컴파일은 make가 합니다. Windows에서 Visual Studio 생성기를 쓰거나 Ninja를 쓰면 make 명령이 없으므로, 생성기에 상관없이 동작하는 cmake --build .을 쓰는 습관을 들이면 스크립트를 플랫폼마다 바꿀 필요가 없습니다. 최신 CMake(3.13+)에서는 cmake -S . -B build 한 줄로 디렉터리 생성과 구성을 함께 할 수 있습니다.

여러 소스 파일

cmake_minimum_required(VERSION 3.10)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)

add_executable(myapp 
    main.cpp
    utils.cpp
    math.cpp
)

라이브러리 만들기

# 정적 라이브러리 (아래 SHARED 버전과 둘 중 하나만 선택)
add_library(mylib STATIC
    lib.cpp
    helper.cpp
)

# 동적 라이브러리
add_library(mylib SHARED
    lib.cpp
    helper.cpp
)

# 실행 파일에 링크
add_executable(myapp main.cpp)
target_link_libraries(myapp mylib)

위 코드는 두 방식을 나란히 보여 주기 위한 것이라, 그대로 한 파일에 넣으면 “add_library cannot create target “mylib” because another target with the same name already exists” 에러가 납니다. 타겟 이름은 프로젝트 전체에서 유일해야 합니다. STATIC/SHARED를 아예 적지 않으면 BUILD_SHARED_LIBS 변수에 따라 결정되므로, 라이브러리를 배포하는 프로젝트라면 키워드를 생략하고 사용자가 -DBUILD_SHARED_LIBS=ON으로 고르게 하는 방식이 관례입니다.

SHARED 라이브러리에는 정적 라이브러리에 없는 함정이 두 가지 있습니다. Windows(MSVC)에서는 심볼을 __declspec(dllexport)로 명시하지 않으면 아무것도 내보내지 않아 .lib 임포트 라이브러리가 생성되지 않고 링크가 실패합니다(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS로 우회 가능). Linux에서는 빌드 트리에서 잘 돌던 실행 파일을 다른 위치로 옮기면 “error while loading shared libraries: libmylib.so: cannot open shared object file”이 나는데, rpath 설정 문제입니다.

헤더 파일 경로

# 프로젝트 구조
# project/
#   CMakeLists.txt
#   src/
#     main.cpp
#   include/
#     mylib.h

cmake_minimum_required(VERSION 3.10)
project(MyProject)

include_directories(${PROJECT_SOURCE_DIR}/include)

add_executable(myapp src/main.cpp)

include_directories는 동작은 하지만 최근에는 권장하지 않는 방식입니다. 이 명령은 그 디렉터리와 하위 디렉터리에 있는 모든 타겟에 경로를 추가하므로, 프로젝트가 커지면 어떤 타겟이 어떤 헤더에 의존하는지 CMakeLists만 봐서는 알 수 없게 됩니다. “Modern CMake”라 불리는 권장 방식은 target_include_directories(myapp PRIVATE include)처럼 타겟 단위로 속성을 지정하는 것입니다. 아래 예시 3과 “자주 발생하는 문제”에서 이 방식을 사용합니다.

실전 예시

예시 1: 기본 프로젝트 구조

MyProject/
├── CMakeLists.txt
├── src/
│   ├── main.cpp
│   ├── math.cpp
│   └── utils.cpp
├── include/
│   ├── math.h
│   └── utils.h
└── build/

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# 헤더 파일 경로
include_directories(${PROJECT_SOURCE_DIR}/include)

# 소스 파일 목록
set(SOURCES
    src/main.cpp
    src/math.cpp
    src/utils.cpp
)

# 실행 파일 생성
add_executable(myapp ${SOURCES})

# 컴파일 옵션
target_compile_options(myapp PRIVATE
    -Wall
    -Wextra
    -O2
)

CMAKE_CXX_STANDARD_REQUIRED를 켜 두는 이유는, 이 값이 꺼져 있으면 컴파일러가 C++17을 지원하지 않을 때 CMake가 조용히 더 낮은 표준으로 내려가서 빌드를 시도하기 때문입니다. 그러면 원인과 무관해 보이는 문법 에러가 쏟아집니다. 켜 두면 구성 단계에서 바로 실패합니다. 또 GCC/Clang에서는 기본적으로 -std=gnu++17(GNU 확장 포함)이 붙는데, 표준 모드만 원하면 set(CMAKE_CXX_EXTENSIONS OFF)를 추가합니다.

target_compile_options에 -O2를 직접 넣은 부분은 주의가 필요합니다. 최적화 수준은 빌드 타입(Debug/Release)이 결정해야 하는 값인데, 여기에 고정해 두면 Debug 빌드에서도 -O2가 붙어 디버거에서 변수가 “optimized out”으로 보입니다. 또 -Wall 같은 GCC 스타일 플래그는 MSVC에서 인식되지 않으므로, 여러 컴파일러를 지원해야 한다면 아래 “컴파일러 체크” 절처럼 분기하는 편이 안전합니다.

빌드:

mkdir build && cd build
cmake ..
make
./myapp

예시 2: 외부 라이브러리 사용

cmake_minimum_required(VERSION 3.10)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)

# pthread 찾기
find_package(Threads REQUIRED)

add_executable(myapp main.cpp)

# pthread 링크
target_link_libraries(myapp Threads::Threads)

Threads::Threads처럼 ::가 들어간 이름은 임포트 타겟입니다. 라이브러리 파일 경로만이 아니라 필요한 컴파일 플래그와 헤더 경로까지 함께 들고 있어서, 링크만 하면 나머지가 따라옵니다. 예전 방식처럼 ${OpenCV_LIBS} 같은 변수를 쓰면 헤더 경로를 따로 넣어야 하고, 오타가 나도 빈 문자열로 확장되어 에러 없이 넘어가다가 링크 단계에서야 실패합니다. 반면 존재하지 않는 :: 타겟을 링크하면 구성 단계에서 “Target “myapp” links to target “Foo::Foo” but the target was not found”로 즉시 알려 줍니다. find_package가 패키지를 못 찾으면 “Could not find a package configuration file provided by …” 에러가 나는데, 이때는 CMAKE_PREFIX_PATH에 설치 경로를 지정하거나 vcpkg/Conan 툴체인 파일을 넘겨야 합니다.

예시 3: 서브디렉토리 구조

MyProject/
├── CMakeLists.txt
├── app/
│   ├── CMakeLists.txt
│   └── main.cpp
└── lib/
    ├── CMakeLists.txt
    ├── mylib.cpp
    └── mylib.h

루트 CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)

add_subdirectory(lib)
add_subdirectory(app)

lib/CMakeLists.txt:

add_library(mylib STATIC
    mylib.cpp
)

target_include_directories(mylib PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}
)

app/CMakeLists.txt:

add_executable(myapp main.cpp)
target_link_libraries(myapp mylib)

이 구조의 핵심은 lib/CMakeLists.txt의 PUBLIC 키워드입니다. target_include_directories와 target_link_libraries에는 세 가지 범위가 있습니다.

  • PRIVATE: 이 타겟을 빌드할 때만 사용하고, 링크하는 쪽에는 전파하지 않습니다.
  • INTERFACE: 이 타겟 자신은 쓰지 않고, 링크하는 쪽에만 전파합니다(헤더 전용 라이브러리).
  • PUBLIC: 둘 다입니다.

mylib의 헤더 경로를 PUBLIC으로 선언했기 때문에, app/CMakeLists.txt는 target_link_libraries(myapp mylib) 한 줄만으로 mylib.h를 찾을 수 있습니다. 경로를 PRIVATE으로 바꾸면 mylib 자체는 빌드되지만 main.cpp에서 “mylib.h: No such file or directory”가 납니다. 판단 기준은 간단합니다. 공개 헤더에 등장하는 의존성은 PUBLIC, 구현 파일(.cpp)에서만 쓰는 의존성은 PRIVATE입니다. 이렇게 나눠 두면 라이브러리 내부에서만 쓰는 무거운 의존성이 사용자 타겟의 컴파일 명령에 섞여 들어가지 않습니다.

제가 CMake 프로젝트를 정리할 때 가장 자주 손보는 부분이 이것입니다. 모든 것을 루트에서 include_directories로 뿌려 두면 당장은 편하지만, 라이브러리 하나를 다른 프로젝트로 떼어 내려고 하는 순간 숨은 의존성이 줄줄이 드러납니다. 타겟마다 자기 의존성을 선언해 두면 add_subdirectory 하나로 옮겨 붙일 수 있습니다.

고급 기능

조건부 컴파일

option(BUILD_TESTS "Build tests" ON)

if(BUILD_TESTS)
    add_subdirectory(tests)
endif()

# 사용 (터미널)
# cmake -DBUILD_TESTS=OFF ..

option()은 캐시 변수를 만들기 때문에, 한 번 -DBUILD_TESTS=OFF로 구성하면 그 값이 CMakeCache.txt에 저장되어 다음 cmake ..에서도 유지됩니다. CMakeLists.txt의 기본값을 ON으로 바꿔도 이미 캐시가 있는 빌드 디렉터리에는 반영되지 않는 이유입니다(문제 3 참고).

플랫폼별 설정

if(WIN32)
    # Windows
    add_definitions(-DWINDOWS)
elseif(APPLE)
    # macOS
    add_definitions(-DMACOS)
elseif(UNIX)
    # Linux
    add_definitions(-DLINUX)
endif()

WIN32는 64비트 Windows에서도 참이고, UNIX는 macOS에서도 참이라 APPLE 검사를 UNIX보다 먼저 두어야 합니다. 위 코드가 그 순서를 지키고 있습니다. 다만 이런 매크로는 대부분 필요 없습니다. 컴파일러가 이미 _WIN32, __APPLE__, __linux__를 정의해 주므로 소스에서 바로 쓸 수 있고, 굳이 정의해야 한다면 add_definitions 대신 target_compile_definitions(myapp PRIVATE MACOS)처럼 타겟 단위로 지정하는 편이 좋습니다.

빌드 타입

# Debug, Release, RelWithDebInfo, MinSizeRel
# 사용자가 지정하지 않았을 때만 기본값을 Release로
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# 사용 (터미널)
# cmake -DCMAKE_BUILD_TYPE=Debug ..

여기에는 흔한 함정이 있습니다. CMakeLists.txt에 set(CMAKE_BUILD_TYPE Release)를 그냥 적으면 일반 변수가 캐시 변수를 가려서, 사용자가 -DCMAKE_BUILD_TYPE=Debug를 넘겨도 무시되고 항상 Release로 빌드됩니다. 디버거에서 브레이크포인트가 안 걸린다는 문의의 상당수가 이 한 줄에서 나옵니다. 그래서 위처럼 비어 있을 때만 기본값을 넣습니다. 또 Visual Studio나 Xcode 같은 멀티 구성 생성기는 CMAKE_BUILD_TYPE을 아예 쓰지 않고 빌드할 때 cmake --build . --config Release로 구성을 고릅니다. CMAKE_CONFIGURATION_TYPES 검사를 넣은 이유가 이것입니다.

설치 규칙

# 실행 파일 설치
install(TARGETS myapp
    DESTINATION bin
)

# 헤더 파일 설치
install(FILES mylib.h
    DESTINATION include
)

# 라이브러리 설치
install(TARGETS mylib
    DESTINATION lib
)

# 사용 (터미널)
# make install
# 또는 설치 위치 지정: cmake --install . --prefix ~/local

make install의 기본 설치 위치(CMAKE_INSTALL_PREFIX)는 Linux에서 /usr/local이라 권한이 없으면 “Permission denied”로 실패합니다. sudo로 밀어붙이기보다 --prefix로 사용자 디렉터리에 설치하거나, 패키지 매니저를 쓰는 편이 시스템을 덜 더럽힙니다. 다른 CMake 프로젝트가 find_package(mylib)로 이 라이브러리를 찾게 하려면 install(EXPORT ...)로 Config 파일까지 설치해야 하는데, 이 부분은 find_package 글에서 다룹니다.

자주 발생하는 문제

문제 1: 헤더 파일을 찾을 수 없음

증상: fatal error: mylib.h: No such file or directory

원인: include 경로 미설정

해결법:

# ❌ 경로 없음
add_executable(myapp main.cpp)

# ✅ 경로 추가
include_directories(${PROJECT_SOURCE_DIR}/include)
add_executable(myapp main.cpp)

# ✅ target 단위로 추가 (더 권장)
target_include_directories(myapp PRIVATE
    ${PROJECT_SOURCE_DIR}/include
)

문제 2: 라이브러리 링크 에러

증상: undefined reference to function

원인: 라이브러리 링크 누락

해결법:

# ❌ 링크 안 됨
add_executable(myapp main.cpp)

# ✅ 라이브러리 링크
add_executable(myapp main.cpp)
target_link_libraries(myapp mylib)

undefined reference는 컴파일이 아니라 링크 단계 에러입니다. 헤더를 include해서 선언은 보이지만, 그 함수의 정의가 들어 있는 오브젝트 파일이나 라이브러리가 링크 명령에 없다는 뜻입니다. 라이브러리를 링크했는데도 나온다면 소스 파일이 add_library 목록에서 빠졌거나, C 라이브러리를 extern "C" 없이 C++에서 호출해 이름 맹글링이 어긋난 경우가 많습니다. 실제로 어떤 명령이 실행됐는지는 cmake --build . --verbose로 확인할 수 있습니다.

문제 3: CMake 캐시 문제

증상: 설정 변경이 반영 안 됨

원인: 캐시된 설정

해결법:

# build 디렉토리 삭제
rm -rf build
mkdir build && cd build
cmake ..

# 또는 캐시만 삭제
rm CMakeCache.txt
cmake ..

유용한 명령어

변수 출력

message(STATUS "CMAKE_CXX_COMPILER: ${CMAKE_CXX_COMPILER}")
message(STATUS "PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")

파일 목록 자동 생성

file(GLOB SOURCES "src/*.cpp")
add_executable(myapp ${SOURCES})

# 재귀적으로
file(GLOB_RECURSE SOURCES "src/*.cpp")

file(GLOB)은 편하지만 CMake 공식 문서도 소스 목록 수집에는 권장하지 않습니다. GLOB은 구성 시점에 한 번만 실행되기 때문에, 새 .cpp 파일을 추가하고 make만 다시 돌리면 새 파일이 빌드에 포함되지 않습니다. 그 결과 새 파일에 정의한 함수가 “undefined reference”로 나오고, cmake ..를 다시 실행해야만 해결됩니다. CMake 3.12부터 CONFIGURE_DEPENDS 옵션을 붙이면 빌드 때마다 목록을 다시 확인하지만, 모든 생성기에서 동작이 보장되지는 않고 빌드마다 약간의 비용이 듭니다. 파일 목록을 명시적으로 적어 두면 코드 리뷰에서 추가·삭제가 diff로 드러난다는 장점도 있습니다.

컴파일러 체크

if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    target_compile_options(myapp PRIVATE -Wall -Wextra)
elseif(MSVC)
    target_compile_options(myapp PRIVATE /W4)
endif()

FAQ

Q1: CMake vs Makefile?

A:

  • CMake: 크로스 플랫폼, 자동 생성
  • Makefile: 플랫폼 종속적, 수동 작성

Q2: out-of-source 빌드란?

A: 소스 디렉토리와 빌드 디렉토리를 분리하는 것입니다. 권장됩니다.

# out-of-source (권장)
mkdir build && cd build
cmake ..

# in-source (비권장)
cmake .

Q3: 어떤 버전의 CMake를 사용해야 하나요?

A: 최소 3.10 이상, 가능하면 최신 버전을 사용하세요.

Q4: find_package는 어떻게 작동하나요?

A: CMake가 시스템에서 라이브러리를 찾아줍니다.

find_package(OpenCV REQUIRED)
target_link_libraries(myapp ${OpenCV_LIBS})

find_package는 두 가지 모드로 동작합니다. Module 모드는 CMake에 포함된(또는 프로젝트가 제공한) FindXxx.cmake 스크립트를 실행하고, Config 모드는 라이브러리가 설치할 때 함께 깔아 둔 XxxConfig.cmake를 찾습니다. 최근 라이브러리는 대부분 Config 모드를 지원하며 Xxx::Xxx 형태의 임포트 타겟을 제공하니, 가능하면 변수 대신 타겟을 링크하세요.

Q5: 여러 빌드 타입을 동시에?

A: 빌드 디렉토리를 여러 개 만드세요.

cmake -S . -B build-debug -DCMAKE_BUILD_TYPE=Debug
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release
cmake --build build-debug

프로젝트 루트에서 -S(소스)와 -B(빌드 디렉터리)를 지정하면 cd로 옮겨 다닐 필요가 없어 경로 실수가 줄어듭니다.

Q6: CMake 학습 리소스는?

A:

  • 공식 문서: cmake.org
  • Modern CMake: cliutils.gitlab.io/modern-cmake
  • 예제: github.com/ttroy50/cmake-examples

같이 보면 좋은 글