CMake 3.28+ 프리셋과 모듈로 크로스 플랫폼 빌드 구성하기

이 글의 핵심

외부 라이브러리 경로가 환경마다 다르고 CI 빌드 설정이 복잡해지는 문제에서 출발합니다. 변수 대신 타겟에 속성을 붙이는 모던 CMake 방식이 왜 전파 문제를 줄이는지, 구성·생성·빌드 단계와 제너레이터 표현식이 어떻게 맞물리는지 이해하고 나면 멀티 플랫폼 프리셋을 CI까지 그대로 가져갈 수 있습니다.

플랫폼마다 다른 빌드 설정을 CMake 하나로

팀에서 겪는 빌드 문제들

C++ 프로젝트를 여러 플랫폼에서 개발하다 보면 이런 문제를 겪습니다:

  • 플랫폼별 빌드 파일 — Windows는 .vcxproj, Linux는 Makefile, macOS는 Xcode 프로젝트를 따로 관리
  • 팀원마다 다른 환경 — 누구는 Visual Studio, 누구는 CLion, 누구는 VS Code
  • 의존성 지옥 — Boost, OpenSSL 등 외부 라이브러리 경로가 환경마다 다름
  • CI/CD 설정 중복 — GitHub Actions, GitLab CI에서 각각 빌드 스크립트 작성
  • 빌드 시간 증가 — 프로젝트가 커질수록 빌드가 느려짐 CMake로 해결:
# ❌ 기존 방법 (플랫폼별 관리)
# Windows: msbuild MyProject.vcxproj
# Linux: make -f Makefile.linux
# macOS: xcodebuild -project MyProject.xcodeproj
# ✅ CMake 방법 (한 번 작성, 어디서나 빌드)
mkdir build && cd build
cmake ..
cmake --build .
flowchart LR
    subgraph input[입력]
        cmake[CMakeLists.txt]
        preset["CMakePresets.json (3.19+)"]
    end
    subgraph cmake_tool[CMake]
        gen[Generator]
    end
    subgraph output[출력]
        make["Makefile (Linux)"]
        vs["Visual Studio (Windows)"]
        xcode["Xcode (macOS)"]
        ninja["Ninja (모든 플랫폼)"]
    end
    cmake --> gen
    preset --> gen
    gen --> make
    gen --> vs
    gen --> xcode
    gen --> ninja

Generator: 플랫폼별 빌드 파일을 만드는 CMake의 역할

빌드 시스템 생성기 (Generator) - CMake의 핵심 역할:

CMake는 직접 빌드하지 않습니다!
→ 플랫폼별 빌드 시스템 파일을 생성하는 메타 빌드 시스템

흐름:
1. CMakeLists.txt 작성 (플랫폼 독립적)
2. cmake 명령 실행 → Generator 선택
3. Generator가 플랫폼 특화 빌드 파일 생성
4. 실제 빌드 도구 (make, msbuild, ninja 등)가 빌드

예시:
cmake -G "Unix Makefiles" ..
→ Makefile 생성 (Linux/macOS)

cmake -G "Visual Studio 17 2022" ..
→ .sln, .vcxproj 생성 (Windows)

cmake -G "Xcode" ..
→ .xcodeproj 생성 (macOS)

cmake -G "Ninja" ..
→ build.ninja 생성 (모든 플랫폼)

Generator 내부 동작:

cmake 명령 실행 시 (Configure + Generate):

1. Configure 단계:
   CMakeLists.txt 파싱
   ↓
   변수 치환: ${PROJECT_NAME}, ${CMAKE_CXX_FLAGS} 등
   ↓
   타겟 정의 수집:
   - add_executable(myapp main.cpp)
   - add_library(mylib mylib.cpp)
   ↓
   의존성 그래프 생성:
   myapp → mylib → pthread
   ↓
   컴파일러 감지:
   - Linux: g++ (GCC), clang++
   - Windows: cl.exe (MSVC)
   - macOS: clang++ (Xcode)
   ↓
   플랫폼 특화 설정:
   - Windows: /MD, /O2, /W4
   - Linux: -O2, -Wall, -fPIC
   ↓
   CMakeCache.txt 생성:
   - 설정 캐싱 (재실행 시 빠름)
   - CMAKE_CXX_COMPILER=/usr/bin/g++
   - CMAKE_BUILD_TYPE=Release
   - 사용자가 직접 수정 가능

2. Generate 단계:
   타겟 의존성 그래프
   ↓
   Generator 실행 (예: Unix Makefiles)
   ↓
   Makefile 생성 (개념을 보여주려고 단순화한 예이며,
   실제 CMake는 CMakeFiles/ 아래에 재귀 Makefile을 만듭니다):
   
   all: myapp
   
   myapp: main.o mylib.a
       g++ -o myapp main.o -L. -lmylib -lpthread
   
   main.o: main.cpp
       g++ -c main.cpp -I./include -std=c++20 -O2
   
   mylib.a: mylib.o
       ar rcs mylib.a mylib.o
   
   mylib.o: mylib.cpp
       g++ -c mylib.cpp -I./include -std=c++20 -O2
   
   clean:
       rm -f *.o myapp mylib.a
   
   install:
       cp myapp /usr/local/bin/

생성된 파일 구조 (build 디렉토리):
build/
├── CMakeCache.txt       ← 설정 캐시
├── CMakeFiles/          ← 내부 메타데이터
│   ├── myapp.dir/       ← myapp 타겟 빌드 정보
│   │   ├── flags.make   ← 컴파일 플래그
│   │   ├── build.make   ← 빌드 규칙
│   │   └── link.txt     ← 링크 명령
│   └── mylib.dir/
├── Makefile             ← 생성된 빌드 파일
└── cmake_install.cmake  ← 설치 스크립트

Generator 종류와 특징:

Unix Makefiles (Linux/macOS 기본):
- 순차적 빌드 (단일 CPU)
- 병렬: make -j$(nproc)
- 디버깅 용이 (Makefile 직접 확인 가능)

Ninja (권장):
- 병렬 빌드 기본
- 빌드 속도 최적화 (증분 빌드 우수)
- 모든 플랫폼 지원
- 설치: apt install ninja-build
- 사용: cmake -G Ninja ..

Visual Studio (Windows):
- .sln (솔루션) + .vcxproj (프로젝트) 생성
- IDE 통합 (IntelliSense, 디버거)
- 멀티 구성 (Debug/Release 한 번에)
- cmake -G "Visual Studio 17 2022" -A x64 ..

Xcode (macOS):
- .xcodeproj 생성
- IDE 통합
- 멀티 구성

Generator 선택 기준:
개발: IDE Generator (Visual Studio, Xcode)
CI/CD: Ninja (빠름)
디버깅: Unix Makefiles (직접 확인 가능)

타겟 기반 빌드:

Modern CMake (3.0+) 철학:
타겟 = 빌드 결과물 + 속성 (의존성, 플래그, 경로)

타겟 정의:
add_executable(myapp main.cpp)
→ myapp 타겟 생성 (실행 파일)

add_library(mylib mylib.cpp)
→ mylib 타겟 생성 (라이브러리)

타겟 속성 설정 (Modern CMake):
target_include_directories(mylib PUBLIC include/)
→ mylib 사용 시 include/ 경로 자동 추가

target_link_libraries(myapp PRIVATE mylib)
→ myapp이 mylib에 의존

target_compile_features(myapp PRIVATE cxx_std_20)
→ C++20 기능 요구

의존성 전파:
myapp → mylib (PRIVATE)
→ myapp 빌드 시에만 mylib 사용

mylib → pthread (PUBLIC)
→ mylib 사용하는 모든 타겟이 pthread도 링크

interface_lib → header_only (INTERFACE)
→ 헤더 전용 라이브러리

타겟 의존성 그래프:

예시:
add_executable(myapp main.cpp)
add_library(mylib mylib.cpp)
add_library(utils utils.cpp)

target_link_libraries(myapp PRIVATE mylib)
target_link_libraries(mylib PUBLIC utils)
target_link_libraries(utils PUBLIC pthread)

의존성 그래프:
       myapp
         ↓ (PRIVATE)
       mylib
         ↓ (PUBLIC)
       utils
         ↓ (PUBLIC)
      pthread

빌드 순서 (CMake 자동 결정):
1. pthread (시스템 라이브러리, 이미 존재)
2. utils.o → libutils.a
3. mylib.o → libmylib.a (정적 라이브러리는 링크하지 않고 묶기만 함)
4. main.o → myapp (mylib 링크)

