C/C++ 개발자가 IDE의 빌드 버튼 하나로 프로그램을 실행하는 것은 편리하지만, 프로젝트의 규모가 커지고 협업 환경이 다양해지면 곧 한계에 부딪힙니다.
내 컴퓨터(Windows/VS)에서는 잘 돌아가는 코드가 서버(Linux/GCC)에서는 빌드조차 되지 않는 상황, 이 '환경의 파편화'를 해결하는 것이 바로 CMake의 존재 이유입니다.

CMake는 특정 IDE나 OS에 종속되지 않는 독립적인 빌드 환경을 구축하기 위한 '메타 빌드 시스템'입니다.
C/C++ 프로젝트를 빌드할 때는 전통적으로 Makefile을 작성해야 합니다.
하지만 개발자가 Makefile이나 Solution 파일을 직접 짜는 대신, 고수준의 설정 파일(CMakeLists.txt)만 작성하면 CMake가 타겟 환경에 맞는 빌드 파일을 알아서 빌드를 해줍니다.
또한 운영체제마다 다른 빌드 시스템(Linux의 Make, Windows의 Visual Studio 등)을 사용하기 때문에 OS별로 빌드 스크립트를 중복 관리할 필요가 없습니다.
소스 파일이 늘어나도 설정 파일의 변수나 와일드카드 기능을 통해 복잡한 의존 관계를 간단하게 업데이트할 수 있습니다.
왜 CMake를 사용해야 할까요? 단순히 Visual Studio를 쓰기 싫어서가 아니라,
어떤 환경에서도 동작하는 지속 가능한 빌드 시스템이 필요하기 때문입니다.
전통적으로 Visual Studio 같은 IDE 없이 직접 코드를 빌드하기 위해서는 컴파일러 명령어를 터미널에 직접 입력해야 합니다.
# 1. 각 소스 파일(.c)을 컴파일하여 기계어인 목적 파일(.o)로 변환
gcc -c main.c # main.c -> main.o 생성
gcc -c A.c # A.c -> A.o 생성
# 2. 생성된 목적 파일들을 하나로 Link하여 최종 실행 파일(code.out) 생성
gcc -o code.out main.o A.o
파일이 2~3개일 때는 문제가 없지만, 소스 파일이 수십, 수백 개로 늘어난다면 이런 명령어를 매번 입력하는 것은 불가능에 가깝습니다.
이 과정을 자동화하기 위해 make가 등장했습니다.
Makefile에 빌드 규칙을 한 번만 적어두면, make가 파일의 변경 사항을 감지하여 필요한 부분만 효율적으로 컴파일해 줍니다. 즉 모든 명령어를 입력할 필요 없이 make 입력 한번으로 끝납니다.
# Makefile 예시
code.out: main.o A.o
gcc -o code.out main.o A.o
main.o: main.c
gcc -c main.c
A.o: A.c
gcc -c A.c
make는 훌륭한 도구이지만, 다음과 같은 한계가 있습니다.
1. 플랫폼 종속성: 리눅스에서 작성한 Makefile은 윈도우의 nmake나 Visual Studio 환경에서 그대로 사용할 수 없습니다. 운영체제마다 빌드 도구가 다르기 때문입니다.
2. 복잡한 문법: 프로젝트가 커질수록 Makefile 자체의 문법이 난해해져 유지보수가 어려워집니다.
3. IDE와의 연동성 부족: 현대 엔지니어링에서는 VS Code, CLion, Visual Studio 등 다양한 에디터를 사용하는데, 각 에디터에 맞는 프로젝트 파일을 수동으로 생성하는 것은 매우 비효율적입니다.
결국 CMake는 운영체제마다 다른 Makefile이나 프로젝트 파일을 자동으로 생성해 주는 메타 빌드 시스템으로서, 현대 C/C++ 개발 환경의 사실상 표준이 되었습니다.
Cmake의 단계는 아래 이미지와 같습니다.

CMake 문법을 3단계로 나누어서 설명해 보겠습니다.
🟢 당장 프로젝트를 빌드하는 데 필요한 것(기초)
🟡 협업·확장 가능한 구조를 만드는 데 필요한 것(모듈화)
🔵 대형 프로젝트, 패키징, 자동화까지 다루는 영역(심화-생략)
CMake 문법 자체가 워낙 방대하고 어려운 부분도 있어 해당 포스팅에서는 2단계까지만 설명하겠습니다.
최소한의 빌드 환경 구축에는 다음과 같은 과정이 필요합니다.
해당 단계의 목표는 수동으로 gcc 명령어를 치던 과정을 CMakeLists.txt 파일로 대체하는 것입니다. 이때 파일 이름은 대소문자를 구분하여 생성해야 합니다.
CMakeLists.txt 파일을 생성합니다.cmake_minimum_required(VERSION [버전]) : CMake의 최소 버전을 지정합니다.
project([프로젝트 이름] VERSION [발전 단계] LANGUAGES [사용 언어]) : 프로젝트 정보들을 설정합니다.
add_executable([실행파일 이름] [필요한 소스 파일들]) : 실행파일을 빌드할 때 [필요한 소스 코드들]을 컴파일하고 링크하여 [실행파일 이름]이라는 실행파일을 생성합니다.

