CMake Error 자주 나는 10가지: find_package 실패·target not found·캐시 오염 해결

이 글의 핵심

CMake 에러는 메시지가 길고 실제 원인이 몇 줄 위에 있는 경우가 많아, 에러 문구만 검색하면 엉뚱한 해법에 시간을 쓰기 쉽습니다. 패키지 탐색 경로, 타겟 이름과 정의 순서, 변수 미정의, 캐시에 남은 이전 설정처럼 원인의 종류를 먼저 가르면 수정이 빨라집니다. CMakeLists.txt 기본 구조와 자주 쓰는 변수, 해결 체크리스트도 함께 정리했습니다.

들어가며: “CMake 에러가 너무 많아요”

CMake는 C++ 프로젝트의 빌드 시스템을 자동화하는 도구이지만, 에러 메시지가 불친절해서 초보자가 어려워합니다.

# ❌ 에러 코드
add_executable(myapp main.cpp)
target_link_libraries(myapp mylib)  # mylib 정의 없음

# CMake 구성 단계는 통과하고, 빌드(링크) 단계에서 실패:
# /usr/bin/ld: cannot find -lmylib

이 예제가 CMake를 처음 쓸 때 헷갈리는 이유를 잘 보여 줍니다. mylib이라는 타겟이 없으면 CMake는 에러를 내지 않고 “시스템에 있는 libmylib를 링크하라”는 뜻으로 해석합니다. 그래서 cmake ..은 성공하고, make나 ninja를 실행한 뒤 링커가 cannot find -lmylib(MSVC는 LNK1104: cannot open file 'mylib.lib')로 실패합니다. 타겟 이름 오타 하나가 CMake 에러가 아니라 링커 에러로 나타나는 것입니다. mylib::mylib처럼 ::가 들어간 이름을 쓰면 CMake가 반드시 타겟이어야 한다고 판단해 구성 단계에서 바로 에러를 내 주므로, 모던 CMake에서 네임스페이스가 붙은 타겟 이름을 권장하는 이유 중 하나가 이것입니다.

CMake 에러를 읽을 때는 어느 단계에서 실패했는지부터 구분하는 것이 가장 중요합니다. CMake는 CMakeLists.txt를 읽고 변수와 타겟을 만드는 구성(configure), 빌드 파일을 쓰는 생성(generate), 실제 컴파일러를 호출하는 빌드(build) 단계로 나뉩니다. CMake Error at CMakeLists.txt:12 (add_executable):처럼 파일과 줄 번호가 붙은 메시지는 구성 단계 에러이고, undefined reference나 cannot find -l...은 빌드 단계의 컴파일러·링커 에러라서 CMakeLists.txt의 문법이 아니라 타겟 설정 내용을 봐야 합니다. 그리고 CMake 에러 메시지는 첫 줄보다 바로 아래 들여쓴 설명과 “Call Stack”에 원인이 적혀 있는 경우가 많으므로, 출력 전체를 위에서부터 읽는 습관이 필요합니다.

이 글은 CMakeLists.txt를 직접 쓰다가 자주 만나는 작성 실수(버전, 타겟 이름, 괄호, 경로, 변수, 중복 타겟)를 에러 메시지별로 모았습니다. 컴파일러 탐지, 툴체인 파일, 링커, 런타임 라이브러리 경로, FetchContent처럼 단계별로 원인을 좁혀 가는 진단 절차는 CMake 에러 해결: 단계별로 좁히는 법에, find_package가 패키지를 못 찾는 경우는 CMake “Could NOT find” 에러에 따로 정리했습니다.


CMake 에러 10가지

에러 1: CMake version too old

CMake Error: CMake 3.20 or higher is required. You are running version 3.16

해결법 1: cmake_minimum_required 낮추기

# ❌ 높은 버전 요구
cmake_minimum_required(VERSION 3.20)

# ✅ 낮은 버전으로 변경
cmake_minimum_required(VERSION 3.16)

해결법 2: CMake 업데이트

# Ubuntu
sudo apt install cmake