링크 명령:
g++ -o myapp main.o -lmylib -lutils -lpthread

PRIVATE vs PUBLIC vs INTERFACE:
PRIVATE:
  - 타겟 내부에서만 사용
  - 의존성 전파 안 됨
  - 예: 구현 세부사항

PUBLIC:
  - 타겟 + 의존하는 타겟 모두 사용
  - 의존성 전파
  - 예: 헤더에 노출된 API

INTERFACE:
  - 타겟 자체는 사용 안 함
  - 의존하는 타겟만 사용
  - 예: 헤더 전용 라이브러리
  • 빌드 시스템 생성기: CMake 자체는 빌드를 하지 않으며, 플랫폼별 빌드 시스템(Makefile, .sln 등)을 생성합니다.
  • 타겟 기반: add_executable, add_library로 빌드 타겟을 정의하며, target_link_libraries, target_include_directories로 의존성을 명시합니다.
  • 프리셋 (3.19+): CMakePresets.json으로 빌드 설정을 공유하고 재사용합니다.
  • C++20 모듈 (3.28+): C++20 모듈을 네이티브로 지원합니다. CMake는 C++에서 사실상 표준에 가까운 빌드 생성기입니다. Rust 입문 글의 Cargo나 Node.js의 npm·모듈 해석처럼 빌드와 패키지가 한 도구에 묶여 있지는 않지만, 크로스 플랫폼 빌드와 vcpkg·Conan 연동으로 비슷한 목표를 달성합니다. Go 모듈·go.sum의 의존성 고정, Python의 pip·uv·Poetry의 락·가상환경 개념과 나란히 보면 의존성 관리 차이가 선명해집니다. 언어별 빌드 철학·도구 스택 비교는 C++ 빌드 시스템 완전 비교를 참고하세요. 병렬 빌드·캐시(ccache 등)는 Makefile 가이드와도 이어집니다.

이 글의 예제는 CMake 3.20 이상을 기준으로 하며, C++20 모듈 절은 3.28 이상이 필요합니다.


최소 CMakeLists.txt로 시작하기

Hello World 빌드

# CMakeLists.txt - CMake 설정 파일
# CMake 최소 버전 지정 (3.20 이상 필요)
cmake_minimum_required(VERSION 3.20)
# 프로젝트 정보 설정 (이름, 버전, 언어)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# C++ 표준 설정
set(CMAKE_CXX_STANDARD 20)              # C++20 사용
set(CMAKE_CXX_STANDARD_REQUIRED ON)     # C++20 미지원 시 에러
set(CMAKE_CXX_EXTENSIONS OFF)           # GNU 확장 등 비활성화 (이식성 향상)
# 실행 파일 타겟 생성
# myapp: 타겟 이름 (빌드 결과물 이름)
# main.cpp: 소스 파일
add_executable(myapp main.cpp)
// main.cpp
#include <iostream>
int main() {
    std::cout << "Hello, CMake!" << std::endl;
    return 0;
}
# 빌드 디렉토리 생성 (out-of-source build)
mkdir build
cd build

# CMake 실행 (빌드 시스템 생성)
cmake ..
# 내부 동작:
# 1. CMakeLists.txt 파싱
# 2. Generator 선택 (플랫폼별 기본값)
#    - Linux: Unix Makefiles
#    - Windows: Visual Studio 2022
#    - macOS: Unix Makefiles
# 3. 빌드 파일 생성
#    - Makefile (Linux)
#    - .sln + .vcxproj (Windows)
#    - .xcodeproj (macOS with Xcode)
# 4. CMakeCache.txt 생성 (설정 캐시)

# 빌드 (생성된 빌드 시스템으로 컴파일)
cmake --build .
# 내부 동작:
# - Linux: make를 호출
# - Windows: msbuild를 호출
# - Ninja: ninja를 호출

# 실행
./myapp  # Linux/macOS
# myapp.exe  # Windows

CMake 빌드 프로세스 상세:

단계별 흐름:

1. Configuration Phase (cmake ..)
   ┌─────────────────────────────────────┐
   │ CMakeLists.txt 파싱                  │
   │ - project() 실행 → 프로젝트 메타데이터  │
   │ - add_executable() → 타겟 등록       │
   │ - target_link_libraries() → 의존성   │
   ├─────────────────────────────────────┤
   │ Generator 선택                       │
   │ - 명시: cmake -G "Ninja" ..         │
   │ - 자동: 플랫폼 기본값                 │
   ├─────────────────────────────────────┤
   │ Compiler Detection                  │
   │ - CMAKE_CXX_COMPILER 탐지            │
   │ - 컴파일러 feature 테스트             │
   │ - ABI 확인 (32/64bit)               │
   ├─────────────────────────────────────┤
   │ Cache 생성: CMakeCache.txt          │
   │ - 모든 변수 저장                     │
   │ - 다음 실행 시 재사용                │
   └─────────────────────────────────────┘
   
2. Generation Phase
   ┌─────────────────────────────────────┐
   │ 빌드 파일 생성                        │
   │ Unix Makefiles:                     │
   │   - Makefile                        │
   │   - cmake_install.cmake             │
   │ Visual Studio:                      │
   │   - MyProject.sln                   │
   │   - myapp.vcxproj                   │
   │ Ninja:                              │
   │   - build.ninja                     │
   ├─────────────────────────────────────┤
   │ compile_commands.json (옵션)        │
   │ - CMAKE_EXPORT_COMPILE_COMMANDS=ON  │
   │ - LSP(clangd 등) 지원               │
   └─────────────────────────────────────┘

3. Build Phase (cmake --build .)
   ┌─────────────────────────────────────┐
   │ 생성된 빌드 시스템 실행               │
   │ - make (Unix Makefiles)             │
   │ - msbuild (Visual Studio)           │
   │ - ninja (Ninja)                     │
   ├─────────────────────────────────────┤
   │ 의존성 그래프 실행                   │
   │ - 헤더 변경 → 소스 재컴파일           │
   │ - 라이브러리 변경 → 실행파일 재링크    │
   │ - 병렬 빌드 (make -j, ninja 자동)    │
   └─────────────────────────────────────┘

CMakeCache.txt의 역할:
- 첫 실행 시 모든 변수를 저장
- 다음 실행 시 캐시를 읽어 빠르게 구성
- 수동 편집 가능 (ccmake, cmake-gui도 이용 가능)
- 삭제 시 완전히 새로 구성 (권장: rm -rf build/)

Generator 종류:
- Unix Makefiles: 전통적, 느림
- Ninja: 빠른 빌드, 병렬 처리 우수 (권장!)
- Visual Studio: Windows IDE 통합
- Xcode: macOS IDE 통합
- Ninja Multi-Config: 여러 빌드 타입을 한 빌드 디렉토리에

최소 예제에 쓰인 명령어

명령어설명
cmake_minimum_required(VERSION 3.20)최소 CMake 버전 지정
project(MyProject VERSION 1.0.0)프로젝트 이름과 버전 설정
set(CMAKE_CXX_STANDARD 20)C++20 표준 사용
set(CMAKE_CXX_EXTENSIONS OFF)GNU 확장 등 비활성화 (이식성 향상)
add_executable(myapp main.cpp)실행 파일 타겟 생성
cmake ..부모 디렉토리의 CMakeLists.txt로 빌드 시스템 생성
cmake --build .플랫폼 독립적 빌드 명령

CMake 버전별 주요 기능

버전주요 기능
3.19CMakePresets.json 도입
3.20C++23 지원
3.23FILE_SET 도입 (헤더 관리 개선)
3.25LINUX 변수, SYSTEM 타겟 속성 추가
3.28C++20 모듈 네이티브 지원

실행 파일과 라이브러리 타겟

실행 파일

# 단일 소스
add_executable(myapp main.cpp)
# 여러 소스
add_executable(myapp
    src/main.cpp
    src/utils.cpp
    src/config.cpp
)
# 변수 사용
set(SOURCES
    src/main.cpp
    src/utils.cpp
)
add_executable(myapp ${SOURCES})

라이브러리

# 정적 라이브러리 (.a, .lib)
add_library(mylib STATIC
    src/lib.cpp
    src/helper.cpp
)
# 동적 라이브러리 (.so, .dll)
add_library(mylib SHARED
    src/lib.cpp
    src/helper.cpp
)
# 헤더 전용 라이브러리
add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE include)
# 실행 파일에 링크
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)

SHARED는 런타임에 .so/.dll 경로가 잡혀야 하므로 설치·배포 규칙(RUNTIME DESTINATION 등)을 함께 설계하세요.

