CMake 입문 | 수십 개 파일 컴파일할 때 필요한 빌드 자동화 (CMakeLists.txt 기초)
이 글의 핵심
프로젝트가 커질 때 필요한 빌드 자동화. CMakeLists.txt, add_executable·target_link_libraries·find_package로 크로스 플랫폼 빌드 구성, VS Code CMake Tools 설정까지.
[C++ 실전 가이드 #4] CMake 입문
이 글을 읽으면 CMakeLists.txt 기본 작성, add_executable·target_link_libraries·find_package로 크로스 플랫폼 빌드를 구성할 수 있습니다.
프로젝트가 커지면 수십 개의 파일을 매번 컴파일하는 것이 번거로워집니다. 이때 필요한 것이 빌드 자동화 도구입니다. 비유하면 CMake는 “레시피(CMakeLists.txt)를 주면, 주방 환경에 맞는 조리 순서서(Makefile이나 Visual Studio 프로젝트)를 만들어 주는 조리 도우미”와 같습니다. CMake는 C++ 프로젝트에서 가장 많이 사용되는 빌드 시스템(소스 코드를 실행 파일·라이브러리로 만들어 주는 도구) 생성 도구로, 한 번 설정하면 크로스 플랫폼(Windows, Linux, macOS 등 서로 다른 OS에서 같은 설정으로 빌드할 수 있는 상태)에서 자동으로 빌드할 수 있습니다. CMake는 “빌드 스크립트를 만드는 메타 도구”입니다. 직접 Makefile이나 Visual Studio 솔루션을 쓰기보다 CMakeLists.txt만 작성하면, 각 OS·IDE에 맞는 네이티브 빌드 파일을 생성해 주므로 유지보수 부담이 줄어듭니다.
다른 언어와 나란히 보면, Rust의 Cargo나 Node.js의 npm처럼 빌드와 패키지가 한 도구인 구조는 아니지만, Go 모듈의 재현 가능한 의존성 고정이나 Python 패키지 매니저의 락 파일과 문제를 푸는 방식을 비교할 수 있습니다. 라이브러리 설치는 Conan·vcpkg, Makefile과의 관계는 Makefile 가이드·CMake 완벽 가이드·빌드 시스템 비교로 이어집니다.
CMake 빌드 흐름을 한눈에 보면 아래와 같습니다.
flowchart LR A[CMakeLists.txt] --> B[cmake configure] B --> C[Makefile / .sln 등] C --> D[빌드] D --> E[실행파일·라이브러리]
이전 글: C++ 실전 가이드 #3: VS Code C++ 개발 환경 설정에서 IntelliSense, 빌드 작업, 디버깅 설정 방법을 다뤘습니다.
요구 환경: CMake 3.10+, g++/Clang 또는 MSVC. Linux/macOS에서는 cmake, build-essential(또는 Xcode CLI) 설치 후 사용. Windows에서는 Visual Studio 또는 VS Build Tools + CMake 설치 권장.
목차
- 수동 빌드 문제 시나리오
- 왜 CMake가 필요한가?
- CMake 설치 및 기본 개념
- 첫 CMakeLists.txt
- 멀티파일 프로젝트
- 외부 라이브러리 연동
- VS Code 통합
- 자주 발생하는 에러와 해결법
- 빌드 최적화 및 성능 팁
- 프로덕션 패턴
1. 수동 빌드 문제 시나리오
실제 상황: 3인 팀의 빌드 지옥
상황: C++로 데이터 처리 도구를 개발하는 3인 팀이 있습니다. 초기에는 main.cpp 하나로 시작했지만, 기능이 늘어나며 utils.cpp, parser.cpp, database.cpp, network.cpp 등 10개 이상의 소스 파일이 생겼습니다.
문제 1 - 매번 긴 명령어 입력: 새 파일 logger.cpp를 추가할 때마다 빌드 명령어를 수동으로 수정해야 합니다. 한 글자라도 틀리면 링크 에러가 납니다.
문제 2 - OS마다 다른 스크립트: 팀원 B는 Windows, 팀원 C는 macOS. build.bat과 build.sh를 따로 유지하다 보니 한쪽만 업데이트하고 놓치는 일이 자주 발생합니다.
문제 3 - 전체 재빌드: utils.cpp 한 줄만 수정해도 모든 10개 파일이 다시 컴파일됩니다. 프로젝트가 커질수록 빌드 시간이 2분, 5분, 10분으로 늘어납니다.
문제 4 - 헤더 의존성: parser.h를 수정했을 때 어떤 cpp를 다시 컴파일해야 하는지 수동 빌드 스크립트는 모릅니다. 개발자가 직접 기억해야 합니다.
수동 빌드 명령어 예시 (파일이 늘어날수록 더 길어짐):
g++ -std=c++17 -I./include src/main.cpp src/utils.cpp src/parser.cpp \
src/database.cpp src/network.cpp -L./lib -lsqlite3 -lpthread -o myapp
수동 빌드 vs CMake 비교 다이어그램
flowchart TB
subgraph 수동["수동 빌드 (g++ 직접 호출)"]
M1[파일 추가] --> M2[명령어 수동 수정]
M2 --> M3[전체 재컴파일]
M3 --> M4[OS별 스크립트 별도 유지]
end
subgraph cmake["CMake 사용"]
C1[CMakeLists.txt에 파일 추가] --> C2[cmake --build]
C2 --> C3[증분 빌드: 변경된 파일만]
C3 --> C4[단일 설정으로 모든 OS 지원]
end
style 수동 fill:#ffcccc
style cmake fill:#ccffcc
CMake 도입 후: add_executable(myapp src/main.cpp src/utils.cpp ...) 한 줄에 파일만 나열하면, CMake가 의존성을 추적하고 변경된 파일만 컴파일합니다. 팀원 A, B, C 모두 같은 CMakeLists.txt를 사용합니다.
2. 왜 CMake가 필요한가?
간단한 C++ 프로그램은 g++ hello.cpp -o hello 한 줄로 컴파일할 수 있습니다. 하지만 프로젝트가 커지면 이런 방식으로는 관리가 불가능해집니다.
수동 빌드의 한계
실제 프로젝트에서는 다음과 같이 복잡한 명령어가 필요합니다. -std=c++20은 C++20 표준으로 컴파일하고, -I./include는 헤더 검색 경로를 추가합니다. -L./lib는 라이브러리 검색 경로, -lsqlite3 -lpthread는 해당 라이브러리를 링크하라는 뜻입니다. 파일이 하나만 늘어나도 이 한 줄을 수정해야 하고, Windows에서는 cl과 옵션 체계가 완전히 달라서 같은 프로젝트를 두 벌로 유지하기가 부담됩니다. 제가 처음에는 이런 식으로 스크립트를 짜다가, 파일이 10개 넘어가면서 CMake로 넘어갔습니다.
# 여러 파일, 헤더 경로, 라이브러리를 모두 지정
g++ -std=c++20 -I./include \
src/main.cpp src/utils.cpp src/database.cpp src/network.cpp \
-L./lib -lsqlite3 -lpthread -o myapp
이런 방식의 문제점:
파일 추가 시마다 명령어 수정 필요: 새 소스 파일을 추가할 때마다 빌드 명령어를 수정해야 합니다. 실수로 파일을 빠뜨리면 링크 에러가 발생합니다.
운영체제마다 다른 명령어:
Windows에서는 cl 명령어를, Linux에서는 g++를 사용해야 합니다. 크로스 플랫폼 개발 시 두 개의 빌드 스크립트를 유지해야 합니다.
증분 빌드 불가능: 증분 빌드(incremental build—수정된 파일만 다시 컴파일하고 나머지는 재사용하는 방식)가 되지 않아, 모든 파일을 매번 컴파일해야 하므로, 큰 프로젝트에서는 빌드 시간이 매우 오래 걸립니다.
의존성 관리 어려움: 헤더 파일이 변경되면 어떤 소스 파일을 다시 컴파일해야 하는지 수동으로 추적해야 합니다.
빌드 디렉토리를 소스와 분리하는 이유: CMake는 보통 프로젝트 루트가 아닌 build/ 같은 별도 디렉토리에서 실행합니다. 이렇게 하면 생성된 Makefile·오브젝트 파일·실행 파일이 소스와 섞이지 않고 한곳에 모여, git clean으로 지울 때도 build/만 지우면 됩니다. Debug 빌드와 Release 빌드를 서로 다른 디렉토리(build-debug, build-release)에서 돌리면 옵션이 섞이지 않고 관리하기 쉽습니다.
실무에서는 파일이 2~3개만 있어도 위 명령어를 매번 치기 번거롭고, 팀원마다 OS가 다르면 스크립트가 갈라집니다. CMake로 넘어가면 “무엇을 빌드할지”만 선언하고, “어떤 명령으로 빌드할지”는 CMake가 플랫폼에 맞게 생성해 줍니다.
CMake가 해결하는 방법
CMake는 빌드 시스템 생성기입니다. 우리가 작성한 CMakeLists.txt 파일을 읽고, 각 플랫폼에 맞는 빌드 파일을 자동으로 생성합니다.
동작 과정:
CMakeLists.txt → CMake 실행 → Makefile(Linux) 또는 Visual Studio 프로젝트(Windows) 생성 → 빌드
CMake의 장점:
✅ 크로스 플랫폼: CMakeLists.txt 하나로 Windows, macOS, Linux 모두 지원합니다.
✅ 자동 의존성 추적: 헤더 파일이 변경되면 관련된 소스 파일만 자동으로 재컴파일합니다.
✅ 외부 라이브러리 자동 탐지: find_package() 명령으로 설치된 라이브러리를 자동으로 찾아서 연결합니다.
✅ IDE 통합: VS Code, Visual Studio, CLion 등 주요 IDE에서 모두 지원합니다.
CMake 빌드 단계 시퀀스
CMake가 실제로 빌드를 수행하는 과정을 시퀀스 다이어그램으로 보면 다음과 같습니다.
sequenceDiagram
participant Dev as 개발자
participant CMake as CMake
participant Gen as Generator
participant Build as 빌드 도구
Dev->>CMake: cmake ..
CMake->>CMake: CMakeLists.txt 파싱
CMake->>CMake: 플랫폼/컴파일러 탐지
CMake->>Gen: Makefile/Ninja/VS 프로젝트 생성
Gen->>Dev: 빌드 파일 생성 완료
Dev->>Build: cmake --build .
Build->>Build: 소스 컴파일
Build->>Build: 링크
Build->>Dev: 실행 파일/라이브러리 출력
3. CMake 설치 및 기본 개념
설치
# Windows (Chocolatey)
choco install cmake
# macOS
brew install cmake
# Linux (Ubuntu/Debian)
sudo apt install cmake
# Linux (최신 버전 필요 시)
sudo snap install cmake --classic
# 확인
cmake --version
설치 확인 예시:
cmake version 3.28.1
CMake suite maintained and supported by Kitware (kitware.com/cmake).
핵심 개념
CMakeLists.txt: 빌드 방법 정의 파일. 쉽게 말해 “무엇을 빌드할지(실행 파일·라이브러리), 어떤 소스·헤더를 쓰는지”를 적어 두는 설정서입니다.
빌드 디렉토리: 소스와 빌드 결과 분리 (build/). 생성된 Makefile·오브젝트 파일이 소스와 섞이지 않게 합니다.
Generator: 빌드 시스템 종류 (Makefile, Ninja 등). CMake가 “어떤 형식의 빌드 파일”을 만들지 결정합니다.
Target: 빌드 결과물 (실행 파일, 라이브러리). 하나의 실행 파일이나 라이브러리 단위를 타겟이라고 부릅니다.
Generator 비교 (Make vs Ninja)
CMake는 여러 Generator를 지원합니다. 실무에서 자주 쓰는 두 가지를 비교하면 다음과 같습니다.
| 항목 | Unix Makefiles | Ninja |
|---|---|---|
| 빌드 속도 | 보통 | 빠름 (병렬 최적화) |
| 설정 파일 크기 | 큼 | 작음 |
| 재구성 속도 | 느림 | 빠름 |
| 권장 환경 | 기본 환경 | CI/CD, 대규모 프로젝트 |
Ninja를 사용하려면:
cmake -G Ninja ..
cmake --build .
4. 첫 CMakeLists.txt
Hello World
가장 단순한 구성은 실행 파일 하나만 만드는 것입니다. main.cpp는 그냥 Hello를 출력하고, CMakeLists.txt는 “C++20으로 hello라는 실행 파일을 main.cpp에서 만들어라”라고 선언합니다. cmake_minimum_required는 이 스크립트가 동작하는 CMake 최소 버전을 정하고, project()는 프로젝트 이름과 버전을 설정합니다. set(CMAKE_CXX_STANDARD 20)으로 C++ 표준을 20으로 고정하면, add_executable(hello main.cpp) 한 줄로 “hello” 실행 파일의 소스가 main.cpp임을 알려 줍니다. 처음 CMake를 쓸 때는 이 네 줄만 이해해도 단일 파일 프로젝트는 다 다룰 수 있습니다.
main.cpp (복사해 붙여넣은 뒤 g++ -std=c++20 main.cpp -o hello && ./hello 또는 아래 CMake로 빌드 가능):
// 복사해 붙여넣은 뒤: g++ -std=c++20 main.cpp -o hello && ./hello
#include <iostream>
int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}
실행 결과: 터미널에 Hello, CMake! 가 출력됩니다.
CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
project(HelloCMake VERSION 1.0)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(hello main.cpp)
코드 상세 설명:
-
cmake_minimum_required(VERSION 3.15): 이 CMakeLists.txt를 실행하는 데 필요한 CMake 최소 버전을 지정합니다.- 3.15 미만 버전에서 실행하면 에러가 발생합니다.
- 너무 낮은 버전을 지정하면 최신 기능을 못 쓰고, 너무 높으면 구형 시스템에서 실행 불가합니다.
- 3.15는 2019년 버전으로 대부분의 환경에서 사용 가능합니다.
-
project(HelloCMake VERSION 1.0): 프로젝트 이름과 버전을 설정합니다.- 프로젝트 이름:
HelloCMake(공백 없이) - 버전:
1.0(선택사항) - 이 명령은
PROJECT_NAME,PROJECT_VERSION같은 변수를 자동으로 설정합니다.
- 프로젝트 이름:
-
set(CMAKE_CXX_STANDARD 20): C++ 표준 버전을 20(C++20)으로 설정합니다.- 이렇게 하면
-std=c++20옵션이 자동으로 추가됩니다. - 11, 14, 17, 20, 23 등을 지정할 수 있습니다.
- 이렇게 하면
-
set(CMAKE_CXX_STANDARD_REQUIRED ON): 지정한 C++ 표준을 필수로 만듭니다.ON: 컴파일러가 C++20을 지원하지 않으면 에러 발생OFF: 지원하지 않으면 낮은 버전으로 폴백 (권장하지 않음)
-
add_executable(hello main.cpp):hello라는 이름의 실행 파일을main.cpp로부터 생성합니다.- 첫 번째 인자: 생성할 실행 파일 이름 (확장자 없이)
- 나머지 인자: 소스 파일 목록
- 여러 파일:
add_executable(hello main.cpp utils.cpp)
빌드
mkdir build && cd build
cmake ..
cmake --build .
./hello
빌드 명령어 상세 설명:
1. mkdir build && cd build:
build디렉토리를 만들고 그 안으로 이동합니다.- 왜 별도 디렉토리?: 생성되는 파일들(Makefile, 오브젝트 파일, 실행 파일)을 소스와 분리하기 위함입니다.
- 소스 디렉토리가 깨끗하게 유지되고,
rm -rf build로 빌드 결과를 쉽게 삭제할 수 있습니다.
2. cmake ..:
- 상위 디렉토리(
..)의CMakeLists.txt를 읽고 빌드 파일을 생성합니다. - 생성되는 것: Makefile (Linux/Mac), Visual Studio 프로젝트 (Windows), 또는 Ninja 파일
- 이 단계에서는 컴파일하지 않고 빌드 시스템만 생성합니다.
- 출력 예시:
-- Configuring done,-- Generating done,-- Build files have been written to: /path/to/build
3. cmake --build .:
- 현재 디렉토리(
.)의 빌드 파일을 사용해 실제로 컴파일합니다. - 내부적으로
make(Linux/Mac) 또는msbuild(Windows)를 호출합니다. - 장점: 플랫폼에 관계없이 같은 명령어를 사용할 수 있습니다.
- 병렬 빌드:
cmake --build . -j8(8개 코어 사용)
4. ./hello:
- 생성된 실행 파일을 실행합니다.
- Windows에서는
./hello.exe또는.\hello.exe
Debug vs Release 빌드
개발 중에는 디버그 심볼이 필요하고, 배포 시에는 최적화된 바이너리가 필요합니다. CMake는 CMAKE_BUILD_TYPE으로 이를 제어합니다.
# Debug 빌드 (기본값, 디버깅용)
cmake -DCMAKE_BUILD_TYPE=Debug ..
# Release 빌드 (최적화, 배포용)
cmake -DCMAKE_BUILD_TYPE=Release ..
빌드 타입별 차이:
| 빌드 타입 | 최적화 | 디버그 심볼 | 용도 |
|---|---|---|---|
| Debug | 없음 (-O0) | 포함 | 개발, 디버깅 |
| Release | 최대 (-O3) | 없음 | 배포, 성능 측정 |
| RelWithDebInfo | 중간 (-O2) | 포함 | 프로파일링 |
주요 명령어
| 명령어 | 설명 |
|---|---|
cmake_minimum_required() | CMake 최소 버전 |
project() | 프로젝트 이름/버전 |
set() | 변수 설정 |
add_executable() | 실행 파일 생성 |
5. 멀티파일 프로젝트
프로젝트 구조
calculator/
├── CMakeLists.txt
├── main.cpp
├── src/
│ ├── operations.cpp
│ └── utils.cpp
└── include/
├── operations.h
└── utils.h
소스 코드 전체 예시
include/operations.h:
#pragma once
// 덧셈, 뺄셈, 곱셈, 나눗셈 함수 선언
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
double divide(int a, int b);
include/utils.h:
#pragma once
// 입력 검증 유틸리티
bool is_valid_number(const char* str);
void print_result(const char* op, double result);
src/operations.cpp:
#include "operations.h"
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
double divide(int a, int b) {
if (b == 0) return 0.0;
return static_cast<double>(a) / b;
}
src/utils.cpp:
#include "utils.h"
#include <iostream>
#include <cstdlib>
bool is_valid_number(const char* str) {
if (!str || !*str) return false;
char* end;
strtol(str, &end, 10);
return *end == '\0';
}
void print_result(const char* op, double result) {
std::cout << op << " 결과: " << result << std::endl;
}
main.cpp:
#include "operations.h"
#include "utils.h"
#include <iostream>
int main(int argc, char* argv[]) {
if (argc != 4) {
std::cerr << "사용법: calculator <a> <op> <b>\n";
return 1;
}
if (!is_valid_number(argv[1]) || !is_valid_number(argv[3])) {
std::cerr << "잘못된 숫자입니다.\n";
return 1;
}
int a = std::atoi(argv[1]);
int b = std::atoi(argv[3]);
std::string op = argv[2];
if (op == "+") print_result("덧셈", add(a, b));
else if (op == "-") print_result("뺄셈", subtract(a, b));
else if (op == "*") print_result("곱셈", multiply(a, b));
else if (op == "/") print_result("나눗셈", divide(a, b));
else { std::cerr << "알 수 없는 연산: " << op << "\n"; return 1; }
return 0;
}
CMakeLists.txt (권장 방법)
여러 cpp를 묶어서 정적 라이브러리로 만들고, main에서는 그 라이브러리만 링크하는 방식이 좋습니다. target_include_directories로 헤더 경로를 주고, target_link_libraries로 실행 파일이 라이브러리를 쓰도록 연결합니다. 이렇게 하면 의존 관계가 명확하고 증분 빌드도 잘 됩니다.
cmake_minimum_required(VERSION 3.15)
project(Calculator VERSION 1.0)
set(CMAKE_CXX_STANDARD 20)
# 라이브러리 생성
add_library(calculator_lib STATIC
src/operations.cpp
src/utils.cpp
)
# 헤더 경로 설정
target_include_directories(calculator_lib PUBLIC include)
# 실행 파일 생성 및 링크
add_executable(calculator main.cpp)
target_link_libraries(calculator PRIVATE calculator_lib)
코드 상세 설명:
1. add_library(calculator_lib STATIC ...):
calculator_lib라는 이름의 정적 라이브러리를 생성합니다.- STATIC: 정적 라이브러리 (
.aon Linux,.libon Windows)- 다른 옵션:
SHARED(동적 라이브러리.so/.dll)
- 다른 옵션:
- 여러 소스 파일을 나열하면 모두 컴파일되어 하나의 라이브러리로 묶입니다.
- 왜 라이브러리로?: 코드를 모듈화하고, 다른 실행 파일에서도 재사용할 수 있습니다.
2. target_include_directories(calculator_lib PUBLIC include):
calculator_lib를 빌드할 때include디렉토리를 헤더 검색 경로에 추가합니다.- PUBLIC: 이 라이브러리를 링크하는 다른 타겟도
include경로를 자동으로 사용합니다.- PRIVATE: 이 라이브러리만 사용 (다른 타겟에 전파 안 됨)
- INTERFACE: 이 라이브러리는 사용 안 하고 링크하는 타겟만 사용
- 결과:
#include "operations.h"가include/operations.h를 찾을 수 있습니다.
3. add_executable(calculator main.cpp):
calculator라는 실행 파일을main.cpp로부터 생성합니다.
4. target_link_libraries(calculator PRIVATE calculator_lib):
calculator실행 파일이calculator_lib라이브러리를 링크합니다.- PRIVATE: 이 링크 관계는
calculator만 사용 (다른 타겟에 전파 안 됨) - 효과:
calculator_lib의 함수를main.cpp에서 사용할 수 있습니다.calculator_lib의PUBLIC헤더 경로도 자동으로 상속됩니다.
전체 흐름:
operations.cpp,utils.cpp→ 컴파일 →calculator_lib.a생성main.cpp→ 컴파일 →main.o생성main.o+calculator_lib.a→ 링크 →calculator실행 파일 생성
멀티파일 빌드 의존성 다이어그램
flowchart TB
subgraph 소스
main[main.cpp]
ops[operations.cpp]
utils[utils.cpp]
end
subgraph 헤더
ops_h[operations.h]
utils_h[utils.h]
end
subgraph 빌드
lib[calculator_lib.a]
exe[calculator]
end
ops --> ops_h
utils --> utils_h
ops --> lib
utils --> lib
main --> ops_h
main --> utils_h
main --> exe
lib --> exe
빌드 및 실행
mkdir build && cd build
cmake ..
cmake --build .
./calculator 10 + 5
실행 예시:
덧셈 결과: 15
완전한 예제: 복사해서 바로 사용 가능한 최소 프로젝트
calculator_complete/{src,include,build} 디렉토리를 만들고 아래 파일들을 생성하면 즉시 빌드할 수 있습니다.
include/calc.h:
#pragma once
int add(int a, int b);
int subtract(int a, int b);
src/calc.cpp:
#include "calc.h"
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
main.cpp (루트):
#include "calc.h"
#include <iostream>
int main() {
std::cout << "3 + 5 = " << add(3, 5) << std::endl;
return 0;
}
CMakeLists.txt (루트):
cmake_minimum_required(VERSION 3.15)
project(CalcComplete VERSION 1.0)
set(CMAKE_CXX_STANDARD 17)
add_library(calc STATIC src/calc.cpp)
target_include_directories(calc PUBLIC include)
add_executable(calc_app main.cpp)
target_link_libraries(calc_app PRIVATE calc)
빌드: cd build && cmake .. && cmake --build . && ./calc_app
빌드 파이프라인 전체 흐름 (Mermaid)
flowchart LR
subgraph 소스
A[main.cpp]
B[calc.cpp]
C[calc.h]
end
subgraph CMake
D[CMakeLists.txt]
end
subgraph Configure
E[cmake ..]
end
subgraph Build
F[calc.o]
G[main.o]
H[calc_app]
end
D --> E
E --> F
E --> G
B --> F
C --> F
C --> G
A --> G
F --> H
G --> H
6. 외부 라이브러리 연동
라이브러리 설치
# Ubuntu
sudo apt install nlohmann-json3-dev
# macOS
brew install nlohmann-json
# Windows (vcpkg)
vcpkg install nlohmann-json
CMakeLists.txt
시스템이나 vcpkg에 설치된 라이브러리는 find_package로 찾습니다. 버전을 REQUIRED로 지정하면 없을 때 설정이 실패하고, 찾은 타겟을 target_link_libraries에 넣으면 include 경로와 링크 설정이 자동으로 붙습니다.
cmake_minimum_required(VERSION 3.15)
project(JsonExample)
set(CMAKE_CXX_STANDARD 20)
# vcpkg 사용 시 (Windows)
# set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
# 라이브러리 찾기
find_package(nlohmann_json 3.11.0 REQUIRED)
add_executable(json_app main.cpp)
target_link_libraries(json_app PRIVATE nlohmann_json::nlohmann_json)
JSON 예시 main.cpp
#include <nlohmann/json.hpp>
#include <iostream>
#include <fstream>
using json = nlohmann::json;
int main() {
// JSON 객체 생성
json j;
j["name"] = "CMake 예제";
j["version"] = 1.0;
j["tags"] = {"C++", "CMake", "JSON"};
// JSON 문자열 출력
std::cout << j.dump(2) << std::endl;
// 파일로 저장
std::ofstream out("output.json");
out << j.dump(2);
out.close();
return 0;
}
find_package 옵션
# 필수 라이브러리
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem)
# 선택적 라이브러리
find_package(OpenCV QUIET)
if(OpenCV_FOUND)
message(STATUS "OpenCV found")
target_link_libraries(myapp PRIVATE opencv_core)
endif()
# 버전 범위 지정
find_package(ZLIB 1.2 REQUIRED)
find_package 동작 원리
find_package는 다음 순서로 라이브러리를 찾습니다:
1. CMAKE_PREFIX_PATH에 지정된 경로
2. 시스템 기본 경로 (/usr, /usr/local 등)
3. vcpkg/Conan 등 패키지 매니저 경로 (CMAKE_TOOLCHAIN_FILE 사용 시)
4. Find<Package>.cmake 또는 <Package>Config.cmake 모듈 실행
7. VS Code 통합
CMake Tools 확장 설치
- VS Code 확장 탭 (Ctrl+Shift+X)
- “CMake Tools” 검색 및 설치
사용법
1. Kit 선택: Ctrl+Shift+P → “CMake: Select a Kit”
2. Configure: Ctrl+Shift+P → “CMake: Configure”
3. 빌드: F7 또는 상태바 “Build” 클릭
4. 실행: Shift+F5 또는 상태바 “Run” 클릭
.vscode/settings.json
{
"cmake.configureOnOpen": true,
"cmake.buildDirectory": "${workspaceFolder}/build",
"cmake.generator": "Ninja",
"cmake.buildBeforeRun": true,
"cmake.parallelJobs": 8
}
.vscode/launch.json (디버깅)
{
"version": "0.2.0",
"configurations": [
{
"name": "CMake 디버그",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/calculator",
"args": ["10", "+", "5"],
"cwd": "${workspaceFolder}",
"preLaunchTask": "CMake: build"
}
]
}
8. 자주 발생하는 에러와 해결법
에러 1: CMake를 찾을 수 없음 (cmake: command not found)
증상:
bash: cmake: command not found
원인: CMake가 설치되지 않았거나 PATH에 등록되지 않음.
해결법:
# Ubuntu/Debian
sudo apt update
sudo apt install cmake build-essential
# Fedora
sudo dnf install cmake gcc-c++
# macOS
brew install cmake
# Windows (PowerShell, Chocolatey)
choco install cmake
# 설치 확인
cmake --version
PATH 문제 시 (설치했는데 인식 안 될 때): export PATH="/opt/homebrew/bin:$PATH" (macOS)를 ~/.zshrc에 추가 후 source ~/.zshrc.
에러 2: CMake 버전이 너무 낮음
# Ubuntu
sudo snap install cmake --classic
# macOS
brew upgrade cmake
# 버전 확인
cmake --version
라이브러리 못 찾음
증상: Could NOT find package nlohmann_json
해결 1 - CMAKE_PREFIX_PATH 설정:
set(CMAKE_PREFIX_PATH "/usr/local" ${CMAKE_PREFIX_PATH})
find_package(nlohmann_json REQUIRED)
해결 2 - vcpkg 사용 시:
set(CMAKE_TOOLCHAIN_FILE "C:/vcpkg/scripts/buildsystems/vcpkg.cmake")
해결 3 - 수동 경로 지정:
set(nlohmann_json_INCLUDE_DIR "/usr/include")
add_executable(json_app main.cpp)
target_include_directories(json_app PRIVATE ${nlohmann_json_INCLUDE_DIR})
에러 5: 헤더 파일 못 찾음
증상:
fatal error: operations.h: No such file or directory
fatal error: nlohmann/json.hpp: No such file or directory
원인: 헤더 검색 경로가 설정되지 않음.
해결:
# 자체 헤더 (include/ 디렉토리)
target_include_directories(myapp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# find_package 사용 시 - target_link_libraries에 패키지를 넣으면 대부분 자동 해결
target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json)
에러 3: undefined reference (링킹 에러)
증상:
undefined reference to `add(int, int)'
undefined reference to `nlohmann::basic_json<>::dump()'
collect2: error: ld returned 1 exit status
원인: 함수나 클래스를 선언만 하고 정의가 있는 오브젝트/라이브러리를 링크하지 않음. find_package 또는 add_library 후 target_link_libraries를 호출하지 않은 경우가 대부분입니다.
에러 해결 플로우:
flowchart TD
A[undefined reference 에러] --> B{어떤 심볼?}
B --> C[자체 함수/클래스]
B --> D[외부 라이브러리]
C --> E[add_library로 라이브러리 생성했는가?]
E --> F[target_link_libraries 추가]
D --> G[find_package 호출했는가?]
G --> H["target_link_libraries에 패키지 ..."]
F --> I[해결]
H --> I
해결:
# find_package 사용 시 - 반드시 target_link_libraries 호출
find_package(nlohmann_json REQUIRED)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json)
# 자체 라이브러리 사용 시
add_library(calculator_lib STATIC src/operations.cpp src/utils.cpp)
add_executable(calculator main.cpp)
target_link_libraries(calculator PRIVATE calculator_lib)
자주 하는 실수: target_include_directories만 추가하고 target_link_libraries를 빠뜨림. include는 헤더 검색 경로만 설정하고, 실제 오브젝트 코드를 링크하려면 target_link_libraries가 필수입니다.
에러 4: 캐시 문제
rm -rf build
mkdir build && cd build
cmake ..
설명: CMake는 이전 설정을 build/ 안에 캐시합니다. 컴파일러를 바꿨거나 옵션을 잘못 줬을 때는 build 폴더를 지우고 다시 cmake ..부터 실행하면 깨끗한 상태로 설정이 적용됩니다.
CMake 자주 하는 실수
| 실수 | 결과 | 권장 |
|---|---|---|
| 소스 디렉터리에서 cmake 실행 | 소스와 빌드 산출물이 섞임 | 반드시 build/ 등 별도 디렉터리에서 cmake .. 실행 (out-of-source build) |
| add_executable에 헤더만 넣기 | 불필요한 재빌드 유발 | 실행 파일 타겟에는 .cpp만, 헤더는 target_include_directories로 경로만 지정 |
| find_package 후 링크 누락 | undefined reference | find_package(... REQUIRED) 후 target_link_libraries(타겟 PRIVATE 패키지::타겟) 필수 |
| C++ 표준 미지정 | 컴파일러 기본값에 의존 | set(CMAKE_CXX_STANDARD 17) 또는 target_compile_features(타겟 PRIVATE cxx_std_17) 사용 |
실무 팁: “CMake 라이브러리 못 찾음”, “CMake undefined reference” 등으로 검색할 때 위 항목을 먼저 점검하면 해결되는 경우가 많습니다.
9. 빌드 최적화 및 성능 팁
병렬 빌드
# CPU 코어 수만큼 병렬 빌드 (Linux)
cmake --build . -j$(nproc)
# macOS
cmake --build . -j$(sysctl -n hw.ncpu)
# 8개 작업 동시 실행 (고정)
cmake --build . -j8
Ninja는 재구성(configure) 시간도 짧고, 병렬 빌드 최적화가 잘 되어 있어 CI/CD와 대규모 프로젝트에 적합합니다. 중규모(100파일) 프로젝트에서 Make 대비 약 40% 빌드 시간 단축이 일반적입니다.
# Ninja 사용 (권장)
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
ccache로 재빌드 속도 향상
동일한 소스를 다시 빌드할 때 ccache가 이전 컴파일 결과를 재사용합니다. 클린 빌드 후 두 번째 빌드에서 효과가 극대화됩니다.
# Ubuntu
sudo apt install ccache
# macOS
brew install ccache
# CMake에서 ccache 사용
cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ..
cmake --build .
효과: 100개 파일 프로젝트에서 클린 빌드 45초 → ccache 히트 시 5초 이하로 단축 가능.
추가 팁: 수백 개 파일 이상의 대규모 프로젝트에서는 target_precompile_headers()로 Precompiled Headers(PCH)를 사용하면 빌드 시간을 더 줄일 수 있습니다. CMake 3.16+에서 지원합니다.
실무 프로젝트 구조 예시
my_project/
├── CMakeLists.txt # 루트 설정
├── src/
│ ├── CMakeLists.txt # 소스 빌드
│ ├── main.cpp
│ └── ...
├── include/
│ └── ...
├── tests/
│ ├── CMakeLists.txt # 테스트 빌드
│ └── test_main.cpp
├── third_party/ # 서드파티 라이브러리
└── build/
CMake 구현 체크리스트
프로젝트에 CMake를 도입할 때 확인할 항목입니다:
-
cmake_minimum_required버전 지정 (3.15 이상 권장) -
project()로 프로젝트 이름 설정 -
set(CMAKE_CXX_STANDARD 17)또는 20 지정 -
build/등 별도 디렉토리에서 out-of-source 빌드 -
add_executable에.cpp파일만, 헤더는target_include_directories로 -
find_package후target_link_libraries필수 호출 -
.gitignore에build/,CMakeCache.txt추가
10. 프로덕션 패턴
모듈화된 CMake 구조
실제 프로덕션 프로젝트에서는 루트 CMakeLists.txt가 서브디렉토리를 add_subdirectory로 포함하는 구조를 사용합니다.
프로젝트 구조:
myapp/
├── CMakeLists.txt # 루트: 프로젝트 설정, 서브디렉토리 포함
├── src/
│ ├── CMakeLists.txt # 메인 앱 빌드
│ ├── main.cpp
│ └── core/
│ ├── CMakeLists.txt # core 라이브러리
│ ├── core.cpp
│ └── core.h
├── libs/
│ └── utils/
│ ├── CMakeLists.txt # utils 라이브러리
│ ├── utils.cpp
│ └── utils.h
├── tests/
│ └── CMakeLists.txt # 테스트 타겟
└── build/
루트 CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 서브디렉토리 추가 (순서 중요: 의존되는 쪽 먼저)
add_subdirectory(libs/utils)
add_subdirectory(src/core)
add_subdirectory(src)
add_subdirectory(tests)
libs/utils/CMakeLists.txt: add_library(utils STATIC utils.cpp) + target_include_directories(utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
src/core/CMakeLists.txt: add_library(core STATIC core.cpp) + target_link_libraries(core PRIVATE utils)
src/CMakeLists.txt: add_executable(myapp main.cpp) + target_link_libraries(myapp PRIVATE core)
프로덕션 아키텍처 다이어그램
flowchart TB
subgraph 루트["루트 CMakeLists.txt"]
R[project, C++ 표준]
end
subgraph 라이브러리["라이브러리 계층"]
U[utils]
C[core]
end
subgraph 앱["애플리케이션"]
A[myapp]
end
subgraph 테스트["테스트"]
T[tests]
end
R --> U
R --> C
R --> A
R --> T
C --> U
A --> C
T --> C
CI/CD 통합 패턴
GitHub Actions, GitLab CI 등에서는 Ninja + ccache 조합을 권장합니다:
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
cmake --build build -j$(nproc)
패키지 배포 (install 규칙)
라이브러리를 배포할 때는 install(TARGETS ...), install(DIRECTORY include/ ...)로 설치 규칙을 정의하고, Config.cmake를 생성하면 다른 프로젝트에서 find_package로 찾을 수 있습니다. 자세한 내용은 CMake 공식 문서를 참고하세요.
프로덕션 체크리스트
-
add_subdirectory로 모듈 분리 -
target_link_libraries에 PRIVATE/PUBLIC/INTERFACE 명시 - CI에서 Ninja + ccache 사용
-
CMAKE_BUILD_TYPE을 Release로 고정 (배포 빌드 시) -
.gitignore에build/,CMakeCache.txt,CMakeFiles/추가 - 라이브러리 배포 시
install()규칙 정의
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)
- C++ 패키지 매니저 | vcpkg·Conan으로 “라이브러리 설치 지옥” 탈출하기
- C++ 개발 환경 구축 | “C++ 어디서 시작하죠?” 컴파일러 설치부터 Hello World까지
이 글에서 다루는 키워드 (관련 검색어)
CMake 입문, CMakeLists.txt 작성, add_executable, find_package, 크로스 플랫폼 빌드, CMake 라이브러리 연동, CMake 외부 라이브러리, VS Code CMake, CMake 설치, C++ 빌드 자동화 등으로 검색하시면 이 글이 도움이 됩니다.
자주 묻는 질문 (FAQ)
CMake가 왜 필요한가요?
파일이 많아지면 g++ file1.cpp file2.cpp ... 명령어를 매번 입력하기 번거롭고, 운영체제마다 명령어가 달라집니다. CMake는 CMakeLists.txt 하나로 모든 플랫폼에서 자동으로 빌드 파일을 생성해 주므로, 크로스 플랫폼 개발과 대규모 프로젝트 관리가 쉬워집니다.
CMake와 Make의 차이는 무엇인가요?
Make는 Makefile을 읽어서 빌드를 수행하는 도구입니다. CMake는 Makefile을 생성하는 도구입니다. CMake는 플랫폼에 맞는 빌드 파일(Linux는 Makefile, Windows는 Visual Studio 프로젝트)을 자동으로 만들어 줍니다.
build 폴더는 왜 따로 만드나요?
소스 코드와 빌드 결과물(오브젝트 파일, 실행 파일)을 분리하기 위해서입니다. 이렇게 하면 build/ 폴더만 지우면 빌드 결과물을 깔끔하게 제거할 수 있고, 소스 코드 디렉토리가 깨끗하게 유지됩니다.
CMake 에러가 나면 어떻게 해결하나요?
가장 흔한 해결법은 1) build/ 폴더를 삭제하고 다시 cmake .. 실행, 2) CMake 버전 확인(cmake --version), 3) 컴파일러 경로 확인입니다. 에러 메시지를 자세히 읽고 어떤 라이브러리나 파일을 찾지 못했는지 확인하세요.
find_package가 라이브러리를 못 찾아요.
라이브러리가 시스템에 설치되어 있는지 확인하세요. Ubuntu는 sudo apt install lib이름-dev, macOS는 brew install 라이브러리, Windows는 vcpkg로 설치할 수 있습니다. 설치 후에도 안 되면 CMAKE_PREFIX_PATH를 설정해야 할 수 있습니다.
Ninja와 Make 중 어떤 것을 써야 하나요?
Ninja가 대체로 더 빠르고, CI/CD나 대규모 프로젝트에 적합합니다. Make는 기본 지원이 넓어 호환성이 좋습니다. VS Code CMake Tools는 Ninja를 기본으로 사용하는 경우가 많습니다.
정적 라이브러리와 동적 라이브러리의 차이는?
정적(STATIC): 컴파일 시 실행 파일에 코드가 포함됩니다. 배포가 단순하지만 실행 파일 크기가 커집니다. 동적(SHARED): 런타임에 .so/.dll을 로드합니다. 여러 프로그램이 공유할 수 있어 메모리 절약이 되지만, 배포 시 라이브러리 파일을 함께 전달해야 합니다.
마무리
핵심 요약
CMake를 사용하면 C++ 프로젝트 관리가 훨씬 쉬워집니다. 이 글에서 배운 내용을 정리하겠습니다:
✅ CMake의 역할: 플랫폼별로 다른 빌드 파일(Makefile, Visual Studio 프로젝트 등)을 자동으로 생성합니다.
✅ CMakeLists.txt: 프로젝트의 빌드 방법을 정의하는 파일입니다. 한 번 작성하면 모든 플랫폼에서 사용할 수 있습니다.
✅ Out-of-source build: 소스 코드와 빌드 결과물을 분리하여 프로젝트를 깔끔하게 유지합니다.
✅ find_package: 외부 라이브러리를 자동으로 찾아서 연결합니다.
✅ VS Code 통합: CMake Tools 확장으로 VS Code에서 편리하게 CMake를 사용할 수 있습니다.
기본 워크플로우
CMake를 사용한 기본적인 빌드 과정입니다. 이 명령어들을 기억해두세요:
mkdir build && cd build # 빌드 디렉토리 생성
cmake .. # 빌드 파일 생성
cmake --build . # 실제 빌드 수행
./myapp # 실행
한 줄 요약: CMakeLists.txt로 add_executable·target_link_libraries만 쓰면 크로스 플랫폼 빌드가 가능합니다. 다음으로 컴파일 과정(#5)을 읽어보면 좋습니다.
이전 글: C++ 실전 가이드 #3: VS Code C++ 개발 환경 설정
다음 글: C++ 실전 가이드 #5: 컴파일 과정 분석 - 소스 코드가 실행 파일이 되는 과정을 단계별로 설명합니다.
참고 자료
관련 글
- C++ GDB 기초 완벽 가이드 | 브레이크포인트·워치포인트
- C++ LLDB 기초 완벽 가이드 | macOS·브레이크포인트
- VS Code C++ 설정 | IntelliSense·빌드·디버깅
- C++ 전처리기 완벽 가이드 | #define·#ifdef
- C++ 컴파일 과정 |