# 또는 최신 버전 설치
wget https://github.com/Kitware/CMake/releases/download/v3.28.0/cmake-3.28.0-linux-x86_64.sh
sudo sh cmake-3.28.0-linux-x86_64.sh --prefix=/usr/local --skip-license

두 해결법 중 버전을 낮추는 방법은 신중해야 합니다. 프로젝트가 3.20을 요구하는 데는 대개 이유가 있습니다. 예를 들어 CMake 3.20 미만에서는 cmake_path() 명령이 없고, 3.19 미만에서는 CMakePresets.json을 읽지 못하며, 3.24 미만에서는 --fresh 옵션이 없습니다. 숫자만 낮추면 이번 에러는 사라지지만 몇 줄 아래에서 Unknown CMake command "cmake_path" 같은 다른 에러가 나옵니다. 남이 만든 프로젝트라면 CMake를 올리는 쪽이 맞고, 내 프로젝트라면 실제로 쓰는 기능이 요구하는 최소 버전으로 맞추면 됩니다.

cmake_minimum_required는 단순히 버전 확인만 하는 것이 아니라 정책(policy) 기본값도 결정합니다. 같은 CMakeLists.txt라도 최소 버전을 3.5로 적느냐 3.20으로 적느냐에 따라 일부 명령의 동작이 달라집니다. 반대 방향의 에러, 즉 CMake 4.0에서 오래된 서드파티 프로젝트가 Compatibility with CMake < 3.5 has been removed 에러로 멈추는 경우는 단계별 진단 글의 1.1절에서 우회 방법과 함께 다룹니다. 우분투의 apt는 LTS 배포판 시점의 버전을 제공하므로 최신 버전이 필요하면 Kitware의 APT 저장소나 pip install cmake를 쓰는 방법도 있습니다.

에러 2: target not found

# ❌ 에러 코드
target_link_libraries(myapp mylib)  # myapp이 아직 정의되지 않음
add_executable(myapp main.cpp)

# CMake Error at CMakeLists.txt:1 (target_link_libraries):
#   Cannot specify link libraries for target "myapp" which is not built by
#   this project.

해결: 타겟을 먼저 정의.

# ✅ 올바른 순서
add_library(mylib mylib.cpp)
add_executable(myapp main.cpp)
target_link_libraries(myapp mylib)  # myapp이 이미 정의됨

에러 메시지의 target은 첫 번째 인자(myapp)입니다. target_link_libraries, target_include_directories, target_compile_options처럼 target_으로 시작하는 명령은 모두 첫 인자로 받은 타겟에 속성을 추가하므로, 그 타겟이 명령보다 먼저 만들어져 있어야 합니다. 반면 두 번째 이후 인자(링크할 대상)는 생성 단계에서 해석되기 때문에, mylib을 target_link_libraries보다 나중에 add_library해도 문제없이 동작합니다. 서브디렉터리가 여러 개인 프로젝트에서 add_subdirectory 순서를 바꿨더니 이 에러가 사라지거나 생기는 것도 같은 이유입니다.

이 에러를 자주 만나는 또 다른 경우는 타겟 이름과 변수 이름을 혼동할 때입니다. project(MyApp) 후 add_executable(${PROJECT_NAME} ...)로 만들었는데 아래에서 target_link_libraries(myapp ...)라고 소문자로 쓰면, CMake 타겟 이름은 대소문자를 구분하므로 다른 타겟으로 취급됩니다.

에러 3: syntax error (괄호 불일치)