PUBLIC vs PRIVATE vs INTERFACE

flowchart TB
    subgraph LibA[라이브러리 A]
        A_PRIVATE["PRIVATE 헤더<br/>src/internal/"]
        A_PUBLIC["PUBLIC 헤더<br/>include/"]
    end
    
    subgraph LibB[라이브러리 B]
        B_CODE[B의 코드]
    end
    
    subgraph App[애플리케이션]
        APP_CODE[App 코드]
    end
    
    A_PRIVATE -.->|사용 가능| LibA
    A_PUBLIC -->|사용 가능| LibA
    A_PUBLIC -->|사용 가능| LibB
    A_PUBLIC -->|사용 가능| App
    
    LibB -->|링크| LibA
    App -->|링크| LibB
    
    style A_PRIVATE fill:#FFB6C1
    style A_PUBLIC fill:#90EE90
# PRIVATE: 타겟 자신만 사용
target_include_directories(mylib PRIVATE src/internal)
# PUBLIC: 타겟 + 이 타겟을 링크하는 타겟도 사용
target_include_directories(mylib PUBLIC include)
# INTERFACE: 이 타겟을 링크하는 타겟만 사용 (헤더 전용 라이브러리)
target_include_directories(mylib INTERFACE include)

실무 예시: mylib가 내부적으로 src/internal/impl.h를 쓰면 PRIVATE로 추가하며, 외부에 노출하는 include/mylib/api.h는 PUBLIC으로 추가합니다. myapp이 mylib를 링크하면 PUBLIC 헤더만 접근할 수 있습니다.

구성·생성·빌드 단계와 속성 전파의 내부 동작

이 절에서는 위에서 요약한 Generator, PUBLIC/PRIVATE/INTERFACE를 넘어, CMake가 실제로 언제 무엇을 평가하는지(구성·생성·빌드), 사용 요구사항(usage requirements) 이 타겟 그래프를 따라 어떻게 전파되는지, 제너레이터 표현식으로 어떤 제약을 풀 수 있는지, 그리고 저장소·CI·배포 관점의 프로덕션 패턴을 정리합니다. CMakeLists.txt는 “빌드 스크립트”가 아니라 구성 시점에 실행되는 DSL이라는 점을 염두에 두면 이후 디버깅이 수월해집니다.

PRIVATE include 경로가 소비자에 전파되지 않아 나는 오류, 오타 난 타겟 이름이 링크 단계에서 드러나는 오류, 공유 라이브러리 순환 오류, 공백이 든 제너레이터 표현식의 결과, 프리셋 version 오류

그림: 실제 실행 결과 — PRIVATE로 추가한 헤더 경로는 라이브러리를 링크한 쪽에서 api.h를 찾지 못한다. 존재하지 않는 타겟 이름은 구성 단계는 통과하고 링크 단계에서 library not found가 된다. 공유 라이브러리끼리의 순환은 구성 단계에서 막힌다. 공백이 든 $CONFIG:Debug:-O0 -g 표현식은 이 버전(3.30.3)에서는 오류 없이 -O0 -g로 풀렸다. CMakePresets.json의 version이 지원 범위를 넘으면 프리셋을 읽지 못한다. (환경: macOS 27.0.1 · CMake 3.30.3 · Apple clang 21.0.0)

구성(Configure)·생성(Generate)·빌드(Build) 단계

CMake는 한 번의 cmake -S … -B … 호출 안에서 대략 다음 순서로 동작합니다.

  1. Configure(구성): 최상위 CMakeLists.txt부터 add_subdirectory·FetchContent 등을 따라가며 CMake 스크립트를 실행합니다. 이때 project(), add_library(), target_link_libraries() 등이 호출되고, 타겟과 속성, 의존성 그래프가 메모리 상에 구축됩니다. CMakeCache.txt에 캐시 변수가 기록되고, 컴파일러 검사(try_compile 등)가 이 단계에서 수행될 수 있습니다.
  2. Generate(생성): 선택한 Generator(Ninja, Visual Studio, Xcode, Makefiles 등)에 맞게 실제 빌드 도구가 읽을 파일(build.ninja, .vcxproj, Makefile 등)을 씁니다. 이 단계에서 대부분의 제너레이터 표현식이 풀려, 설정별(Debug/Release 등) 컴파일·링크 줄이 확정됩니다.
  3. Build(빌드): cmake --build가 호출되면, 생성된 Ninja/Make/MSBuild가 컴파일러·링커를 실행합니다. add_custom_command의 빌드 시점 규칙, file(GENERATE)로 만든 파일 등은 이 단계와 맞물립니다.

중요한 구분: if(UNIX) 같은 조건은 구성 시점에 평가됩니다. 반면 $<CONFIG:Debug> 같은 제너레이터 표현식은 구성 시점의 if() 안에서 그대로 해석되지 않습니다(아래 제너레이터 절 참고). “왜 CMakeLists에서는 맞는데 생성된 빌드 파일은 다르지?” 같은 혼란의 상당수가 평가 시점 착오에서 옵니다.

싱글 구성 vs 멀티 구성 Generator: Ninja/Make 계열은 보통 한 빌드 트리에 하나의 CMAKE_BUILD_TYPE을 가정하는 경우가 많고(싱글 구성), Visual Studio·Xcode는 한 트리에 Debug/Release 등 여러 구성을 동시에 두는 멀티 구성입니다. 따라서 $<CONFIG:…>·OUTPUT_NAME·디버그 postfix 등은 멀티 구성에서 특히 중요합니다.

cmake --install: 빌드 산출물을 CMAKE_INSTALL_PREFIX 아래에 배치하는 단계로, 설치 규칙(install(TARGETS …), install(EXPORT …))이 여기서 소비됩니다. 라이브러리를 다른 프로젝트의 find_package로 제공하려면 이 경로와 exported target 설계가 핵심입니다.

타겟 속성 전파와 PRIVATE / PUBLIC / INTERFACE

Modern CMake에서 target_link_libraries(A PRIVATE|PUBLIC|INTERFACE B)는 단순히 “링크 순서”가 아니라 사용 요구사항(usage requirements)의 전파 규칙을 정합니다.

  • PRIVATE: A가 자기 자신을 빌드할 때만 필요한 의존성입니다. B로부터 오는 include 경로·compile definition·대부분의 링크 정보는 A의 구현 단위에만 적용되고, A에 링크하는 상위 타겟으로는 넘어가지 않습니다.
  • PUBLIC: A를 사용하는 쪽( A의 헤더/API를 쓰는 클라이언트)도 동일한 요구사항이 필요할 때 씁니다. 예: A의 공개 헤더가 B의 헤더를 포함한다면, B의 include와 링크 요구사항이 전이(transitive) 되어야 합니다.
  • INTERFACE: A 자체는 빌드 산출물이 없거나(헤더 전용 인터페이스 라이브러리), 요구사항만 전달하는 역할일 때 씁니다. INTERFACE로 연결된 항목은 의존하는 타겟에 전파됩니다.

전파되는 것의 예: INTERFACE_INCLUDE_DIRECTORIES, INTERFACE_COMPILE_DEFINITIONS, INTERFACE_LINK_LIBRARIES 등은 타겟 속성으로 저장되며, 링크 그래프를 따라 누적됩니다. 직접 링크한 타겟뿐 아니라 전이 의존성에 의해 컴파일·링크 줄에 반영될 수 있으므로, “내가 target_link_libraries에 쓰지 않았는데 플래그가 붙는다”는 현상은 종종 PUBLIC 체인 때문입니다.

PRIVATE로 링크해야 하는 경우: 의존성이 구현 디테일이고 공개 헤더에 노출되지 않을 때입니다. 반대로 API에 노출되면 PUBLIC이 맞으며, 그렇지 않으면 캡슐화가 깨져 불필요한 재컴파일·ABI 노출이 커집니다.

$<LINK_ONLY:…>는 링크 의존성만 전파하고 include 경로나 컴파일 정의 같은 나머지 사용 요구사항은 전파하지 않는 제너레이터 표현식입니다. 직접 쓰는 일은 드물고, 정적 라이브러리가 다른 라이브러리를 PRIVATE로 링크하면 CMake가 INTERFACE_LINK_LIBRARIES에 이 형태로 자동 기록합니다. 생성된 Config 파일이나 오류 메시지에서 LINK_ONLY가 보이면 이 경우입니다. (CMake 3.24에서 추가된 것은 $<LINK_LIBRARY:…>, $<LINK_GROUP:…>이며 LINK_ONLY는 그보다 훨씬 오래된 기능입니다.)