# 폴더 구조
Project_Root/
├── CMakeLists.txt
├── mycomplex.cpp
└── mycomplex_test.cpp
프로젝트가 커지면 모든 소스를 한곳에 두는 것은 불가능합니다.
CMake의 핵심은 물리적으로는 폴더(Directory)로 나누고,
논리적으로는 타겟(Target)으로 묶어 의존성을 관리하는 것입니다.
add_subdirectory()를 사용하면 프로젝트를 기능 단위로 물리적으로 분리할 수 있습니다.
프로젝트의 파일 구조를 트리 형태로 조직화하여 관리의 편의성을 높이는 것입니다
폴더별로 CMakeLists.txt를 두어 해당 폴더 내의 소스들만 관리하게 함으로써 관심사를 분리합니다.
기본적으로 다음의 구조를 따릅니다.
# 폴더 구조
FIR-FILTER/
├── CMakeLists.txt # [Root] 프로젝트 전체 관리 정의
├── lib
│ ├── CMakeLists.txt # `mycomplex` 정적 라이브러리 타깃 정의
│ ├── mycomplex.h # `MyComplex` 클래스 선언(헤더)
│ └── mycomplex.cpp # `MyComplex` 클래스 구현(소스)
└── fir_1d
├── model
│ └── cpp
│ ├── CMakeLists.txt # fir_1d_ref 정적 라이브러리 타겟 정의
│ ├── fir_1d_ref.h # 1D FIR 모델 인터페이스(클래스 선언)
│ └── fir_1d_ref.cpp # 1D FIR 모델 구현
└── sim
└── tests
└── CMakeLists.txt # 최종 실행 파일 타겟 정의
A. 최상위 CMakeLists.txt가 가장 먼저 호출되어 프로젝트를 정의하고 하위 작업을 지정합니다.

# Root CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(FIR-FILTER VERSION 1.0 LANGUAGES C CXX)
# 하위 디렉토리 추가 (컴파일 순서 제어)
add_subdirectory(lib)
add_subdirectory(fir_1d/model/cpp)
add_subdirectory(fir_1d/sim/tests)
이때 하위 디렉토리는 명령어가 나열된 순서대로 탐색됩니다.
따라서 fir_1d와 같은 하위 디렉토리에서 lib의 결과물을 참조하여 빌드하려면, 반드시 lib을 먼저 추가하여 라이브러리 타겟이 먼저 정의되도록 해야 합니다.
B. 범용 라이브러리 모듈화
소스 코드를 재사용 가능한 이진 파일(Target)로 캡슐화하고, 컴파일에 필요한 헤더 경로를 외부에 전파합니다.

mycomplex라는 이름의 정적 라이브러리를 생성하고 이를 사용할 다른 타겟들에게 헤더 위치를 공개합니다.
# Lib CMakeLists.txt
# 라이브러리 생성
add_library(mycomplex
[STATIC|SHARED|MODULE]
mycomplex.cpp
)
# mycomplex가 사용할 헤더 파일 경로 지정
target_include_directories(mycomplex
[PRIVATE|PUBLIC|INTERFACE]
${CMAKE_CURRENT_SOURCE_DIR}
)
C. 소스 파일의 라이브러리화
fir_1d_ref 라이브러리를 생성하고, 자체 헤더 경로와 하위 의존성(mycomplex)을 모두 PUBLIC으로 설정하여 상위 타겟(test_fir_1d)으로 한 번에 전파하는 과정입니다.