# ❌ 괄호 불일치
add_executable(myapp
    main.cpp
    utils.cpp
# 닫는 괄호 없음

# CMake Error at CMakeLists.txt:5:
#   Parse error.  Function missing ending ")".  End of file reached.

괄호 불일치 에러는 줄 번호가 실제 실수한 위치보다 뒤를 가리키는 경우가 많습니다. CMake는 닫는 괄호를 찾을 때까지 계속 읽기 때문에, 괄호를 빠뜨린 명령 다음의 모든 줄을 인자로 먹어 버리고 파일 끝에서야 에러를 냅니다. 그래서 에러가 파일 끝을 가리키면, 위쪽에서 여러 줄에 걸쳐 쓴 명령(add_executable, set, target_sources)부터 확인하는 것이 빠릅니다. 따옴표 불일치도 같은 증상을 보이는데, Windows 경로의 백슬래시가 "C:\path\"처럼 따옴표를 이스케이프해서 문자열이 닫히지 않는 경우가 흔합니다. CMake 안에서는 경로에 슬래시(/)를 쓰는 것이 안전합니다.

해결:

# ✅ 괄호 일치
add_executable(myapp
    main.cpp
    utils.cpp
)

에러 4: Could NOT find package

# ❌ 라이브러리 없음
find_package(Boost REQUIRED)

# CMake Error: Could NOT find Boost (missing: Boost_INCLUDE_DIR)

해결: CMake “Could NOT find” 에러 해결 참고.

# ✅ CMAKE_PREFIX_PATH 설정
set(CMAKE_PREFIX_PATH "/usr/local" ${CMAKE_PREFIX_PATH})
find_package(Boost REQUIRED)

# 또는 vcpkg 사용

위 코드처럼 CMakeLists.txt 안에서 set(CMAKE_PREFIX_PATH ...)로 경로를 하드코딩하는 것은 내 컴퓨터에서만 동작하는 빌드를 만들기 쉽습니다. 설치 경로는 사람마다 다르므로, 명령줄에서 cmake -B build -DCMAKE_PREFIX_PATH=/opt/mylibs로 넘기거나 CMakePresets.json에 두는 편이 낫습니다. Module 모드와 Config 모드의 차이, 버전·컴포넌트 불일치, vcpkg·Conan 연동까지 원인별 해결은 CMake “Could NOT find” 에러에서 순서대로 다루고, CMake 3.30에서 FindBoost가 제거 대상(정책 CMP0167)이 된 영향은 단계별 진단 글의 1.5절에 있습니다.

에러 5: 파일 경로 오류

# ❌ 파일 없음
add_executable(myapp
    main.cpp
    utils.cpp  # 파일이 없음
)

# CMake Error: Cannot find source file: utils.cpp

해결: 파일 존재 확인.

# 파일 확인
ls utils.cpp

# 또는 CMake 변수 사용
file(GLOB SOURCES "src/*.cpp")
add_executable(myapp ${SOURCES})

상대 경로의 소스 파일은 CMakeLists.txt가 있는 디렉터리(CMAKE_CURRENT_SOURCE_DIR) 기준으로 해석됩니다. 파일이 src/utils.cpp에 있는데 utils.cpp만 적었거나, 하위 디렉터리의 CMakeLists.txt에서 상위 경로 기준으로 적으면 이 에러가 납니다. Linux에서는 파일 이름의 대소문자도 구분하므로 Windows에서 되던 Utils.cpp가 CI에서 실패하는 경우도 흔합니다.

file(GLOB)은 편하지만 CMake 공식 문서도 소스 목록 수집용으로는 권장하지 않습니다. GLOB은 구성 시점에 한 번만 실행되기 때문에, 새 .cpp 파일을 추가해도 cmake를 다시 실행하기 전까지는 빌드에 포함되지 않습니다. 결과적으로 “파일을 추가했는데 undefined reference 링크 에러가 난다”는 혼란스러운 증상이 생깁니다. file(GLOB SOURCES CONFIGURE_DEPENDS "src/*.cpp")를 쓰면 빌드할 때마다 목록을 다시 확인하지만 모든 빌드 도구에서 보장되지는 않으므로, 소스 파일을 명시적으로 나열하는 것이 가장 확실합니다.

에러 6: 변수 미정의

# ❌ 변수 없음
target_include_directories(myapp PRIVATE ${MY_INCLUDE_DIR})

# 기본 설정에서는 경고 없이 빈 문자열로 치환됨
# cmake --warn-uninitialized 로 실행해야 경고가 나옴:
# CMake Warning (dev) at CMakeLists.txt:3 (target_include_directories):
#   uninitialized variable 'MY_INCLUDE_DIR'

CMake에서 정의되지 않은 변수는 에러도 경고도 없이 빈 문자열로 치환됩니다. 그래서 이 줄은 조용히 아무 경로도 추가하지 않고, 결과는 한참 뒤 컴파일 단계에서 fatal error: mylib.h: No such file or directory로 나타납니다. 변수 이름 오타(MY_INCLUDE_DIRS와 MY_INCLUDE_DIR)가 이런 식으로 숨어 있는 경우가 많은데, cmake --warn-uninitialized -B build로 한 번 구성해 보면 정의되지 않은 변수 사용을 모두 찾아 줍니다.

더 근본적인 해결책은 경로를 변수로 넘기지 않는 것입니다. 라이브러리가 target_include_directories(mylib PUBLIC include)로 자기 헤더 경로를 선언해 두면, 사용하는 쪽은 target_link_libraries(myapp PRIVATE mylib)만 해도 헤더 경로가 자동으로 전파됩니다. 변수로 경로를 주고받는 방식은 오래된 CMake 스타일이고, 타겟 속성으로 전파하는 방식이 모던 CMake의 핵심입니다. 자세한 내용은 CMake 타겟 가이드에서 다룹니다.

해결:

# ✅ 변수 정의
set(MY_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/include")
target_include_directories(myapp PRIVATE ${MY_INCLUDE_DIR})

에러 7: 중복 타겟 정의

# ❌ 중복 정의
add_executable(myapp main.cpp)
add_executable(myapp other.cpp)  # 같은 이름

# CMake Error: add_executable cannot create target "myapp" because 
#              another target with the same name already exists.

해결: 타겟 이름을 다르게.

# ✅ 다른 이름
add_executable(myapp main.cpp)
add_executable(myapp2 other.cpp)

실제로는 이렇게 한 파일에 같은 이름을 두 번 쓰는 경우보다, 서드파티 라이브러리를 add_subdirectory로 두 번 포함하거나 두 라이브러리가 같은 이름의 타겟(gtest, fmt 등)을 만드는 경우가 훨씬 흔합니다. 예를 들어 내 프로젝트와 의존 라이브러리가 각자 add_subdirectory(third_party/googletest)를 하면 add_library cannot create target "gtest" because another target with the same name already exists가 납니다. if(NOT TARGET gtest) add_subdirectory(...) endif()로 감싸거나, 의존성을 FetchContent로 관리하면 같은 이름의 의존성은 한 번만 가져오므로 이 문제를 피할 수 있습니다. 타겟 이름은 프로젝트 전체에서 전역이므로, 라이브러리를 만든다면 myproj_utils처럼 접두사를 붙이는 습관이 좋습니다.

에러 8: 잘못된 명령어

# ❌ 오타
add_executabel(myapp main.cpp)  # executable 오타

# CMake Error: Unknown CMake command "add_executabel".

해결: 철자 확인.

명령 이름은 대소문자를 구분하지 않으므로(ADD_EXECUTABLE도 동작) 이 에러는 거의 항상 오타이거나, 해당 명령을 제공하는 모듈을 include하지 않은 경우입니다. 예를 들어 FetchContent_Declare는 include(FetchContent)를 먼저 해야 하고, check_cxx_compiler_flag는 include(CheckCXXCompilerFlag)가 필요하며, gtest_discover_tests는 include(GoogleTest)가 필요합니다. 인터넷에서 복사한 스니펫이 이 에러를 낸다면 원문에서 include 줄이 빠지지 않았는지 확인하세요. 앞에서 말한 것처럼 설치된 CMake 버전보다 새로운 명령을 쓴 경우에도 같은 메시지가 나옵니다.

에러 9: 경로 공백 처리

# ❌ 공백 처리 안 함
set(MY_PATH C:/Program Files/MyLib)
target_include_directories(myapp PRIVATE ${MY_PATH})

# 에러: C:/Program과 Files/MyLib로 분리됨

# ✅ 따옴표 사용
set(MY_PATH "C:/Program Files/MyLib")
target_include_directories(myapp PRIVATE "${MY_PATH}")

CMake의 모든 값은 문자열이고, 따옴표 없는 인자는 공백과 세미콜론에서 나뉩니다. 첫 번째 set은 MY_PATH에 C:/Program;Files/MyLib라는 두 원소짜리 리스트를 저장하고, 결과적으로 존재하지 않는 두 경로가 인클루드 경로로 추가됩니다. CMake 리스트는 세미콜론으로 구분된 문자열이라는 점을 알면 이 동작이 이해됩니다. 따옴표로 감싸면 공백이 포함된 하나의 값이 됩니다.

실무 규칙은 “경로가 들어가는 변수를 쓸 때는 항상 "${VAR}"로 감싼다”입니다. 다만 의도적으로 리스트를 넘길 때는 따옴표를 빼야 합니다. set(SOURCES a.cpp b.cpp) 후 add_executable(app "${SOURCES}")로 쓰면 a.cpp;b.cpp라는 이름의 파일 하나를 찾게 됩니다. 사용자 이름에 공백이 있는 Windows 환경(C:/Users/Hong Gildong/...)에서 빌드 디렉터리 경로 때문에 서드파티 스크립트가 깨지는 경우도 있으므로, 가능하면 공백 없는 경로에 프로젝트를 두는 것이 불필요한 문제를 줄입니다.

에러 10: 빌드 디렉토리 오염

CMake Error: The source directory is the same as the binary directory.
             In-source builds are not allowed.

해결: out-of-source 빌드.

# ❌ in-source 빌드
cd project/
cmake .

# ✅ out-of-source 빌드
cd project/
mkdir build
cd build
cmake ..

# ✅ CMake 3.13+ 방식 (디렉터리 이동 없이)
cmake -S . -B build
cmake --build build

이 메시지는 CMake 자체의 에러가 아니라, 프로젝트가 CMakeLists.txt에서 in-source 빌드를 막아 둔 경우에 나오는 메시지입니다(LLVM, OpenCV 같은 큰 프로젝트가 이런 검사를 둡니다). CMake 자체는 in-source 빌드를 허용하지만, 그러면 소스 디렉터리에 CMakeCache.txt, CMakeFiles/, Makefile이 섞여 git status가 지저분해지고 깨끗하게 지우기도 어려워집니다.

더 곤란한 점은, 한 번 in-source로 구성하고 나면 소스 디렉터리에 남은 CMakeCache.txt 때문에 이후 build/에서 cmake ..을 실행해도 CMake가 계속 소스 디렉터리를 빌드 디렉터리로 착각한다는 것입니다. 이 경우 소스 디렉터리의 CMakeCache.txt와 CMakeFiles/를 직접 지워야 합니다. 제가 CMake 프로젝트를 다루며 가장 많이 겪은 “고쳤는데도 같은 에러” 상황의 원인도 대부분 이런 캐시였습니다. 컴파일러를 바꾸거나 find_package 경로를 고친 뒤에도 이전 값이 캐시에 남아 있으면 CMake는 그 값을 그대로 씁니다. 빌드 디렉터리를 통째로 지우거나 cmake --fresh -B build(3.24+)로 새로 구성하면 대부분 해결됩니다. out-of-source 빌드를 쓰면 이 “통째로 지우기”가 rm -rf build 한 줄로 끝난다는 것이 가장 실용적인 장점입니다.


CMakeLists.txt 문법

기본 구조

# 최소 버전
cmake_minimum_required(VERSION 3.16)

# 프로젝트 이름
project(MyProject)

# C++ 표준
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 실행 파일
add_executable(myapp
    src/main.cpp
    src/utils.cpp
)

# 헤더 경로
target_include_directories(myapp PRIVATE
    ${CMAKE_SOURCE_DIR}/include
)

# 라이브러리 링크
target_link_libraries(myapp
    pthread
)

이 구조는 최소한의 뼈대이고, 실무에서 바로 손볼 부분이 두 곳 있습니다. 첫째, pthread를 이름으로 직접 링크하면 Linux에서는 동작하지만 Windows(MSVC)에서는 LNK1104: cannot open file 'pthread.lib'로 실패합니다. 이식성을 원한다면 find_package(Threads REQUIRED) 후 target_link_libraries(myapp PRIVATE Threads::Threads)를 써야 플랫폼에 맞는 플래그가 들어갑니다. 둘째, target_link_libraries에 PRIVATE/PUBLIC/INTERFACE 키워드를 생략하면 예전 방식의 “키워드 없는” 시그니처가 되는데, 같은 타겟에 대해 키워드 있는 호출과 섞으면 The keyword signature for target_link_libraries has already been used with the target 에러가 납니다. 처음부터 키워드를 붙이는 습관이 좋습니다.

set(CMAKE_CXX_STANDARD 17)은 반드시 add_executable 앞에 있어야 합니다. 이 변수는 타겟이 만들어질 때 속성의 기본값으로 복사되므로, 타겟을 만든 뒤에 설정하면 적용되지 않습니다. 타겟별로 지정하고 싶다면 target_compile_features(myapp PRIVATE cxx_std_17)가 순서에 덜 민감합니다.

자주 쓰는 변수

# 소스 디렉토리
${CMAKE_SOURCE_DIR}

# 빌드 디렉토리
${CMAKE_BINARY_DIR}

# 현재 디렉토리
${CMAKE_CURRENT_SOURCE_DIR}

# 프로젝트 이름
${PROJECT_NAME}

# 컴파일러
${CMAKE_CXX_COMPILER}

CMAKE_SOURCE_DIR과 CMAKE_CURRENT_SOURCE_DIR의 차이는 프로젝트가 다른 프로젝트에 포함될 때 드러납니다. CMAKE_SOURCE_DIR은 최상위 CMakeLists.txt의 디렉터리라서, 내 라이브러리를 누군가 add_subdirectory나 FetchContent로 가져가면 그 사람 프로젝트의 루트를 가리킵니다. 그러면 ${CMAKE_SOURCE_DIR}/include가 엉뚱한 경로가 되어 헤더를 찾지 못합니다. 라이브러리 CMakeLists.txt에서는 CMAKE_CURRENT_SOURCE_DIR이나 PROJECT_SOURCE_DIR(가장 가까운 project() 기준)을 쓰는 것이 안전합니다. 위 기본 구조의 ${CMAKE_SOURCE_DIR}/include도 단독 실행 파일 프로젝트에서는 문제없지만, 재사용될 라이브러리라면 바꿔 두는 편이 좋습니다.

CMAKE_CXX_COMPILER는 구성 단계에서 한 번 결정되어 캐시됩니다. 이미 구성된 빌드 디렉터리에서 -DCMAKE_CXX_COMPILER=clang++로 컴파일러를 바꾸려 하면 CMake가 경고와 함께 캐시를 지우고 다시 구성하거나, 기대와 다르게 동작할 수 있습니다. 컴파일러를 바꿀 때는 새 빌드 디렉터리를 만드는 것이 원칙입니다.


같이 보면 좋은 글

자주 묻는 질문 (FAQ)

Q. CMakeLists.txt를 고쳤는데도 같은 에러가 계속 나면 무엇을 의심해야 하나요?

A. CMake는 이전 구성에서 찾은 패키지 경로, 옵션 값, 컴파일러 경로를 CMakeCache.txt에 저장해 두고 재구성할 때도 그대로 사용합니다. 그래서 find_package 경로를 바꾸거나 컴파일러를 교체해도 캐시가 남아 있으면 같은 에러가 반복되며, 본문의 빌드 디렉토리 오염 사례가 이 경우입니다. 빌드 디렉토리를 지우고 다시 구성하거나 CMake 3.24 이상에서는 cmake --fresh를 사용하고, 특정 변수만 바꿀 때는 -D로 캐시 값을 명시적으로 덮어쓰면 됩니다.