PUBLIC과 PRIVATE를 고르는 기준은 한 문장으로 줄일 수 있습니다. 이 라이브러리의 공개 헤더가 그 의존성을 필요로 하는가? 공개 헤더가 #include <boost/filesystem.hpp>를 한다면 PUBLIC, .cpp 안에서만 쓰면 PRIVATE입니다. 공개 헤더가 쓰는데 PRIVATE로 두면 라이브러리 자체는 빌드되지만 소비자 쪽에서 fatal error: boost/filesystem.hpp: No such file or directory가 납니다. 또 “PRIVATE로 바꿨는데 왜 링크 줄에 여전히 보이지?”라는 의문은 정상 동작입니다. 정적 라이브러리 libb.a에는 liba의 코드가 들어 있지 않으므로, CMake는 PRIVATE 의존성이라도 최종 링크에 liba를 자동으로 붙입니다(내부적으로 $<LINK_ONLY:liba>로 기록). PRIVATE가 막는 것은 헤더 경로와 컴파일 옵션의 전파입니다.

target_link_libraries에 넘긴 이름이 타겟이 아니면 CMake는 그것을 링커 인자(-lmylibb)로 취급하므로, 타겟 이름 오타가 구성 단계가 아니라 링크 단계의 cannot find -lmylibb로 늦게 드러납니다. add_library(MyProject::mylib ALIAS mylib)처럼 ::가 들어간 별칭을 쓰면 반드시 타겟이어야 한다는 규칙 때문에 오타가 구성 단계 에러로 바로 잡힙니다. 공유 라이브러리끼리 서로 링크하는 순환은 Cyclic dependencies are allowed only among static libraries. 에러가 납니다. 정적 라이브러리 사이의 순환은 CMake가 링크 줄에 반복해 넣는 방식으로 허용하므로, 정적 빌드에서 멀쩡하던 프로젝트가 BUILD_SHARED_LIBS=ON으로 바꾸는 순간 실패하곤 합니다. 공통 코드를 별도 라이브러리로 빼서 순환을 끊는 것이 해결책입니다.

제너레이터 표현식(Generator Expressions)

제너레이터 표현식은 $<…> 형태로, 구성 시점의 if()와는 다른 규칙으로 평가됩니다. 대표적으로:

  • 구성·타겟별: $<CONFIG:Debug>, $<PLATFORM_ID:Windows>, $<CXX_COMPILER_ID:GNU> 등 — 멀티 구성·크로스 컴파일에서 조건 분기를 안전하게 만듭니다.
  • 타겟 정보: $<TARGET_FILE:tgt>, $<TARGET_PROPERTY:tgt,PROP> — 설치 스크립트·add_custom_command·file(GENERATE)에서 경로를 타겟 기준으로 얻을 때 유용합니다.
  • 논리·조건: $<IF:cond,then,else>, $<AND:…>, $<OR:…> — 복잡한 플래그 조합을 한 속성에 담을 때 사용합니다.

흔한 실수: if($<CONFIG:Debug>)처럼 제너레이터 표현식을 if()에 넣으면 구성 시점에는 $<CONFIG:…>가 아직 “문자 그대로”로 다뤄져 의도와 다르게 동작할 수 있습니다. 조건 분기가 구성 시점이면 일반 if(CMAKE_BUILD_TYPE …) 또는 generator expressions가 아닌 캐시 변수를 쓰며, 생성 시점이면 target_compile_options 등에 제너레이터 표현식을 그대로 넣는 패턴이 맞습니다.

디버깅 팁: cmake --build … --verbose로 실제 컴파일/링크 명령을 확인하며, 생성된 build.ninja나 compile_commands.json에서 어떤 플래그가 어느 타겟에 붙었는지를 역추적합니다.

예시: 멀티 구성·컴파일러별로 플래그를 나누되, if() 대신 타겟 속성에 제너레이터 표현식을 둡니다.

add_library(core STATIC src/core.cpp)
target_compile_options(core PRIVATE
  $<$<CXX_COMPILER_ID:MSVC>:/W4>
  "$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall;-Wextra>"
)
target_compile_definitions(core PRIVATE
  $<$<CONFIG:Debug>:CORE_DEBUG=1>
)

제너레이터 표현식 안에 공백을 넣은 $<$<CONFIG:Debug>:-O0 -g>는 CMake 3.30.3에서 시험했더니 오류 없이 Debug에서 -O0 -g로 풀리고 Release에서는 비워졌습니다(위 실행 화면). 그래도 표현식이 길어지면 인자 경계가 헷갈리기 쉬우므로 위 예제처럼 표현식 전체를 따옴표로 감싸고 여러 값은 세미콜론으로 구분하는 쪽이 안전합니다. 더 낮은 버전의 CMake에서는 확인하지 않았으므로, 오래된 CMake를 지원해야 한다면 따옴표 형태를 쓰세요.

모던 CMake 패턴: “변수가 아니라 타겟”

과거 스타일의 include_directories(${FOO_INCLUDE_DIRS}), link_libraries(…), 전역 add_definitions(-DUSE_X)는 전 타겟에 영향을 주고 캐시·서브프로젝트와 충돌하기 쉽습니다. 모던 CMake의 기본기는 다음과 같습니다.

  • 타겟 단위로 한정: target_include_directories, target_compile_definitions, target_compile_options, target_link_libraries로 영향 범위를 타겟에 묶습니다.
  • 인터페이스 라이브러리로 요구사항 묶기: 헤더 전용·플래그 번들·외부 패키지 래핑을 add_library(x INTERFACE) + target_include_directories(x INTERFACE …)로 표현하면, 소비자는 target_link_libraries(app PRIVATE x) 한 줄로 일관된 usage requirements를 가져갑니다.
  • 표준·경고도 타겟별: 전역 CMAKE_CXX_FLAGS에 손대기보다 target_compile_features와 target_compile_options로 필요한 타겟만 C++ 표준·경고 정책을 올립니다.
  • find_package 결과를 Imported Target으로: Foo::foo 같은 타겟이 있으면 변수 나열 대신 타겟 링크가 우선입니다. 없는 레거시 Find 모듈만 변수 경로로 연결합니다.

이렇게 해 두면 서브디렉터리 추가 순서에 덜 민감하며, 정적·동적 라이브러리 전환, 테스트 타겟만 sanitizer 적용 같은 시나리오가 단순해집니다.

운영 환경에서의 CMake 패턴 보강

로컬에서만 돌아가는 설정과, 팀·CI·배포까지 고려한 설정은 다릅니다. 자주 쓰는 보강 패턴:

  • 재현 가능한 구성: CMakePresets.json으로 컴파일러·캐시·generator를 고정하며, CI에서는 동일 프리셋을 사용합니다. 개인 실험은 CMakeUserPresets.json(저장소에서 제외)로 분리합니다.
  • 설치·내보내기: install(TARGETS … EXPORT), install(EXPORT …), export(EXPORT …)로 네임스페이스 타겟(MyProj::core)을 제공하면, 소비자는 find_package(MyProj) 후 target_link_libraries(app PRIVATE MyProj::core)만으로 끝낼 수 있습니다. 버전 호환은 write_basic_package_version_file과 함께 쓰는 경우가 많습니다.
  • 의존성 공급 방식 명시: 동일 라이브러리에 대해 시스템 패키지 / vcpkg / Conan / FetchContent를 혼용하면 중복 심볼·헤더 버전 충돌이 납니다. 팀 규약으로 “CI와 릴리스는 vcpkg toolchain”처럼 한 경로를 기본으로 정하는 것이 안전합니다.
  • FetchContent: 빌드 편의는 좋지만 네트워크·버전 고정·오프라인 빌드 이슈가 있습니다. 태그·GIT_SHALLOW·OVERRIDE_FIND_PACKAGE(프로젝트 정책에 맞을 때) 등으로 재현성을 확보합니다.
  • 테스트: enable_testing() + add_test() + CTest, 프리셋의 testPresets로 동일한 빌드 산출물에 대해 CI에서 반복 실행합니다.
  • 캐시 무효화: “이상하면 빌드 디렉터리 삭제”가 여전히 유효합니다. CMake 3.24+의 cmake --fresh는 캐시를 비우고 재구성할 때 유용합니다.

위 패턴들은 프로덕션 패턴 절의 예제와 겹치지 않게 동작 원리·평가 시점 관점에서 보완한 것입니다. 실무에서는 cmake -B build -S . --trace-expand로 의심 구간만 추적하거나, Ninja를 쓰면 build.ninja에서 최종 플래그를 직접 확인하는 것이 가장 빠른 검증입니다.