add_library(fir_1d_ref STATIC fir_1d_ref.cpp)
# 상위 타겟에 라이브러리 전파
target_include_directories(fir_1d_ref
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
# 범용 라이브러리(mycomplex)를 fir_1d_ref와 연결
target_link_libraries(fir_1d_ref PUBLIC mycomplex)
타겟 생성
fir_1d_ref.cpp 소스 코드를 컴파일하여 정적 라이브러리(STATIC)로 만듭니다. 하나의 독립적인 모듈 블록을 생성하는 것입니다.
헤더 경로 전파 (PUBLIC)
target_include_directories를 PUBLIC으로 설정합니다. 이는 fir_1d_ref를 빌드할 때뿐만 아니라, 이 라이브러리를 가져다 쓰는 상위 실행 파일에게도 "내 헤더 파일은 이 폴더에 있어"라고 위치를 알려주는 역할을 합니다.
라이브러리 체이닝 (의존성 상속)
fir_1d_ref가 동작하려면 앞서 만든 mycomplex 라이브러리가 필요합니다. 이를 PUBLIC으로 링크하면, 나중에 최종 실행 파일이 fir_1d_ref 하나만 링크하더라도 하위의 mycomplex까지 자동으로 딸려와서 링크됩니다. 즉, 의존성이 체인처럼 연결됩니다.
참고:
하위 타겟 (Lower-level Target): 다른 모듈에 의존하지 않고 독립적으로 존재할 수 있는 기본 부품
상위 타겟 (Upper-level Target): 하위 부품들을 조립하여 더 복잡한 기능을 만드는 주체
D. 실행파일 및 라이브러리 연결
Modern CMake의 가장 강력한 기능입니다.
복잡한 헤더 경로를 일일이 추가할 필요 없이, 단순히 타겟을 link하는 것만으로 라이브러리의 모든 정보(헤더 경로, 컴파일 옵션 등)를 자동으로 상속받습니다.

# App CMakeLists.txt
# 실행 파일 생성
add_executable(test_fir_1d
test_fir_1d_golden.cpp
)
# fir_1d_ref가 PUBLIC으로 공개한 헤더 경로와 컴파일 옵션을 자동으로 상속받습니다.
target_link_libraries(test_fir_1d PRIVATE fir_1d_ref)
지금까지 다룬 내용을 바탕으로, 확장 가능한 프로젝트가 구성되는 단계별 흐름을 정리하면 다음과 같습니다.
1. 물리적 격리 (add_subdirectory): 소스 코드를 기능별 폴더로 분리하고 Root에서 순차적으로 호출하여, 빌드 시스템의 스코프(Scope)를 명확히 나눕니다.
2. 논리적 캡슐화 (add_library): 각 기능 모듈(예: mycomplex, fir_1d_ref)을 독립적인 타겟으로 묶어 재사용 가능한 블록으로 만듭니다.
3. 인터페이스 정의 (target_include_directories): 가시성(PRIVATE/PUBLIC/INTERFACE)을 설정하여 내부 구현은 숨기고, 외부 타겟과 결합할 때 필요한 헤더 경로만 선택적으로 전파합니다.
4. 의존성 체이닝 (target_link_libraries): 상위 타겟은 복잡한 경로 설정 없이 하위 타겟을 링크하는 것만으로 컴파일에 필요한 모든 정보를 자동으로 상속받아 최종 실행 파일(add_executable)을 완성합니다.
마이크로소프트가 제공하는 CMake Tools 익스텐션을 사용하면, 복잡한 명령어를 버튼 클릭으로 대체할 수 있습니다.
CMake Tools 설치합니다.CMake: Configure에서 컴파일러(Kit)를 선택하고, 빌드 시스템(Makefile 등)을 생성합니다.F7으로 빌드합니다.mkdir build
cd build
소스 코드와 빌드 결과물(.o, 실행 파일 등)이 섞이지 않도록 별도의 build 디렉토리를 생성하고 진입합니다.
cmake ..
현재 위치(build)에서 상위 디렉토리(..)에 있는 CMakeLists.txt를 읽어들여, 현재 OS와 컴파일러 환경에 맞는 빌드 시스템(Makefile 등)을 생성합니다. (Root 디렉토리로 이동하는 것이 아닙니다.)
cmake --build .
생성된 빌드 시스템을 이용해 소스 코드를 컴파일하고 타겟들을 링킹하여 최종 실행 파일을 만듭니다.
./fir_1d/sim/tests/test_fir_1d
최종 산출물은 Root의 build 폴더를 기준으로 CMakeLists.txt가 있던 계층 구조를 그대로 따라 생성됩니다.
mkdir build && cd build # 1. 빌드 폴더 격리
cmake .. # 2. 구성 (Configure)
cmake --build . # 3. 컴파일 및 링킹 (Build)
./fir_1d/sim/tests/test_fir_1d # 4. 최종 실행 (Run)
단순히 언어의 문법을 넘어, 코드가 어떻게 시스템 위에서 실행 파일이 되는지 이해해야 합니다.
프로젝트의 규모가 커지고 리눅스 서버나 임베디드 보드 등 다양한 환경을 마주하게 되면, "어디서든 빌드되는 프로젝트"를 설계하는 능력은 코딩 실력만큼이나 중요한 핵심 역량이 됩니다.
오늘 다룬 CMake는 단순한 컴파일 도구가 아닙니다.
복잡한 의존성을 통제하고 자동화하는 엔지니어링의 기초입니다.
이 글이 IDE의 편리한 'Build 버튼' 뒤에 숨겨진 원리를 이해하고, 더 견고하고 확장 가능한 프로젝트를 설계하는 첫걸음이 되기를 바랍니다.