C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)
이 글의 핵심
C++ CMake 고급에 대한 실전 가이드입니다. 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드) 등을 예제와 함께 상세히 설명합니다.
들어가며: 프로젝트가 커지면 빌드가 복잡해진다
”라이브러리 추가할 때마다 빌드가 깨져요”
프로젝트에 여러 라이브러리와 실행 파일이 있었습니다. 새 라이브러리를 추가할 때마다 빌드 설정이 꼬였습니다.
요구 환경: CMake 3.10+, g++/Clang 또는 MSVC. #4 CMake 입문에서 기본 사용법을 알고 있으면 좋습니다. 멀티 타겟·옵션·패키지 연동은 동일 도구 체인에서 확장하는 내용입니다.
CMake에서는 add_library로 공통 코드를 라이브러리 타겟으로 만들고, target_link_libraries로 의존 관계만 명시하면 됩니다. 타겟 단위로 나누어 두면 빌드 순서·include 경로·플랫폼별 옵션이 정리되고, 나중에 패키지 매니저(vcpkg, Conan)와 연동할 때도 한 타겟 단위로 붙이기 쉽습니다.
문제 시나리오: 실무에서 겪는 CMake 고통
시나리오 1: “include를 못 찾아요” 에러
fatal error: mylib.h: No such file or directory
원인: target_include_directories를 PRIVATE으로만 설정했거나, 링크 순서가 잘못됐을 때 발생합니다. 라이브러리 A가 B를 사용하는데, B의 include 경로가 A에 전파되지 않으면 이 에러가 납니다.
시나리오 2: “undefined reference” 링크 에러
undefined reference to `mylib::doSomething()'
원인: target_link_libraries에서 라이브러리를 링크하지 않았거나, STATIC/SHARED 혼동, 빌드 순서 문제입니다. add_subdirectory 순서가 잘못되면 아직 빌드되지 않은 타겟을 참조하게 됩니다.
시나리오 3: Windows에서만 빌드 실패
원인: 플랫폼별 라이브러리(ws2_32, pthread 등)를 링크하지 않았거나, 경로 구분자(/ vs \) 문제, DLL export 매크로 누락입니다.
시나리오 4: 외부 라이브러리 버전 충돌
원인: find_package로 시스템 라이브러리를 찾을 때, 프로젝트 A는 Boost 1.70, 프로젝트 B는 1.80을 요구하는 식으로 버전이 엇갈리면 빌드가 불안정해집니다. FetchContent로 버전을 고정하는 것이 해결책입니다.
시나리오 5: 빌드가 너무 느려요
원인: 모든 타겟이 같은 소스를 중복 컴파일하거나, PCH(프리컴파일 헀더)를 쓰지 않거나, 병렬 빌드(-j)를 활용하지 않는 경우입니다.
시나리오 6: 프로토콜 버퍼·코드 생성이 빌드에 포함되지 않아요
원인: .proto 파일을 빌드 전에 컴파일해야 하는데, add_custom_command로 생성 규칙을 정의하지 않아 의존성이 연결되지 않았습니다. 빌드 시점에 생성된 .pb.cc가 없어 링크 에러가 납니다.
시나리오 7: 설치한 라이브러리를 다른 프로젝트에서 find_package로 못 찾아요
원인: install(EXPORT ...)만 하고 Config.cmake·Version.cmake를 만들지 않았거나, CMAKE_MODULE_PATH에 설치 경로가 없을 때입니다. find_package(mylib)가 mylibConfig.cmake를 찾지 못해 실패합니다.
시나리오 8: CI에서 외부 라이브러리 다운로드가 타임아웃돼요
원인: FetchContent가 전체 Git 히스토리를 클론하거나, 네트워크가 불안정할 때 발생합니다. GIT_SHALLOW TRUE로 얕은 클론을 쓰고, GIT_SUBMODULES를 최소화하면 해결됩니다.
문제의 CMakeLists.txt: app1, app2, app3 세 개 실행 파일이 모두 같은 util.cpp, db.cpp를 소스에 나열하고 있습니다. util이나 db를 수정하면 세 타겟이 전부 다시 빌드되고, 새 앱을 추가할 때도 같은 소스 목록을 반복해서 적어야 합니다. 한 파일에 모든 걸 넣으면 중복이 늘고 실수하기 쉽습니다.
# ❌ 모든 것을 한 파일에
add_executable(app1 main1.cpp util.cpp db.cpp)
add_executable(app2 main2.cpp util.cpp db.cpp)
add_executable(app3 main3.cpp util.cpp db.cpp)
# 중복 코드, 관리 어려움
모듈화 후: **add_library(util STATIC util.cpp)**로 공통 코드를 정적 라이브러리(빌드 시 코드가 실행 파일에 포함되는 .a/.lib. 비유하면 공통 부품을 한 번만 만들어 두고 여러 제품에 끼워 넣는 것) 타겟(CMake에서 빌드할 대상 하나—실행 파일·라이브러리 등)으로 만들고, **add_library(db STATIC db.cpp)**로 DB 레이어도 별도 라이브러리로 둡니다. 각 실행 파일은 자기 main만 소스에 넣고, **target_link_libraries(app1 PRIVATE util db)**로 필요한 라이브러리만 링크합니다. 이렇게 하면 util·db는 한 번만 빌드되고, 앱은 링크만 하므로 빌드 시간이 줄고 의존 관계가 명확해집니다.
실행 가능 예제 (위 구조를 한 디렉터리에서 확인하려면):
main1.cpp 하나만 두고 CMake에서 add_executable(app1 main1.cpp) + target_link_libraries(app1 PRIVATE util db) 로 빌드할 수 있습니다. 아래는 app1만 쓰는 최소 예제입니다.
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o app1 main1.cpp && ./app1
#include <iostream>
int main() {
std::cout << "app1 (멀티 타겟 예제)\n";
return 0;
}
# ✅ 라이브러리로 분리
add_library(util STATIC util.cpp)
add_library(db STATIC db.cpp)
add_executable(app1 main1.cpp)
target_link_libraries(app1 PRIVATE util db)
add_executable(app2 main2.cpp)
target_link_libraries(app2 PRIVATE util db)
이 글을 읽으면:
- 멀티 타겟 프로젝트를 구성할 수 있습니다.
- 외부 라이브러리를 효과적으로 관리할 수 있습니다.
- 크로스 플랫폼 빌드를 설정할 수 있습니다.
- 실전에서 대규모 프로젝트를 빌드할 수 있습니다.
프로젝트 의존성 아키텍처
flowchart TB
subgraph apps["실행 파일"]
app1[app1]
app2[app2]
app3[app3]
end
subgraph libs["라이브러리"]
util[util]
db[db]
end
subgraph external["외부 의존성"]
boost[Boost]
json[nlohmann_json]
end
app1 --> util
app1 --> db
app2 --> util
app2 --> db
app3 --> util
app3 --> db
db --> boost
app1 --> json
목차
이 글에서 다루는 고급 주제: FetchContent·ExternalProject, add_custom_command·add_custom_target, Generator Expression, install 규칙(Config.cmake·Version.cmake), 프로토콜 버퍼 연동.
1. 프로젝트 구조
권장 디렉토리 구조
대규모 프로젝트에서는 루트 CMakeLists.txt에서 프로젝트 전역 설정(C++ 표준, 빌드 타입)만 하고, **add_subdirectory**로 src·lib·tests·external을 나눕니다. 각 서브디렉토리마다 자신만의 CMakeLists.txt를 두어, 해당 폴더의 타겟(실행 파일·라이브러리·테스트)만 정의합니다. 이렇게 하면 한 폴더를 보면 “어디에 뭐가 있는지”가 바로 보이고, 팀원이 특정 모듈만 수정하기 쉽습니다. 예시는 아래와 같은 구조입니다.
myproject/
├── CMakeLists.txt # 루트 CMake
├── src/
│ ├── CMakeLists.txt # 소스 CMake
│ ├── main.cpp
│ └── app/
│ ├── CMakeLists.txt
│ └── app.cpp
├── lib/
│ ├── CMakeLists.txt
│ └── mylib/
│ ├── CMakeLists.txt
│ ├── include/
│ │ └── mylib.h
│ └── src/
│ └── mylib.cpp
├── tests/
│ ├── CMakeLists.txt
│ └── test_main.cpp
└── external/ # 외부 라이브러리
└── CMakeLists.txt
루트 CMakeLists.txt
**cmake_minimum_required와 project는 최상단에 한 번만 둡니다. set(CMAKE_CXX_STANDARD 17)로 C++ 표준을 고정하고, CMAKE_BUILD_TYPE**이 비어 있으면 기본을 Release로 두어, 개발자가 옵션을 안 넘겼을 때도 일관된 빌드가 나오게 합니다. add_subdirectory(lib), add_subdirectory(src) 등으로 서브디렉토리를 넘기면, 각 폴더의 CMakeLists.txt가 순서대로 실행되어 라이브러리·실행 파일·테스트 타겟이 등록됩니다.
cmake_minimum_required(VERSION 3.15)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# C++ 표준 설정
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 빌드 타입 기본값
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
# 서브디렉토리 추가 (lib가 먼저 - 의존성 순서 중요)
add_subdirectory(lib)
add_subdirectory(src)
add_subdirectory(tests)
2. 타겟과 라이브러리
라이브러리 생성
STATIC은 .a(Linux/mac) 또는 .lib(Windows) 같은 정적 라이브러리로, 빌드 시점에 실행 파일에 포함됩니다. SHARED는 .so/.dylib/.dll 같은 동적 라이브러리로, 런타임에 로드됩니다. INTERFACE는 소스 파일이 없고 헤더만 있는 라이브러리(헤더 온리)에 씁니다. INTERFACE 타겟에는 **target_include_directories(mylib INTERFACE include/)**처럼 include 경로만 붙여 주면, 이 타겟을 링크하는 쪽이 자동으로 그 경로를 사용합니다.
# 정적 라이브러리
add_library(mylib STATIC
src/mylib.cpp
src/utils.cpp
)
# 동적 라이브러리
add_library(mylib SHARED
src/mylib.cpp
src/utils.cpp
)
# 헤더 온리 라이브러리
add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE include/)
타겟 속성 설정
**target_include_directories**는 “이 타겟을 빌드·사용할 때 어디를 include 경로로 쓸지”를 정합니다. PUBLIC은 mylib를 쓰는 쪽(app)도 필요하므로, app이 mylib를 링크하면 app에도 include 경로가 전파됩니다. PRIVATE은 mylib 내부 구현용이라 전파되지 않습니다. **target_compile_options**로 -Wall -Wextra 등을 타겟별로 넣고, **target_link_libraries**로 이 라이브러리가 의존하는 다른 라이브러리(dependency1, dependency2)를 지정합니다. dependency1을 PUBLIC으로 두면 mylib를 링크하는 실행 파일도 dependency1을 링크하게 됩니다.
add_library(mylib STATIC src/mylib.cpp)
# 인클루드 디렉토리
target_include_directories(mylib
PUBLIC include/ # 사용자도 필요
PRIVATE src/internal/ # 내부에서만 사용
)
# 컴파일 옵션
target_compile_options(mylib PRIVATE
-Wall -Wextra -Werror
)
# 링크 라이브러리
target_link_libraries(mylib
PUBLIC dependency1 # 사용자도 필요
PRIVATE dependency2 # 내부에서만 사용
)
PUBLIC vs PRIVATE vs INTERFACE
PUBLIC으로 링크하면 “B를 사용하는 쪽도 A가 필요하다”는 뜻이 됩니다. 그래서 C가 B만 링크해도 CMake가 “B가 PUBLIC으로 A를 쓰니까 C도 A를 링크해야 한다”고 판단해, C에는 A가 자동으로 전달됩니다. PRIVATE이면 B 내부에서만 A를 쓰고, C는 A를 알 필요가 없습니다. 예: B가 공개 API에서 A의 타입을 쓰면 PUBLIC, B가 구현부에서만 A를 쓰면 PRIVATE로 두면 됩니다.
add_library(A STATIC a.cpp)
add_library(B STATIC b.cpp)
add_library(C STATIC c.cpp)
# B는 A를 PUBLIC으로 링크
target_link_libraries(B PUBLIC A)
# C는 B를 링크
target_link_libraries(C PRIVATE B)
# → C는 자동으로 A도 링크됨 (PUBLIC이므로)
3. 타겟 속성 상세
주요 타겟 속성 요약
| 속성 | 용도 | 예시 |
|---|---|---|
INCLUDE_DIRECTORIES | include 경로 | target_include_directories |
COMPILE_OPTIONS | 컴파일 플래그 | -Wall, /W4 |
COMPILE_DEFINITIONS | 매크로 정의 | ENABLE_DEBUG=1 |
LINK_LIBRARIES | 링크할 라이브러리 | target_link_libraries |
CXX_STANDARD | C++ 표준 | 17, 20 |
POSITION_INDEPENDENT_CODE | PIC (공유 라이브러리용) | ON |
OUTPUT_NAME | 출력 파일명 | mylib → libmylib.a |
VERSION | 라이브러리 버전 | 1.2.3 |
get_target_property / set_target_properties
타겟 속성을 직접 읽거나 설정할 때 사용합니다.
# 속성 읽기
get_target_property(inc_dirs mylib INCLUDE_DIRECTORIES)
message(STATUS "mylib include dirs: ${inc_dirs}")
# 여러 속성 한 번에 설정
set_target_properties(mylib PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
POSITION_INDEPENDENT_CODE ON
VERSION 1.2.3
SOVERSION 1
)
Generator Expression 활용
빌드 타입·플랫폼에 따라 다른 값을 적용할 때 Generator Expression을 씁니다. $<...> 문법은 configure 시점이 아니라 생성 시점에 평가되므로, 빌드 타입·컴파일러·타겟 정보를 안전하게 참조할 수 있습니다.
# Debug일 때만 정의
target_compile_definitions(mylib PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE>
)
# 빌드 시 vs 설치 시 include 경로 분리
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# 링크 라이브러리: Debug일 때만 디버그 라이브러리
target_link_libraries(mylib PRIVATE
$<$<CONFIG:Debug>:debug_lib>
$<$<NOT:$<CONFIG:Debug>>:release_lib>
)
Generator Expression 주요 표현식
| 표현식 | 용도 |
|---|---|
$<CONFIG:Debug> | 빌드 타입이 Debug인지 |
$<BUILD_INTERFACE:path> / $<INSTALL_INTERFACE:path> | 빌드 시 vs 설치 후 경로 |
$<TARGET_FILE:tgt> | 타겟 출력 파일 경로 |
$<OR:cond1,cond2> | 조건 OR (쉼표 대신 사용) |
# 실행 파일 경로를 런타임에 참조 (테스트 등)
add_test(NAME myapp_test COMMAND $<TARGET_FILE:myapp> --test)
# 컴파일러별 옵션 (GNU/Clang vs MSVC)
target_compile_options(mylib PRIVATE
$<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>:-Wall -Wextra -Wpedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
)
# 빌드 타입별 출력 디렉토리
set_target_properties(myapp PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/bin/Debug
RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/bin/Release
)
인터페이스 라이브러리로 컴파일 옵션 전파
여러 타겟에 공통 컴파일 옵션을 적용할 때 INTERFACE 라이브러리를 만들어 전파합니다.
add_library(warnings INTERFACE)
target_compile_options(warnings INTERFACE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
add_library(mylib STATIC mylib.cpp)
target_link_libraries(mylib PRIVATE warnings)
4. 외부 라이브러리 관리
find_package
**find_package(Boost REQUIRED COMPONENTS system filesystem)**은 시스템(또는 vcpkg 등으로 설치된) Boost를 찾고, system, filesystem 컴포넌트가 있어야 한다고 요구합니다. 찾지 못하면 REQUIRED 때문에 설정 단계에서 에러가 납니다. 찾으면 Boost::system, Boost::filesystem 같은 imported 타겟이 생기므로, **target_link_libraries(myapp PRIVATE Boost::system Boost::filesystem)**만 하면 include 경로와 링크 설정이 자동으로 붙습니다. 패키지가 제공하는 타겟 이름은 해당 라이브러리 문서를 참고하면 됩니다.
# 시스템에 설치된 라이브러리 찾기
find_package(Boost REQUIRED COMPONENTS system filesystem)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE
Boost::system
Boost::filesystem
)
FetchContent (CMake 3.11+)
FetchContent는 CMake 설정 단계에서 Git 저장소나 아카이브 URL을 지정해 소스를 받고, 그 안의 CMake를 **FetchContent_MakeAvailable**로 처리합니다. 예시처럼 nlohmann/json을 선언해 두면, configure 시점에 해당 버전(v3.11.2)이 클론되고, nlohmann_json::nlohmann_json 타겟이 생성됩니다. 프로젝트에 서브모듈이나 수동 다운로드를 넣지 않아도 되고, 버전을 GIT_TAG로 고정해 재현 가능한 빌드를 만들 수 있습니다.
include(FetchContent)
# GitHub에서 라이브러리 다운로드
FetchContent_Declare(
json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(json)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json)
FetchContent와 find_package 우선순위
시스템에 설치된 패키지를 우선 쓰고, 없으면 FetchContent로 받는 패턴입니다.
# 1. 먼저 find_package 시도
find_package(nlohmann_json 3.10 QUIET)
# 2. 없으면 FetchContent
if(NOT nlohmann_json_FOUND)
include(FetchContent)
FetchContent_Declare(
json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(json)
endif()
target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json)
ExternalProject
ExternalProject_Add는 configure 단계가 아니라 빌드 단계에서 외부 프로젝트를 다운로드·설치합니다. URL로 tarball을 지정하거나 GIT_REPOSITORY로 저장소를 지정할 수 있고, CMAKE_ARGS로 그 프로젝트의 CMake 옵션(예: 설치 경로)을 넘깁니다. FetchContent는 configure 시점에 소스를 받아서 빌드 트리에 포함시키는 반면, ExternalProject는 별도 디렉토리에서 빌드·설치한 뒤 그 결과를 쓰는 방식이라, CMake 기반이 아닌 라이브러리나 “설치 후 find_package로 찾는” 방식에 맞습니다.
include(ExternalProject)
ExternalProject_Add(
libcurl
URL https://curl.se/download/curl-7.85.0.tar.gz
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/external
)
ExternalProject 완전 예제: CMake 기반 라이브러리 빌드·연동
의존 라이브러리를 빌드 단계에 다운로드·설치하고, IMPORTED 타겟으로 링크하는 패턴입니다. BUILD_BYPRODUCTS를 지정해야 매 빌드마다 재실행되지 않습니다.
include(ExternalProject)
set(SPDLOG_INSTALL_DIR ${CMAKE_BINARY_DIR}/external/spdlog)
ExternalProject_Add(spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.12.1
GIT_SHALLOW TRUE
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${SPDLOG_INSTALL_DIR}
-DSPDLOG_BUILD_SHARED=OFF -DSPDLOG_BUILD_TESTS=OFF
BUILD_BYPRODUCTS ${SPDLOG_INSTALL_DIR}/lib/libspdlog.a
)
add_library(spdlog::spdlog STATIC IMPORTED)
set_target_properties(spdlog::spdlog PROPERTIES
IMPORTED_LOCATION ${SPDLOG_INSTALL_DIR}/lib/libspdlog.a
INTERFACE_INCLUDE_DIRECTORIES ${SPDLOG_INSTALL_DIR}/include
)
add_dependencies(spdlog::spdlog spdlog)
FetchContent 고급 옵션
include(FetchContent)
# GIT_SHALLOW: 전체 히스토리 대신 최신 커밋만 (CI·빌드 시간 단축)
# PATCH_COMMAND: 패치 적용 (예: 호환성 수정)
# OVERRIDE_FIND_PACKAGE: find_package 결과를 무시하고 항상 FetchContent 사용
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.1.1
GIT_SHALLOW TRUE
GIT_PROGRESS ON
CMAKE_ARGS -DFMT_DOC=OFF -DFMT_TEST=OFF
)
FetchContent_MakeAvailable(fmt)
# 여러 의존성 한 번에 처리
FetchContent_Declare(catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_TAG v3.4.0)
FetchContent_MakeAvailable(catch2)
add_custom_command와 add_custom_target
코드 생성(프로토콜 버퍼, IDL, 코드 생성기)을 빌드 파이프라인에 넣을 때 사용합니다.
add_custom_command: 특정 출력 파일 생성
# .proto → .pb.cc, .pb.h 생성 (protobuf)
find_package(Protobuf REQUIRED)
set(PROTO_FILES proto/msg.proto)
set(GENERATED_PROTOBUF_SRCS)
foreach(PROTO_FILE ${PROTO_FILES})
get_filename_component(ABS_PROTO ${PROTO_FILE} ABSOLUTE)
get_filename_component(PROTO_NAME ${PROTO_FILE} NAME_WE)
set(GEN_PB_CC ${CMAKE_CURRENT_BINARY_DIR}/${PROTO_NAME}.pb.cc)
set(GEN_PB_H ${CMAKE_CURRENT_BINARY_DIR}/${PROTO_NAME}.pb.h)
list(APPEND GENERATED_PROTOBUF_SRCS ${GEN_PB_CC} ${GEN_PB_H})
add_custom_command(OUTPUT ${GEN_PB_CC} ${GEN_PB_H}
COMMAND protobuf::protoc
ARGS --cpp_out=${CMAKE_CURRENT_BINARY_DIR} -I${CMAKE_CURRENT_SOURCE_DIR} ${ABS_PROTO}
DEPENDS ${ABS_PROTO}
)
endforeach()
add_library(proto_lib ${GENERATED_PROTOBUF_SRCS})
target_include_directories(proto_lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(proto_lib PUBLIC protobuf::libprotobuf)
add_custom_target: 항상 실행되는 타겟
# 문서 생성 등, 출력 파일이 없거나 매 빌드마다 실행해야 할 때
add_custom_target(
generate_docs
COMMAND ${CMAKE_COMMAND} -E echo "Generating documentation..."
COMMAND doxygen ${CMAKE_SOURCE_DIR}/Doxyfile
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Running documentation generator"
)
# 특정 타겟 빌드 전에 실행하려면
add_dependencies(myapp generate_docs)
configure_file로 코드 생성
# version.h.in → version.h (configure 시점에 생성)
configure_file(
${CMAKE_SOURCE_DIR}/include/version.h.in
${CMAKE_BINARY_DIR}/include/version.h
@ONLY
)
target_include_directories(mylib PUBLIC ${CMAKE_BINARY_DIR}/include)
5. 빌드 옵션과 설정
옵션 정의
**option(이름 "설명" 기본값)으로 cmake 실행 시 -DBUILD_TESTS=OFF처럼 바꿀 수 있는 옵션을 둡니다. if(BUILD_TESTS)로 분기해 테스트 디렉토리를 add_subdirectory(tests)로 넣을지 말지 결정할 수 있고, target_compile_definitions(mylib PRIVATE ENABLE_LOGGING)**로 특정 타겟에만 매크로를 넘길 수 있습니다. 이렇게 하면 “테스트는 빌드하지 않고”, “로깅은 끈” 빌드 등을 한 줄 옵션으로 제어할 수 있습니다.
option(BUILD_TESTS "Build tests" ON)
option(BUILD_EXAMPLES "Build examples" OFF)
option(ENABLE_LOGGING "Enable logging" ON)
if(BUILD_TESTS)
add_subdirectory(tests)
endif()
if(ENABLE_LOGGING)
target_compile_definitions(mylib PRIVATE ENABLE_LOGGING)
endif()
빌드 타입별 설정
CMAKE_BUILD_TYPE이 Debug이면 CMAKE_CXX_FLAGS_DEBUG가, Release이면 CMAKE_CXX_FLAGS_RELEASE가 컴파일러에 전달됩니다. Debug는 -g(디버그 정보), -O0(최적화 끔), **-DDEBUG**로 디버깅에 유리하게 하고, Release는 -O3, **-DNDEBUG**로 성능과 assert 비활성화를 적용합니다. RelWithDebInfo는 **-O2 -g**로 최적화와 디버그 심볼을 함께 써서, 크래시 시 스택을 보면서 성능도 어느 정도 나오게 할 때 쓰입니다.
# Debug 빌드
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -DDEBUG")
# Release 빌드
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
# RelWithDebInfo 빌드
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g")
플랫폼별 설정
WIN32, APPLE, **UNIX는 CMake가 제공하는 플랫폼 변수입니다. target_compile_definitions**로 PLATFORM_WINDOWS 같은 매크로를 넘기면, 코드에서 #ifdef PLATFORM_WINDOWS로 분기할 수 있습니다. Windows에서는 ws2_32(Winsock), Linux에서는 **pthread처럼 플랫폼 전용 라이브러리를 target_link_libraries**로 타겟에만 붙이면, 다른 OS에서는 링크되지 않아 크로스 플랫폼을 유지할 수 있습니다.
if(WIN32)
# Windows 전용
target_compile_definitions(myapp PRIVATE PLATFORM_WINDOWS)
target_link_libraries(myapp PRIVATE ws2_32)
elseif(APPLE)
# macOS 전용
target_compile_definitions(myapp PRIVATE PLATFORM_MACOS)
elseif(UNIX)
# Linux 전용
target_compile_definitions(myapp PRIVATE PLATFORM_LINUX)
target_link_libraries(myapp PRIVATE pthread)
endif()
6. 자주 발생하는 오류와 해결법
오류 1: “No such file or directory” (헤더를 못 찾음)
원인: target_include_directories를 설정하지 않았거나, PUBLIC/PRIVATE을 잘못 썼을 때 발생합니다.
해결법:
# ❌ 잘못된 예: include 경로 없음
add_library(mylib STATIC src/mylib.cpp)
# ✅ 올바른 예: PUBLIC으로 사용자에게 전파
add_library(mylib STATIC src/mylib.cpp)
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
오류 2: “undefined reference” (링크 에러)
원인: target_link_libraries에서 라이브러리를 누락했거나, add_subdirectory 순서가 잘못됐을 때입니다.
해결법:
# ❌ 잘못된 예: util을 링크하지 않음
add_executable(app1 main1.cpp)
# ✅ 올바른 예: 모든 의존성 명시
add_executable(app1 main1.cpp)
target_link_libraries(app1 PRIVATE util db)
오류 3: “Cannot find package” (find_package 실패)
원인: 패키지가 시스템에 설치되지 않았거나, CMAKE_PREFIX_PATH가 설정되지 않았을 때입니다.
해결법:
# vcpkg 사용 시
cmake -B build -DCMAKE_TOOLCHAIN_FILE=[vcpkg root]/scripts/buildsystems/vcpkg.cmake
# Conan 사용 시
cmake -B build -DCMAKE_PREFIX_PATH=$(pwd)/build
# find_package 전에 경로 힌트
set(CMAKE_PREFIX_PATH "/opt/mylib;$ENV{HOME}/local")
find_package(Boost REQUIRED)
오류 4: “Duplicate symbol” (중복 정의)
원인: 헤더에 구현을 넣었는데 inline이나 static을 쓰지 않아, 여러 .cpp에서 include할 때 중복 정의가 발생합니다.
해결법: 헤더 온리 라이브러리는 구현을 inline으로 두거나, .cpp로 분리합니다.
// ❌ 잘못된 예: 헤더에 일반 함수 정의
// mylib.h
int add(int a, int b) { return a + b; }
// ✅ 올바른 예: inline 사용
inline int add(int a, int b) { return a + b; }
오류 5: Windows DLL “dll not found” 런타임 에러
원인: DLL이 실행 파일과 같은 디렉토리에 없거나, PATH에 없을 때입니다.
해결법:
# 빌드 출력을 같은 디렉토리로
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
오류 6: Generator Expression 문법 오류
원인: $<...> 안에 쉼표를 잘못 쓰면 CMake가 인자를 잘못 해석합니다.
해결법:
# ❌ 잘못된 예: 쉼표가 인자 구분으로 해석됨
$<CXX_COMPILER_ID:GNU,Clang>
# ✅ 올바른 예: 세미콜론으로 구분하거나 따로 처리
$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>
오류 7: “add_subdirectory” 순서로 인한 빌드 실패
원인: app이 mylib를 사용하는데, add_subdirectory(src)가 add_subdirectory(lib)보다 먼저 오면 mylib 타겟이 아직 정의되지 않아 에러가 납니다.
해결법: 의존성 순서를 지킵니다. 라이브러리 → 실행 파일 → 테스트 순으로 add_subdirectory를 호출합니다.
# ✅ 올바른 순서
add_subdirectory(lib) # mylib 먼저
add_subdirectory(src) # myapp이 mylib 사용
add_subdirectory(tests) # 테스트가 mylib, myapp 사용
오류 8: “Policy CMP0077” 경고
원인: find_package가 CMAKE_PREFIX_PATH를 무시하는 이전 동작을 쓰고 있을 때입니다.
해결법:
cmake_minimum_required(VERSION 3.13)
# 또는
cmake_policy(SET CMP0077 NEW)
오류 9: add_custom_command OUTPUT이 빌드되지 않아요
원인: add_custom_command의 OUTPUT 파일을 어떤 타겟의 소스/의존성에도 연결하지 않으면, CMake가 “이 출력이 필요하다”고 판단하지 못해 명령을 실행하지 않습니다.
해결법:
# ✅ OUTPUT을 add_library나 add_executable의 소스에 포함
add_library(proto_lib ${GENERATED_PROTOBUF_SRCS})
# 또는 add_custom_target으로 의존성 연결
add_custom_target(proto_gen DEPENDS ${GEN_PB_CC} ${GEN_PB_H})
add_dependencies(proto_lib proto_gen)
오류 10: “Could not find a package configuration file” (Config.cmake 없음)
원인: find_package(mylib)가 mylibConfig.cmake 또는 Findmylib.cmake를 찾지 못할 때입니다. 라이브러리를 설치했지만 Config 파일을 만들지 않았거나, CMAKE_PREFIX_PATH에 설치 경로가 없을 수 있습니다.
해결법:
# 설치 경로를 CMAKE_PREFIX_PATH에 추가
cmake -B build -DCMAKE_PREFIX_PATH=/usr/local
# 또는 CMakeLists.txt에서
list(APPEND CMAKE_PREFIX_PATH "/opt/mylib")
find_package(mylib REQUIRED)
오류 11: ExternalProject 빌드가 매번 재실행돼요
원인: BUILD_BYPRODUCTS를 지정하지 않으면, CMake가 출력 파일을 추적하지 못해 “항상 최신이 아니다”로 판단해 매 빌드마다 다시 실행합니다.
해결법:
ExternalProject_Add(
mydep
...
BUILD_BYPRODUCTS ${INSTALL_DIR}/lib/libmydep.a
)
7. 실전 패턴
패턴 1: 모듈식 프로젝트
**$<BUILD_INTERFACE:...>와 $<INSTALL_INTERFACE:...>**는 generator expression으로, “빌드할 때”는 현재 소스 디렉토리의 include를 쓰고, “설치된 패키지를 쓸 때”는 설치 경로의 include를 쓰게 합니다. 이렇게 하면 같은 CMakeLists.txt로 프로젝트 내 빌드와, 설치 후 다른 프로젝트에서 find_package(mylib)로 쓸 때 모두 올바른 include 경로가 전달됩니다.
# lib/mylib/CMakeLists.txt
add_library(mylib STATIC
src/mylib.cpp
src/utils.cpp
)
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# src/CMakeLists.txt
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)
패턴 2: 테스트 통합
**enable_testing()**으로 이 프로젝트에서 CTest를 쓰겠다고 선언하고, FetchContent로 Google Test를 받아 **FetchContent_MakeAvailable(googletest)**로 사용 가능하게 합니다. 테스트 실행 파일 mylib_test는 mylib와 gtest_main을 링크하고, **add_test(NAME mylib_test COMMAND mylib_test)**로 CTest에 등록합니다. 이후 ctest 또는 **cmake --build build && ctest --test-dir build**로 테스트를 한 번에 실행할 수 있습니다.
# tests/CMakeLists.txt
enable_testing()
# Google Test 다운로드
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)
# 테스트 추가
add_executable(mylib_test test_mylib.cpp)
target_link_libraries(mylib_test PRIVATE
mylib
gtest_main
)
add_test(NAME mylib_test COMMAND mylib_test)
패턴 3: 설치 규칙
**install(TARGETS mylib ...)**로 빌드된 라이브러리·실행 파일을 make install 또는 cmake --install 시 어디에 복사할지 정합니다. LIBRARY는 공유 라이브러리, ARCHIVE는 정적 라이브러리, RUNTIME은 실행 파일용입니다. **EXPORT mylibTargets로 이 타겟 목록을 export하고, install(EXPORT ...)**로 mylibTargets.cmake를 만들어 두면, 다른 프로젝트에서 **find_package(mylib)로 찾을 때 타겟과 경로가 자동으로 설정됩니다. install(DIRECTORY include/)**로 헤더도 설치 경로에 복사합니다.
# 라이브러리 설치
install(TARGETS mylib
EXPORT mylibTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
)
# 헤더 설치
install(DIRECTORY include/
DESTINATION include
)
# CMake 설정 파일 생성
install(EXPORT mylibTargets
FILE mylibTargets.cmake
NAMESPACE mylib::
DESTINATION lib/cmake/mylib
)
설치 규칙 완전 가이드: find_package 호환 Config.cmake
다른 프로젝트에서 **find_package(mylib 1.0 REQUIRED)**로 찾을 수 있게 하려면 mylibConfig.cmake와 mylibConfigVersion.cmake를 설치해야 합니다.
# 1. Version.cmake + Config.cmake 생성
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
# 2. cmake/mylibConfig.cmake.in: @PACKAGE_INIT@ + include("${CMAKE_CURRENT_LIST_DIR}/mylibTargets.cmake")
configure_package_config_file(
${CMAKE_SOURCE_DIR}/cmake/mylibConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/mylibConfig.cmake
INSTALL_DESTINATION lib/cmake/mylib
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/mylibConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/mylibConfigVersion.cmake
DESTINATION lib/cmake/mylib
)
설치 후 다른 프로젝트: set(CMAKE_PREFIX_PATH "/usr/local") 후 find_package(mylib 1.0 REQUIRED) → target_link_libraries(myapp PRIVATE mylib::mylib)
패턴 4: 버전 관리
**project(MyProject VERSION 1.2.3)**으로 버전을 주면 MyProject_VERSION_MAJOR/MINOR/PATCH 변수가 생깁니다. **configure_file(version.h.in ...)**는 version.h.in 안의 @MyProject_VERSION_MAJOR@ 같은 플레이스홀더를 실제 값으로 치환해 빌드 디렉토리에 version.h를 만듭니다. 소스 코드에서 **#include "version.h"**로 버전을 참조하면, CMake만 다시 실행해도 버전이 반영되므로 유지보수가 쉽습니다.
# CMakeLists.txt
project(MyProject VERSION 1.2.3)
# 버전 헤더 생성
configure_file(
${CMAKE_SOURCE_DIR}/version.h.in
${CMAKE_BINARY_DIR}/version.h
)
target_include_directories(mylib PUBLIC ${CMAKE_BINARY_DIR})
// version.h.in
#define PROJECT_VERSION_MAJOR @MyProject_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @MyProject_VERSION_MINOR@
#define PROJECT_VERSION_PATCH @MyProject_VERSION_PATCH@
패턴 5: 프리컴파일 헤더
**target_precompile_headers**로 자주 쓰는 표준 헤더(<iostream>, <vector> 등)를 미리 컴파일해 두면, mylib의 모든 .cpp에서 그 헤더를 include할 때 재사용되어 컴파일 시간이 줄어듭니다. PRIVATE이므로 mylib를 링크하는 쪽에는 PCH가 노출되지 않고, mylib 빌드에만 적용됩니다. 대형 프로젝트에서 컴파일이 느릴 때 시도해 볼 만한 최적화입니다.
add_library(mylib STATIC src/mylib.cpp)
# 프리컴파일 헤더 (CMake 3.16+)
target_precompile_headers(mylib PRIVATE
<iostream>
<vector>
<string>
<memory>
)
8. 모범 사례와 프로덕션 패턴
모범 사례 체크리스트
- 타겟 단위 모듈화: 공통 코드는
add_library로 분리 - PUBLIC/PRIVATE 명확히: API에 노출되는 의존성만 PUBLIC
- FetchContent로 버전 고정: 재현 가능한 빌드
- 플랫폼별 분리:
if(WIN32)등으로 OS별 설정 - 옵션으로 기능 토글:
option(BUILD_TESTS ON)등 - 설치 규칙 정의:
install(TARGETS ...)로 배포 준비 - 경고 수준 통일:
-Wall -Wextra또는/W4
프로덕션 패턴 1: 통합 CMake 모듈
공통 설정을 cmake/ 폴더에 모아 두고 include합니다.
# cmake/CompilerWarnings.cmake
function(set_project_warnings target)
target_compile_options(${target} PRIVATE
$<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>:-Wall -Wextra -Wpedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
endfunction()
# CMakeLists.txt
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
include(CompilerWarnings)
add_library(mylib STATIC mylib.cpp)
set_project_warnings(mylib)
프로덕션 패턴 2: 다중 구성 빌드 (Multi-Config)
Visual Studio나 Ninja Multi-Config에서는 CMAKE_BUILD_TYPE 대신 CMAKE_CONFIGURATION_TYPES를 사용합니다.
if(CMAKE_CONFIGURATION_TYPES)
set(CMAKE_CONFIGURATION_TYPES "Debug;Release;RelWithDebInfo" CACHE STRING "" FORCE)
endif()
프로덕션 패턴 3: CI/CD용 최소 빌드
CI에서 빠른 검증을 위해 테스트만 빌드하는 옵션을 둡니다.
option(BUILD_TESTING "Build tests for CI" ON)
option(BUILD_BENCHMARKS "Build benchmarks" OFF)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
프로덕션 패턴 4: AddressSanitizer/UB Sanitizer 통합
디버그 빌드에서 메모리 오류를 검출하려면 Sanitizer를 활성화합니다.
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
if(ENABLE_ASAN AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(mylib PRIVATE -fsanitize=address)
target_link_options(mylib PRIVATE -fsanitize=address)
endif()
if(ENABLE_UBSAN AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(mylib PRIVATE -fsanitize=undefined)
target_link_options(mylib PRIVATE -fsanitize=undefined)
endif()
프로덕션 패턴 5: 완전한 예제 프로젝트
아래는 위 패턴들을 모두 적용한 실전용 CMake 프로젝트 예제입니다.
# 루트 CMakeLists.txt - 프로덕션 수준
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# 옵션
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
option(BUILD_TESTS "Build tests" ON)
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
# 모듈 경로
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
# 서브디렉토리 (의존성 순서)
add_subdirectory(external)
add_subdirectory(lib)
add_subdirectory(src)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# 설치
include(GNUInstallDirs)
install(TARGETS myapp
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
# external/CMakeLists.txt - FetchContent로 의존성
include(FetchContent)
FetchContent_Declare(
json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(json)
# lib/mylib/CMakeLists.txt - 라이브러리
add_library(mylib STATIC
src/mylib.cpp
)
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(mylib PUBLIC nlohmann_json::nlohmann_json)
target_compile_options(mylib PRIVATE -Wall -Wextra)
빌드 흐름 다이어그램
sequenceDiagram
participant Dev as 개발자
participant CMake as CMake Configure
participant Fetch as FetchContent
participant Build as 빌드 시스템
Dev->>CMake: cmake -B build
CMake->>Fetch: 외부 라이브러리 다운로드
Fetch->>CMake: 타겟 생성
CMake->>CMake: 의존성 그래프 구성
CMake->>Build: Makefile/Ninja 생성
Dev->>Build: cmake --build build
Build->>Build: lib 빌드 (mylib)
Build->>Build: src 빌드 (myapp)
Build->>Build: tests 빌드 (선택)
프로덕션 배포 체크리스트
| 항목 | 확인 |
|---|---|
CMAKE_EXPORT_COMPILE_COMMANDS ON | IDE 자동완성용 |
CMAKE_CXX_STANDARD 고정 | 재현 가능한 빌드 |
FetchContent GIT_TAG 고정 | 의존성 버전 일관성 |
install(TARGETS ...) 정의 | 패키징·배포 준비 |
| 플랫폼별 테스트 | Windows/Linux/macOS CI |
| 경고를 에러로 | -Werror 또는 /WX |
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 패키지 매니저 | vcpkg·Conan으로 “라이브러리 설치 지옥” 탈출하기
- CMake 입문 | 수십 개 파일 컴파일할 때 필요한 빌드 자동화 (CMakeLists.txt 기초)
- C++ 개발 환경 구축 | “C++ 어디서 시작하죠?” 컴파일러 설치부터 Hello World까지
이 글에서 다루는 키워드 (관련 검색어)
CMake 고급, 멀티 타겟, 크로스 플랫폼 빌드, 대규모 프로젝트 CMake, 빌드 시스템 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 기능 | 명령어 |
|---|---|
| 라이브러리 생성 | add_library |
| 실행 파일 생성 | add_executable |
| 링크 | target_link_libraries |
| 인클루드 | target_include_directories |
| 외부 라이브러리 | find_package, FetchContent |
| 옵션 | option |
| 테스트 | enable_testing, add_test |
| 타겟 속성 | set_target_properties, get_target_property |
핵심 원칙:
- 모듈화 (라이브러리 분리)
- PUBLIC/PRIVATE 명확히
- FetchContent로 외부 라이브러리
- 플랫폼별 설정 분리
- 테스트 자동화
- 설치 규칙으로 배포 준비
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. CMake로 멀티 타겟 프로젝트를 구성하고, 외부 라이브러리를 관리하며, 크로스 플랫폼 빌드를 설정하는 방법을 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
한 줄 요약: CMake로 대규모 프로젝트를 서브디렉터리·타겟·옵션으로 체계적으로 빌드할 수 있습니다. 다음으로 패키지 매니저(#17-2)를 읽어보면 좋습니다.
다음 글: [C++ 실전 가이드 #17-2] 패키지 매니저: vcpkg와 Conan으로 의존성 관리하기
이전 글: [C++ 실전 가이드 #16-3] 로깅과 Assertion: 버그를 빠르게 추적하는 방법
관련 글
- C++ 패키지 매니저 | vcpkg·Conan으로
- C++ 반복자 기초 완벽 가이드 | iterator 카테고리·begin/end·역방향 반복자·실전 패턴
- C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional
- C++ 크로스 플랫폼 빌드 | Windows·Linux·macOS·모바일 완벽 가이드 [#55-4]
- C++ GDB/LLDB | cout 100개 찍어도 못 찾은 버그, 디버거로 5분 만에 해결