CMakePresets.json으로 빌드 설정 공유하기 (3.19+)

프리셋이 해결하는 문제

문제: 팀원마다 다른 CMake 명령어를 사용합니다.

# 개발자 A
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++ ..
# 개발자 B
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ -DENABLE_TESTS=ON ..
# CI/CD
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=......

긴 cmake 한 줄을 문서에 복붙하는 방식은 깨지기 쉬우니, 아래 프리셋으로 옮기는 것이 목적입니다. 해결: CMakePresets.json으로 빌드 설정을 공유합니다.

CMakePresets.json 기본 구조

{
  "version": 6,
  "cmakeMinimumRequired": {
    "major": 3,
    "minor": 25,
    "patch": 0
  },
  "configurePresets": [
    {
      "name": "default",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "20",
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    },
    {
      "name": "debug",
      "displayName": "Debug Build",
      "description": "Debug build with sanitizers",
      "inherits": "default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined"
      }
    },
    {
      "name": "release",
      "displayName": "Release Build",
      "description": "Optimized release build",
      "inherits": "default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    },
    {
      "name": "windows-msvc",
      "displayName": "Windows MSVC",
      "inherits": "default",
      "generator": "Visual Studio 17 2022",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "debug",
      "configurePreset": "debug",
      "configuration": "Debug"
    },
    {
      "name": "release",
      "configurePreset": "release",
      "configuration": "Release",
      "jobs": 8
    }
  ],
  "testPresets": [
    {
      "name": "default",
      "configurePreset": "debug",
      "output": {
        "outputOnFailure": true
      }
    }
  ]
}

프리셋 사용법

# 사용 가능한 프리셋 확인
cmake --list-presets
# 프리셋으로 구성
cmake --preset=debug
# 프리셋으로 빌드
cmake --build --preset=debug
# 프리셋으로 테스트
ctest --preset=default

실전 프리셋 예제: 멀티 플랫폼

{
  "version": 6,
  "configurePresets": [
    {
      "name": "linux-gcc",
      "displayName": "Linux GCC",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/linux-gcc",
      "cacheVariables": {
        "CMAKE_CXX_COMPILER": "g++",
        "CMAKE_BUILD_TYPE": "Release"
      },
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Linux"
      }
    },
    {
      "name": "linux-clang",
      "displayName": "Linux Clang",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/linux-clang",
      "cacheVariables": {
        "CMAKE_CXX_COMPILER": "clang++",
        "CMAKE_BUILD_TYPE": "Release"
      },
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Linux"
      }
    },
    {
      "name": "macos",
      "displayName": "macOS",
      "generator": "Xcode",
      "binaryDir": "${sourceDir}/build/macos",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Darwin"
      }
    },
    {
      "name": "windows",
      "displayName": "Windows MSVC",
      "generator": "Visual Studio 17 2022",
      "binaryDir": "${sourceDir}/build/windows",
      "architecture": "x64",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      }
    }
  ]
}

조건 프리셋이 많아지면 이름 규칙을 팀 문서로 고정하지 않으면 “어떤 프리셋이 공식인지” 혼란이 생깁니다.

CI/CD에서 프리셋 활용

# .github/workflows/build.yml
name: Build
on: [push, pull_request]
jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        preset: [debug, release]
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Install CMake
        uses: lukka/get-cmake@latest
      
      - name: Configure
        run: cmake --preset=${{ matrix.preset }}
      
      - name: Build
        run: cmake --build --preset=${{ matrix.preset }}
      
      - name: Test
        if: matrix.preset == 'debug'
        run: ctest --preset=default

testPresets의 default는 debug 구성 프리셋을 가리키므로, 테스트 단계는 debug 행렬에서만 실행해야 아직 빌드되지 않은 디렉터리를 테스트하려다 실패하지 않습니다. 또 위 debug 프리셋은 -fsanitize=address,undefined를 CMAKE_CXX_FLAGS에 넣기 때문에 MSVC에서는 그대로 쓸 수 없습니다(MSVC는 /fsanitize=address만 지원). Windows 행렬은 별도 프리셋으로 나누는 편이 현실적입니다.

사용자별 프리셋 (CMakeUserPresets.json)

팀 공유 설정은 CMakePresets.json, 개인 설정은 CMakeUserPresets.json에 작성합니다. CMakeUserPresets.json은 .gitignore에 추가해 두는 파일입니다. JSON이라 파일 안에는 주석을 넣을 수 없습니다.

{
  "version": 6,
  "configurePresets": [
    {
      "name": "my-dev",
      "inherits": "debug",
      "cacheVariables": {
        "CMAKE_PREFIX_PATH": "/home/myuser/local",
        "ENABLE_EXPERIMENTAL_FEATURES": "ON"
      }
    }
  ]
}

CMakeUserPresets.json은 .gitignore에 넣되, 팀원 온보딩용 예시 파일(CMakeUserPresets.json.example)은 두는 편이 좋습니다.


find_package와 FetchContent로 외부 라이브러리 가져오기

find_package의 내부 동작 원리:

find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)의 동작:

1. Module 모드 검색:
   CMAKE_MODULE_PATH에서 FindBoost.cmake 검색
   → /usr/share/cmake/Modules/FindBoost.cmake 발견
   → FindBoost.cmake 실행
   
2. Config 모드 검색 (Module 모드 실패 시):
   CMAKE_PREFIX_PATH, CMAKE_SYSTEM_PREFIX_PATH에서 검색:
   - <prefix>/lib/cmake/Boost/BoostConfig.cmake
   - <prefix>/lib/cmake/boost-1.70.0/BoostConfig.cmake
   - <prefix>/share/cmake/Boost/BoostConfig.cmake
   - <prefix>/Boost*/BoostConfig.cmake
   
   일반적 경로:
   - Linux: /usr, /usr/local
   - Windows: C:/Program Files/Boost
   - macOS: /usr/local, /opt/homebrew
   
3. BoostConfig.cmake 파싱:
   - 설치된 Boost 버전 확인 (1.70 이상?)
   - COMPONENTS 검색 (filesystem, system)
   - IMPORTED 타겟 생성:
     * Boost::filesystem
     * Boost::system
   - 각 타겟에 속성 설정:
     * INTERFACE_INCLUDE_DIRECTORIES (헤더 경로)
     * INTERFACE_LINK_LIBRARIES (링크 라이브러리)
     * INTERFACE_COMPILE_DEFINITIONS (전처리 매크로)
   
4. REQUIRED 옵션:
   - 실패 시 즉시 에러, 구성 중단
   - 옵션이 없으면 Boost_FOUND 변수만 설정 (조건 분기 가능)

5. target_link_libraries(myapp PRIVATE Boost::filesystem):
   - Boost::filesystem의 모든 속성을 myapp에 전파
   - 헤더 경로 자동 추가 (target_include_directories 불필요)
   - 링크 라이브러리 자동 추가 (libboost_filesystem.a)
   - 의존성 체인 처리 (Boost::filesystem → Boost::system)

find_package 두 가지 모드:

Module 모드:
- FindXxx.cmake 파일 실행 (CMake 제공 또는 프로젝트 제공)
- 변수 기반: Xxx_FOUND, Xxx_INCLUDE_DIRS, Xxx_LIBRARIES
- 오래된 방식, 일부 패키지에만 사용

Config 모드 (권장):
- XxxConfig.cmake 파일 실행 (패키지 제공)
- IMPORTED 타겟 기반: Xxx::Xxx
- 최신 방식, 속성 전파 자동
- 패키지 설치 시 함께 설치됨

Boost 예제 (Config 모드)

Boost 1.70부터는 Boost가 직접 BoostConfig.cmake를 설치하고, CMake 3.30부터는 CMake에 들어 있던 FindBoost 모듈이 정책 CMP0167로 제거 대상이 되었습니다. 그래서 최신 환경에서는 아래 호출이 Config 모드로 동작합니다.

find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)

# 내부 동작:
# 1. BoostConfig.cmake 검색 및 실행
# 2. IMPORTED 타겟 생성:
#    - Boost::filesystem (헤더 경로, 링크 라이브러리 포함)
#    - Boost::system
# 3. Boost_FOUND = TRUE 설정

add_executable(myapp main.cpp)

# PRIVATE Boost::filesystem 링크:
# - myapp만 Boost 사용 (외부에 노출 안 함)
# - 헤더 경로 자동 추가 (Boost 헤더가 /usr/include/boost/에 있으면 include 경로는 /usr/include이며, 시스템 경로라 보통 생략됨)
# - 링크 라이브러리 자동 추가: -lboost_filesystem -lboost_system
target_link_libraries(myapp PRIVATE Boost::filesystem Boost::system)

OpenSSL 예제 (Module 모드)

find_package(OpenSSL REQUIRED)

# 내부 동작:
# 1. FindOpenSSL.cmake 실행 (CMake 제공)
# 2. 시스템에서 OpenSSL 검색:
#    - Linux: /usr/lib/libssl.so, /usr/include/openssl
#    - Windows: C:/Program Files/OpenSSL-Win64
#    - macOS: /usr/local/opt/openssl@3
# 3. IMPORTED 타겟 생성:
#    - OpenSSL::SSL (libssl)
#    - OpenSSL::Crypto (libcrypto)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto)

시스템 OpenSSL과 번들 OpenSSL이 섞이면 런타임 크래시가 날 수 있으므로, 배포 시 동일 빌드를 쓰는지 확인하세요.

찾기 실패 시 처리

find_package(SomeLib)
if(SomeLib_FOUND)
    target_link_libraries(myapp PRIVATE SomeLib::SomeLib)
else()
    message(WARNING "SomeLib not found, using fallback")
endif()

pkg-config 사용

find_package(PkgConfig REQUIRED)
pkg_check_modules(CURL REQUIRED IMPORTED_TARGET libcurl)
target_link_libraries(myapp PRIVATE PkgConfig::CURL)

IMPORTED_TARGET을 주면 PkgConfig::<접두사> 타겟이 만들어져 include 경로와 링크 플래그가 함께 전파됩니다. 변수(CURL_LIBRARIES, CURL_INCLUDE_DIRS)를 직접 쓰는 방식은 라이브러리 경로가 -L 디렉터리로만 들어오는 경우가 있어 링크가 깨지기 쉽습니다. curl은 find_package(CURL)로 CURL::libcurl 타겟도 제공합니다.

FetchContent로 의존성 자동 다운로드 (3.11+)

include(FetchContent)
# GoogleTest 자동 다운로드
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
# 사용 가능하게 만들기
FetchContent_MakeAvailable(googletest)
# 링크
add_executable(tests test.cpp)
target_link_libraries(tests PRIVATE gtest_main)

관련 글: vcpkg나 Conan을 사용하면 더 체계적인 패키지 관리가 가능합니다. GIT_REPOSITORY 대신 압축 파일 URL로 받을 때는 CMake 3.24부터 압축 해제한 파일의 타임스탬프를 어떻게 둘지 정하라는 경고(CMP0135)가 나옵니다. DOWNLOAD_EXTRACT_TIMESTAMP는 URL 다운로드에만 의미가 있는 옵션입니다.

FetchContent_Declare(
    fmt
    URL https://github.com/fmtlib/fmt/archive/refs/tags/10.0.0.tar.gz
    DOWNLOAD_EXTRACT_TIMESTAMP ON  # 3.24+, 압축 해제 시각으로 타임스탬프 설정
)

FetchContent는 네트워크 의존이므로 SBOM·보안 스캔 프로세스와 함께 쓸지 결정해야 합니다.


CMake 3.28+의 C++20 모듈 지원

C++20 모듈이란?

헤더는 #include하는 모든 번역 단위에서 다시 파싱되고, 헤더 안의 매크로가 포함한 쪽 코드로 새어 나갑니다. 모듈은 인터페이스를 한 번 컴파일해 BMI(바이너리 모듈 인터페이스)로 만들고, 가져가는 쪽은 그 결과를 읽으므로 반복 파싱이 줄고 export한 이름만 보입니다. 대신 모듈끼리 순환 import는 허용되지 않고, 빌드 시스템이 소스를 먼저 스캔해 모듈 간 컴파일 순서를 정해야 합니다. CMake 3.28이 하는 일이 바로 이 스캔과 순서 결정입니다.

CMake 3.28+에서 모듈 사용

cmake_minimum_required(VERSION 3.28)
project(ModuleExample CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 모듈 지원 활성화
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
# 라이브러리 (모듈 포함)
add_library(mylib)
target_sources(mylib
    PUBLIC
        FILE_SET CXX_MODULES FILES
            src/mymodule.cppm  # 모듈 인터페이스 파일
    PRIVATE
        src/impl.cpp
)
# 실행 파일
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)

모듈 파일 예제

// src/mymodule.cppm (모듈 인터페이스)
export module mymodule;
export namespace mylib {
    int add(int a, int b);
    int multiply(int a, int b);
}
// src/impl.cpp (모듈 구현)
module mymodule;
namespace mylib {
    int add(int a, int b) {
        return a + b;
    }
    
    int multiply(int a, int b) {
        return a * b;
    }
}

구현 파일에서 잘못된 module 선언은 링크·가시성 오류로 이어지므로, 인터페이스와 구현 쌍을 한 세트로 관리하세요.

// main.cpp (모듈 사용)
import mymodule;
#include <iostream>
int main() {
    std::cout << mylib::add(2, 3) << std::endl;       // 5
    std::cout << mylib::multiply(4, 5) << std::endl;  // 20
    return 0;
}

모듈 + 헤더 혼용

add_library(mylib)
target_sources(mylib
    PUBLIC
        FILE_SET HEADERS FILES
            include/legacy.h  # 기존 헤더
        FILE_SET CXX_MODULES FILES
            src/newmodule.cppm  # 새 모듈
    PRIVATE
        src/legacy.cpp
        src/newmodule.cpp
)
target_include_directories(mylib PUBLIC include)

컴파일러별 모듈 지원 상황

CMake 공식 문서(cmake-cxxmodules) 기준으로 CMake의 모듈 의존성 스캔을 쓸 수 있는 최소 조건은 다음과 같습니다.

컴파일러CMake 모듈 스캔 지원 최소 버전
MSVCVisual Studio 2022 17.4 (툴셋 14.34)
Clang16
GCC14

제너레이터는 Ninja 1.11 이상 또는 Visual Studio 2022가 필요하고, 3.28 기준으로 Makefile 제너레이터는 지원되지 않습니다. 또한 컴파일러가 모듈 스캔을 지원한다는 것과 표준 라이브러리 모듈(import std;)이나 헤더 유닛까지 문제없이 쓸 수 있다는 것은 별개이므로, 실제 도입 전에 사용하는 컴파일러 버전으로 작은 프로젝트를 먼저 빌드해 보는 편이 안전합니다.


CMAKE_BUILD_TYPE과 타겟별 컴파일 옵션

빌드 타입

# 기본값 설정
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()
# 빌드 타입별 플래그
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -DDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g")
set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG")

멀티 설정 생성기(Visual Studio, Xcode)는 CMAKE_BUILD_TYPE 대신 --config로 선택하므로, 이 패턴과 혼동하지 마세요.

# Debug 빌드
cmake -DCMAKE_BUILD_TYPE=Debug ..
# Release 빌드
cmake -DCMAKE_BUILD_TYPE=Release ..
# 디버그 정보 포함 최적화
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..

타겟별 컴파일 옵션

add_executable(myapp main.cpp)
# 경고 활성화
target_compile_options(myapp PRIVATE
    -Wall -Wextra -Wpedantic
    $<$<CONFIG:Debug>:-Werror>  # Debug에서만 경고를 에러로
)
# 전처리 정의
target_compile_definitions(myapp PRIVATE
    APP_VERSION="1.0"
    $<$<CONFIG:Debug>:DEBUG_MODE>
)
# 헤더 경로
target_include_directories(myapp PRIVATE
    ${CMAKE_SOURCE_DIR}/include
    ${CMAKE_BINARY_DIR}/generated
)

Generator Expression

# 빌드 타입별 분기
target_compile_options(myapp PRIVATE
    "$<$<CONFIG:Debug>:-O0;-g>"
    $<$<CONFIG:Release>:-O3>
)
# 컴파일러별 분기
target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:-fno-exceptions>
    $<$<CXX_COMPILER_ID:MSVC>:/EHsc>
)

제너레이터 표현식은 문자열이라 디버깅이 어렵으며, 복잡해지면 target_compile_options를 여러 줄로 나누거나 커스텀 속성으로 감싸 가독성을 지키세요.


src·tests 서브디렉토리로 프로젝트 나누기

권장 디렉토리 구조

project/
├── CMakeLists.txt          # 루트 CMake 설정
├── src/
│   ├── CMakeLists.txt      # 소스 빌드 설정
│   ├── main.cpp
│   └── lib/
│       ├── CMakeLists.txt
│       ├── lib.cpp
│       └── lib.h
├── include/                # 공개 헤더
│   └── mylib/
│       └── api.h
├── tests/
│   ├── CMakeLists.txt
│   └── test_main.cpp
├── external/               # 서브모듈, 외부 라이브러리
└── build/                  # 빌드 출력 (git ignore)

루트 CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 옵션
option(BUILD_TESTS "Build tests" ON)
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
# 서브디렉토리
add_subdirectory(src)
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

src/CMakeLists.txt

# 라이브러리
add_library(mylib
    lib/lib.cpp
    lib/lib.h
)
target_include_directories(mylib
    PUBLIC ${CMAKE_SOURCE_DIR}/include
    PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
)
# 실행 파일
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)

tests/CMakeLists.txt

find_package(GTest REQUIRED)
add_executable(tests
    test_main.cpp
    test_lib.cpp
)
target_link_libraries(tests PRIVATE
    mylib
    GTest::gtest
    GTest::gtest_main
)
add_test(NAME MyTests COMMAND tests)

경로·캐시·링크 순서·프리셋에서 막히는 경우

절대 경로를 하드코딩함

원인: 절대 경로를 하드코딩하면 다른 환경에서 빌드가 깨집니다.

# ❌ 잘못된 사용
target_include_directories(myapp PRIVATE /usr/local/include)
# ✅ 올바른 사용: CMake 변수 사용
target_include_directories(myapp PRIVATE ${CMAKE_SOURCE_DIR}/include)

설치된 패키지 경로가 아니라 소스 트리 변수를 쓰는지 항상 확인하세요.

소스 디렉토리에서 cmake . 실행

원인: 소스 디렉토리에서 cmake .를 실행하면 빌드 파일이 소스와 섞입니다.

# ❌ 잘못된 사용
cd /project
cmake .
# ✅ 올바른 사용: out-of-source build
mkdir build
cd build
cmake ..

설정을 바꿔도 CMakeCache.txt 때문에 반영되지 않음

증상: CMake 설정을 바꿔도 반영되지 않음. 원인: CMakeCache.txt에 이전 설정이 캐시됩니다.

# 해결법 1: 캐시 삭제
rm CMakeCache.txt
cmake ..
# 해결법 2: 빌드 디렉토리 재생성
cd ..
rm -rf build
mkdir build && cd build
cmake ..

삭제 전에 중요한 -D 플래그를 메모하거나 프리셋으로 옮기세요.

링크 순서로 인한 undefined reference

GNU ld 계열 링커는 정적 라이브러리를 왼쪽에서 오른쪽으로 한 번만 훑기 때문에, 다른 라이브러리를 쓰는 쪽이 먼저 와야 합니다. 다만 CMake 타겟끼리라면 순서를 손으로 맞추기보다 의존 관계 자체를 선언하는 것이 정답입니다.

# ❌ lib1이 lib2에 의존하는데 의존 관계를 선언하지 않고 순서에만 기댐
target_link_libraries(myapp PRIVATE lib2 lib1)
# ✅ 의존 관계를 선언하면 CMake가 링크 줄에 lib1 다음 lib2를 넣어 줌
target_link_libraries(lib1 PUBLIC lib2)   # 공개 헤더가 lib2를 쓰지 않으면 PRIVATE
target_link_libraries(myapp PRIVATE lib1)

순서 문제는 대부분 -lfoo 같은 타겟이 아닌 이름을 직접 나열할 때 생깁니다.

PUBLIC/PRIVATE 가시성 혼동

증상: 헤더를 찾지 못하거나, 불필요한 헤더가 노출됩니다.

# ❌ 잘못된 사용: 내부 헤더를 PUBLIC으로
target_include_directories(mylib PUBLIC src/internal)
# ✅ 올바른 사용
target_include_directories(mylib
    PUBLIC include          # 외부에 노출
    PRIVATE src/internal    # 내부 전용
)

Could not find package SomeLib

증상: Could not find package SomeLib. 원인: 라이브러리가 설치되지 않았거나, CMake가 찾을 수 없는 위치에 있음.

# 해결법 1: 패키지 설치
sudo apt install libboost-dev  # Linux
brew install boost             # macOS
# 해결법 2: CMAKE_PREFIX_PATH 지정
cmake -DCMAKE_PREFIX_PATH=/custom/install/path ..
# 해결법 3: 수동으로 경로 지정
cmake -DBoost_ROOT=/usr/local/boost ..

cmake —preset 실행 시 No such preset

증상: cmake --preset=debug 실행 시 “No such preset” 에러. 원인: 프리셋 이름이 CMakePresets.json에 없거나 hidden: true로 숨겨져 있는 경우, JSON 문법 오류, 혹은 파일의 version이 설치된 CMake가 지원하는 것보다 높은 경우입니다(예: version: 6은 CMake 3.25 이상 필요). 3.19 미만이면 --preset 옵션 자체를 인식하지 못합니다.

# 해결법 1: CMake 버전 확인
cmake --version  # 프리셋 파일 version 6이면 3.25 이상 필요
# 해결법 2: JSON 문법 검증
# VSCode나 온라인 JSON validator 사용
# 해결법 3: 프리셋 목록 확인
cmake --list-presets

병렬 빌드·Ninja·ccache·PCH·Unity 빌드로 빌드 시간 줄이기

병렬 빌드

# 모든 CPU 코어 사용
cmake --build . -j$(nproc)
# 또는 make
make -j$(nproc)

Ninja 사용

Ninja는 Make보다 빠른 빌드 시스템입니다.

# Ninja로 빌드 시스템 생성
cmake -G Ninja ..
# 빌드
ninja

ccache로 컴파일 캐싱

find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
endif()

프리컴파일 헤더 (PCH)

target_precompile_headers(myapp PRIVATE
    <iostream>
    <vector>
    <string>
)

Unity 빌드 (3.16+)

여러 소스 파일을 하나로 합쳐 컴파일 시간 단축.

set(CMAKE_UNITY_BUILD ON)
set(CMAKE_UNITY_BUILD_BATCH_SIZE 16)
add_executable(myapp ${SOURCES})

타겟 명령·제너레이터 표현식·설치 규칙 작성 원칙

전역 명령 대신 타겟 명령

# ❌ 구식 방법 (전역 변수)
include_directories(include)
link_libraries(mylib)
add_definitions(-DDEBUG)
# ✅ 현대적 방법 (타겟 기반)
target_include_directories(myapp PRIVATE include)
target_link_libraries(myapp PRIVATE mylib)
target_compile_definitions(myapp PRIVATE DEBUG)

제너레이터 표현식으로 조건별 플래그 주기

# 빌드 타입별 플래그
target_compile_options(myapp PRIVATE
    "$<$<CONFIG:Debug>:-O0;-g>"
    "$<$<CONFIG:Release>:-O3;-DNDEBUG>"
)
# 컴파일러별 플래그
target_compile_options(myapp PRIVATE
    "$<$<CXX_COMPILER_ID:GNU>:-Wall;-Wextra>"
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
    $<$<CXX_COMPILER_ID:Clang>:-Weverything>
)
# 플랫폼별 링크
target_link_libraries(myapp PRIVATE
    $<$<PLATFORM_ID:Linux>:pthread>
    $<$<PLATFORM_ID:Windows>:ws2_32>
)

인터페이스 라이브러리로 공통 설정 묶기

# 공통 설정을 인터페이스 라이브러리로
add_library(common_settings INTERFACE)
target_compile_features(common_settings INTERFACE cxx_std_20)
target_compile_options(common_settings INTERFACE
    "$<$<CXX_COMPILER_ID:GNU>:-Wall;-Wextra;-Wpedantic>"
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)
# 모든 타겟에 적용
target_link_libraries(myapp PRIVATE common_settings)
target_link_libraries(mylib PRIVATE common_settings)

configure_file로 버전 헤더 생성

# version.h.in
#pragma once
#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define PROJECT_VERSION "@PROJECT_VERSION@"
# CMakeLists.txt
project(MyProject VERSION 1.2.3)
configure_file(
    ${CMAKE_SOURCE_DIR}/version.h.in
    ${CMAKE_BINARY_DIR}/generated/version.h
    @ONLY
)
target_include_directories(myapp PRIVATE ${CMAKE_BINARY_DIR}/generated)

install 규칙과 Config 파일 내보내기

# 실행 파일 설치
install(TARGETS myapp
    RUNTIME DESTINATION bin
)
# 라이브러리 설치
install(TARGETS mylib
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
)
# 헤더 설치 (FILE_SET 사용, 3.23+)
install(TARGETS mylib
    FILE_SET HEADERS DESTINATION include
)
# Config 파일 생성 (다른 프로젝트에서 find_package 가능)
install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE MyLib::
    DESTINATION lib/cmake/MyLib
)

옵션·플랫폼 분기·CI 경고·버전 고정 운영 패턴

option()으로 기능 켜고 끄기

option(ENABLE_LOGGING "Enable logging" ON)
option(ENABLE_PROFILING "Enable profiling" OFF)
option(BUILD_EXAMPLES "Build examples" OFF)
if(ENABLE_LOGGING)
    target_compile_definitions(myapp PRIVATE ENABLE_LOGGING)
    target_sources(myapp PRIVATE src/logger.cpp)
endif()
if(ENABLE_PROFILING)
    target_link_libraries(myapp PRIVATE profiler)
endif()
if(BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

플랫폼별 소스 목록 관리

# 공통 소스
set(COMMON_SOURCES
    src/main.cpp
    src/core.cpp
)
# 플랫폼별 소스
if(WIN32)
    list(APPEND PLATFORM_SOURCES src/platform/windows.cpp)
elseif(APPLE)
    list(APPEND PLATFORM_SOURCES src/platform/macos.cpp)
elseif(UNIX)
    list(APPEND PLATFORM_SOURCES src/platform/linux.cpp)
endif()
add_executable(myapp ${COMMON_SOURCES} ${PLATFORM_SOURCES})

CI에서만 경고를 에러로

function(enable_warnings_as_errors target)
    if(MSVC)
        target_compile_options(${target} PRIVATE /W4 /WX)
    else()
        target_compile_options(${target} PRIVATE
            -Wall -Wextra -Wpedantic -Werror
        )
    endif()
endfunction()
# CI 환경에서만 활성화
if(DEFINED ENV{CI})
    enable_warnings_as_errors(myapp)
endif()

Release 빌드 후 strip

# Release 빌드 시 스트립
if(CMAKE_BUILD_TYPE STREQUAL "Release" AND UNIX AND NOT APPLE)  # macOS strip은 --strip-all 미지원
    add_custom_command(TARGET myapp POST_BUILD
        COMMAND ${CMAKE_STRIP} --strip-all $<TARGET_FILE:myapp>
        COMMENT "Stripping binary"
    )
endif()
# 빌드 정보 출력
add_custom_command(TARGET myapp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo "Build complete: $<TARGET_FILE:myapp>"
    COMMAND ${CMAKE_COMMAND} -E echo "Build type: ${CMAKE_BUILD_TYPE}"
)

FetchContent 의존성 버전 고정

include(FetchContent)
# 버전 변수로 관리
set(GOOGLETEST_VERSION "1.14.0")
set(FMT_VERSION "10.0.0")
set(SPDLOG_VERSION "1.12.0")
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v${GOOGLETEST_VERSION}
    GIT_SHALLOW ON  # 최신 커밋만 다운로드
)
FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG ${FMT_VERSION}
    GIT_SHALLOW ON
)
FetchContent_MakeAvailable(googletest fmt)

DEVELOPER_MODE로 sanitizer·compile_commands 켜기

option(DEVELOPER_MODE "Enable developer mode" OFF)
if(DEVELOPER_MODE)
    # 모든 경고 활성화
    add_compile_options(-Wall -Wextra -Wpedantic)
    
    # Sanitizer 활성화
    add_compile_options(-fsanitize=address,undefined)
    add_link_options(-fsanitize=address,undefined)
    
    # 컴파일 명령 내보내기 (clangd, ccls 등에서 사용)
    set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
    
    # 테스트 빌드
    set(BUILD_TESTS ON)
endif()

일상적으로 쓰는 CMake 명령

  1. 빌드 디렉토리 여러 개 유지
   # 디버그, 릴리스, 프로파일링 동시 유지
   cmake --preset=debug
   cmake --preset=release
   cmake --preset=profile
   
   # 필요할 때 각각 빌드
   cmake --build build/debug
   cmake --build build/release
  1. 컴파일 명령 내보내기 (LSP 서버용)
   # compile_commands.json 생성 (clangd, ccls 등에서 사용)
   set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
   
   # 루트에 심볼릭 링크 생성
   # ln -s build/compile_commands.json .
  1. 빌드 시간 측정
   # 빌드 시간 측정
   time cmake --build build
   
   # 실제로 실행되는 컴파일/링크 명령 확인 (시간 측정은 아님)
   cmake --build build --verbose
  1. 타겟별 선택 빌드
   # 특정 타겟만 빌드 (전체 빌드 불필요)
   cmake --build build --target mylib
   cmake --build build --target tests
   
   # 여러 타겟 동시 빌드
   cmake --build build --target mylib --target myapp
  1. 캐시 변수 확인
   # 현재 캐시 변수 확인
   cmake -L build
   
   # 고급 변수까지 확인
   cmake -LA build
   
   # 특정 변수 확인
   cmake -L build | grep CMAKE_CXX_COMPILER

CMake 명령어 치트시트

# 🏗️ 프로젝트 설정
cmake -S . -B build              # 소스: 현재, 빌드: build/
cmake --preset=debug             # 프리셋 사용 (3.19+)
cmake -DCMAKE_BUILD_TYPE=Release # 빌드 타입 지정
# 🔨 빌드
cmake --build build              # 빌드 실행
cmake --build build -j8          # 병렬 빌드 (8 코어)
cmake --build build --target myapp  # 특정 타겟만
# 🧪 테스트
ctest --test-dir build           # 테스트 실행
ctest --preset=default           # 프리셋으로 테스트
# 📦 설치
cmake --install build            # 설치 실행
cmake --install build --prefix /usr/local  # 경로 지정
# 🧹 정리
cmake --build build --target clean  # 빌드 결과물 삭제
rm -rf build                     # 완전 초기화

기존 Makefile 프로젝트를 CMake로 옮기기

처음부터 완벽한 구조를 만들기보다 빌드가 되는 상태를 유지하며 단계적으로 옮기는 편이 안전합니다. file(GLOB_RECURSE)는 새 파일을 추가해도 CMake를 다시 구성하기 전까지 목록에 반영되지 않으므로 첫 단계에서만 임시로 쓰고, 이후에는 소스를 명시적으로 나열합니다.

# 1단계: 최소 CMakeLists.txt 작성
cmake_minimum_required(VERSION 3.20)
project(MyProject)
file(GLOB_RECURSE SOURCES "src/*.cpp")  # 임시로 GLOB 사용
add_executable(myapp ${SOURCES})
# 2단계: 소스 파일 명시적으로 나열
set(SOURCES
    src/main.cpp
    src/module1.cpp
    # ...
)
# 3단계: 타겟별로 분리
add_library(mylib src/lib.cpp)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)
# 4단계: 서브디렉토리 구조화
add_subdirectory(src)
add_subdirectory(tests)

Git 커밋 해시를 버전 헤더에 넣기

execute_process는 구성 단계에서 한 번만 실행되므로, 아래 방식으로 넣은 해시는 CMake를 다시 구성할 때만 갱신됩니다. 커밋 후 빌드만 다시 하면 이전 해시가 남으므로, 빌드마다 갱신이 필요하면 add_custom_command로 빌드 시점에 헤더를 생성해야 합니다.

# Git 커밋 해시 가져오기
execute_process(
    COMMAND git rev-parse --short HEAD
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    OUTPUT_VARIABLE GIT_COMMIT_HASH
    OUTPUT_STRIP_TRAILING_WHITESPACE
)
# 버전 헤더 생성
configure_file(
    ${CMAKE_SOURCE_DIR}/version.h.in
    ${CMAKE_BINARY_DIR}/version.h
)
# version.h.in
#define VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define VERSION_MINOR @PROJECT_VERSION_MINOR@
#define GIT_COMMIT "@GIT_COMMIT_HASH@"

참고 자료


같이 보면 좋은 글

  • CMake find_package로 외부 라이브러리 연결하기
  • Conan 기초: conanfile로 의존성 선언, 프로필, Remote, CMake 연동
  • vcpkg 기초: 설치, Manifest 모드, Triplet, 버전 고정, 오버레이 커스텀 포트
  • C++ 빌드 시스템 완전 비교 | CMake·Meson·Bazel·Makefile·패키지 매니저 선택 가이드
  • C++ 프로젝트용 Makefile 작성: 변수, 패턴 규칙, 의존성 자동 생성, 병렬 빌드
  • C++ 성능 측정: chrono와 Google Benchmark로 신뢰할 수 있는 벤치마크 만들기