C++ Include Path | 인클루드 경로 가이드
이 글의 핵심
헤더를 못 찾는 에러는 파일이 없어서라기보다 컴파일러가 어느 디렉토리를 어떤 순서로 뒤지는지 모를 때 자주 생깁니다. 따옴표 include는 현재 파일 위치부터, 꺾쇠 include는 시스템과 -I 경로부터 찾는다는 차이를 기준으로 프로젝트 디렉토리 구조를 짜는 법, 경로 순서 때문에 엉뚱한 헤더가 잡히는 문제, 환경 변수로 경로를 지정하는 방법까지 정리했습니다.
들어가며
빌드가 깨지거나 잘못된 헤더가 끌려오는 문제는 대부분 인클루드 검색 경로 설정에서 시작됩니다. 이 글에서는 <>와 ""의 차이를 바탕으로, 툴체인별로 경로를 추가하는 방법과 읽기 쉬운 프로젝트 구조를 갖추는 데 필요한 기준을 정리합니다.
인클루드 경로는 헤더 파일을 찾는 디렉토리 목록입니다.
컴파일러가 헤더를 찾는 경로
#include <iostream> // -I 경로 → 시스템 경로 검색
#include "myheader.h" // 이 파일이 있는 디렉토리 → -I 경로 → 시스템 경로
차이점:
<>: 시스템 헤더 (표준 라이브러리, 설치된 서드파티 라이브러리)"": 사용자 헤더 (프로젝트 파일)
C++ 표준은 두 형식이 “구현이 정의한 방식으로” 헤더를 찾는다고만 규정하고, 구체적인 검색 순서는 컴파일러에 맡깁니다. 그래서 아래 순서는 GCC와 Clang의 실제 동작을 기준으로 한 설명이고, MSVC는 "" 형식에서 현재 파일뿐 아니라 그 파일을 포함한 상위 파일들의 디렉토리까지 거슬러 올라가며 찾는다는 점이 다릅니다. 이 차이 때문에 MSVC에서는 빌드되던 코드가 GCC에서는 헤더를 못 찾는 경우가 생깁니다.
전처리기가 #include를 만나면 해당 파일 내용을 그 자리에 그대로 복사해 넣습니다. 즉 헤더를 “찾는” 단계는 컴파일보다 앞선 전처리 단계에서 끝나며, 여기서 실패하면 컴파일 자체가 시작되지 않고 fatal error로 즉시 중단됩니다.
"" 와 <> 의 검색 순서
#include “file.h”: 현재 파일 디렉토리부터
#include를 쓴 파일이 있는 디렉토리 (터미널의 현재 작업 디렉토리가 아님)- -iquote 옵션 경로 (지정한 경우)
- -I 옵션 경로 (순서대로)
- -isystem 경로와 시스템 경로
#include <file.h>: -I 경로와 시스템 경로
- -I 옵션 경로 (순서대로)
- -isystem 경로와 시스템 경로
첫 번째 항목을 “현재 디렉토리”로 기억하면 혼동이 생깁니다. src/main.cpp에서 #include "utils.h"를 쓰면 컴파일러는 g++를 실행한 폴더가 아니라 src/ 폴더에서 utils.h를 찾습니다. 프로젝트 루트에서 빌드하든 build/ 폴더에서 빌드하든 결과가 같은 이유가 이것입니다. 반대로 헤더가 include/에 있으면 src/에서는 찾지 못하므로 -I가 필요합니다.
-isystem으로 추가한 경로는 시스템 헤더처럼 취급되어 그 안에서 나는 경고가 표시되지 않습니다. 서드파티 헤더에서 쏟아지는 -Wall -Wextra 경고를 가리고 싶을 때 -I 대신 -isystem을 쓰는 것이 일반적입니다. -iquote는 "" 형식에만 적용되므로, 프로젝트 헤더가 <> 형식으로 실수로 잡히지 않게 분리하고 싶을 때 씁니다.
-I 옵션과 CMake로 경로 추가하기
가장 단순한 include 구조
// main.cpp
#include <iostream> // 시스템
#include <vector>
#include <string>
#include "myclass.h" // 사용자
#include "utils.h"
int main() {
std::cout << "Hello\n";
return 0;
}
표준 라이브러리와 외부 라이브러리는 <>, 프로젝트 내부 헤더는 ""로 쓰는 것이 관례입니다. 기능상으로는 ""로 표준 헤더를 불러도 결국 시스템 경로까지 내려가 찾아지지만, 형식을 구분해 두면 코드를 읽는 사람이 “이 헤더가 프로젝트 안에 있는가”를 바로 알 수 있고, 프로젝트에 우연히 string.h 같은 이름의 파일이 생겼을 때 표준 헤더 대신 그 파일이 잡히는 사고를 줄일 수 있습니다.
-I 옵션의 순서와 형식
-I는 검색 경로 목록의 앞쪽에 디렉토리를 추가합니다. 여러 번 쓰면 적은 순서대로 검색되며, -I./include처럼 붙여 쓰든 -I ./include처럼 띄어 쓰든 같습니다. 상대 경로는 컴파일러를 실행하는 작업 디렉토리 기준이라는 점이 #include ""의 기준과 다르므로, 빌드 스크립트가 다른 폴더에서 실행되면 경로가 깨질 수 있습니다.
# -I 옵션으로 경로 추가
g++ -I./include main.cpp
# 여러 경로
g++ -I./include -I./lib/include main.cpp
# 상대 경로
g++ -I../common/include main.cpp
# 절대 경로 (비권장)
g++ -I/usr/local/mylib/include main.cpp
include/src를 나눈 디렉토리 구조
project/
├── include/
│ ├── myclass.h
│ └── utils.h
├── src/
│ ├── main.cpp
│ ├── myclass.cpp
│ └── utils.cpp
└── build/
# 컴파일
g++ -I./include src/main.cpp src/myclass.cpp -o build/myapp
# 또는 각각 컴파일
g++ -I./include -c src/main.cpp -o build/main.o
g++ -I./include -c src/myclass.cpp -o build/myclass.o
g++ build/main.o build/myclass.o -o build/myapp
-I는 컴파일 단계(-c)에만 필요하고 마지막 링크 단계에서는 의미가 없습니다. 헤더 검색 경로와 라이브러리 검색 경로(-L)는 완전히 별개라서, 헤더는 찾았는데 undefined reference 링크 에러가 난다면 -I가 아니라 -L과 -l 쪽을 봐야 합니다. utils.cpp를 컴파일 목록에서 빠뜨렸을 때 나는 에러도 헤더 문제가 아니라 링크 문제입니다.
target_include_directories 설정
CMake에서는 두 가지 방법이 있지만 현대 CMake에서는 target_include_directories를 권장합니다. include_directories()는 그 호출 이후 같은 디렉토리와 하위 디렉토리에서 정의되는 모든 타깃에 경로를 추가하므로, 테스트 타깃이나 서드파티 서브프로젝트에까지 경로가 새어 들어가 이름 충돌을 일으키기 쉽습니다.
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyApp)
# 인클루드 경로 추가
include_directories(${CMAKE_SOURCE_DIR}/include)
include_directories(${CMAKE_SOURCE_DIR}/lib/include)
# 또는 target 별로
add_executable(myapp
src/main.cpp
src/myclass.cpp
)
target_include_directories(myapp PRIVATE
${CMAKE_SOURCE_DIR}/include
)
target_include_directories의 PRIVATE, PUBLIC, INTERFACE 키워드는 경로가 어디까지 전파되는지를 정합니다. PRIVATE는 그 타깃 자신을 컴파일할 때만 쓰이고, PUBLIC은 그 타깃을 target_link_libraries로 링크하는 다른 타깃에도 전달되며, INTERFACE는 자신은 쓰지 않고 사용하는 쪽에만 전달됩니다. 라이브러리를 만든다면 공개 헤더 디렉토리는 PUBLIC, 내부 구현용 헤더 디렉토리는 PRIVATE로 나누는 것이 정석입니다. 이렇게 해두면 사용하는 쪽에서 경로를 따로 적지 않아도 링크만 하면 헤더가 보입니다.
컴파일러의 기본 시스템 경로 확인
헤더를 설치했는데도 못 찾는다면, 컴파일러가 실제로 어떤 디렉토리를 기본으로 뒤지는지부터 확인하는 것이 빠릅니다.
GCC/Clang
# GCC 기본 경로 확인
g++ -v -E -x c++ /dev/null
# 또는
echo | g++ -Wp,-v -x c++ - -fsyntax-only
# Clang
clang++ -v -E -x c++ /dev/null
출력 예시:
#include <...> search starts here:
/usr/include/c++/11
/usr/include/x86_64-linux-gnu/c++/11
/usr/local/include
/usr/include
출력에는 #include "..." search starts here:와 #include <...> search starts here: 두 구역이 나오며, 위에서 아래 순서가 곧 검색 순서입니다. 여기에 없는 경로에 설치된 라이브러리(예: Homebrew가 Apple Silicon Mac에 설치하는 /opt/homebrew/include)는 -I로 직접 알려줘야 합니다. 목록에 c++/11 같은 버전 번호가 보이는데, 여러 GCC 버전이 설치된 환경에서 Clang이 예상과 다른 버전의 libstdc++ 헤더를 잡는 문제도 이 출력으로 확인할 수 있습니다.
실제로 어떤 파일이 포함됐는지 보려면 g++ -H -fsyntax-only main.cpp를 쓰면 됩니다. 포함된 헤더가 들여쓰기 깊이(점 개수)와 함께 전체 경로로 출력되므로, 같은 이름의 헤더 중 어느 쪽이 잡혔는지 한눈에 보입니다.
헤더를 못 찾거나 엉뚱한 헤더가 포함될 때
fatal error: No such file or directory
# 에러
main.cpp:1:10: fatal error: myheader.h: No such file or directory
1 | #include "myheader.h"
# 해결
g++ -I./include main.cpp
이 에러가 나면 먼저 파일 이름의 대소문자를 확인합니다. Windows와 macOS 기본 파일 시스템은 대소문자를 구분하지 않아 #include "MyHeader.h"로 myheader.h를 불러도 빌드되지만, Linux CI 서버에서는 같은 코드가 No such file or directory로 실패합니다. 로컬에서는 잘 되다가 CI에서만 깨지는 include 에러의 상당수가 이 경우입니다. 그다음으로 -I에 준 경로가 헤더 파일 자체가 아니라 헤더가 들어 있는 디렉토리인지, 그리고 #include에 적은 하위 경로("mylib/api.h")와 합쳐 실제 경로가 맞는지 확인합니다.
절대 경로를 하드코딩함
절대 경로는 내 컴퓨터에서만 맞는 경로이므로 다른 사람이 저장소를 받으면 바로 빌드가 깨집니다. 빌드 설정에는 프로젝트 루트를 기준으로 한 경로를 쓰고, CMake에서는 ${CMAKE_SOURCE_DIR}이나 ${CMAKE_CURRENT_SOURCE_DIR} 변수로 기준점을 명시하는 것이 안전합니다. 서브프로젝트로 포함될 가능성이 있는 라이브러리라면 최상위 기준인 CMAKE_SOURCE_DIR보다 해당 CMakeLists.txt 위치 기준인 CMAKE_CURRENT_SOURCE_DIR이 맞습니다.
# ❌ 절대 경로 (이식성 낮음)
g++ -I/home/user/project/include main.cpp
# ✅ 상대 경로
g++ -I./include main.cpp
# ✅ CMake 변수 사용
include_directories(${CMAKE_SOURCE_DIR}/include)
-I 순서 때문에 다른 헤더가 먼저 잡힘
# 먼저 지정한 경로가 우선
g++ -I./include1 -I./include2 main.cpp
# include1/utils.h가 include2/utils.h보다 우선
순서 문제는 에러 없이 조용히 틀린 헤더를 쓴다는 점에서 더 위험합니다. 제가 자주 본 경우는 시스템에 설치된 구버전 라이브러리 헤더(/usr/include)와 프로젝트에 번들한 신버전 헤더가 섞이는 상황입니다. 헤더는 신버전을, 링크는 구버전 .so를 쓰게 되면 컴파일은 통과하는데 실행 중에 구조체 크기가 맞지 않아 알 수 없는 크래시가 납니다. 이럴 때는 -H 출력과 ldd 결과를 나란히 놓고 헤더와 라이브러리 버전이 같은지 확인하는 것이 가장 빠릅니다.
같은 이름의 헤더 파일 충돌
아래 예시에서 네임스페이스는 헤더 안의 심볼 충돌을 막아주는 것이지, 같은 파일 이름의 헤더 중 어느 쪽이 포함되는지를 정해주지는 않습니다. 파일 이름 충돌은 #include 경로에 디렉토리 접두사를 붙여서 해결해야 합니다.
// 두 경로에 같은 이름 헤더
// include1/utils.h
// include2/utils.h
// ❌ 모호함
#include "utils.h"
// ✅ 명시적 경로
#include "include1/utils.h"
// ✅ 네임스페이스 사용
namespace lib1 { /* ... */ }
namespace lib2 { /* ... */ }
접두사 방식을 쓰려면 -I를 include1이 아니라 그 상위 디렉토리로 지정해야 합니다. 그래서 많은 라이브러리가 include/<라이브러리이름>/header.h 구조를 씁니다. -I./include만 추가하고 #include "mylib/utils.h"로 쓰면, 다른 라이브러리에 utils.h가 있어도 충돌하지 않습니다. Boost(<boost/...>)나 nlohmann json(<nlohmann/json.hpp>)이 이 방식입니다.
C_INCLUDE_PATH·CPLUS_INCLUDE_PATH 환경 변수
# Linux/Mac
export C_INCLUDE_PATH=/usr/local/include
export CPLUS_INCLUDE_PATH=/usr/local/include
# Windows (PowerShell)
$env:CPLUS_INCLUDE_PATH="C:\libs\include"
# 사용
g++ main.cpp # 자동으로 경로 추가
GCC와 Clang은 CPATH(C/C++ 공통), C_INCLUDE_PATH(C 전용), CPLUS_INCLUDE_PATH(C++ 전용) 환경 변수를 읽어 검색 경로에 추가합니다. CPATH는 -I처럼, 나머지 두 변수는 -isystem처럼 취급됩니다. MSVC는 이 변수들을 읽지 않고 INCLUDE 환경 변수를 사용하므로 위 PowerShell 예시는 MinGW나 Clang을 쓸 때만 의미가 있습니다.
환경 변수 방식은 편하지만 빌드 설정이 저장소 밖에 숨는다는 단점이 큽니다. 내 셸에서는 빌드되는데 동료나 CI에서는 실패하는 “내 컴퓨터에서는 되는데” 문제의 전형적인 원인이 되므로, 개인 실험용으로만 쓰고 팀 프로젝트는 CMake 같은 빌드 설정에 경로를 명시하는 것이 좋습니다.
external 폴더의 헤더 전용 라이브러리 연결하기
디렉토리 구조와 컴파일 명령
project/
├── external/
│ └── json/
│ └── json.hpp
├── include/
│ └── config.h
└── src/
└── main.cpp
// main.cpp
#include <iostream>
#include "json.hpp" // external/json/json.hpp
#include "config.h" // include/config.h
int main() {
nlohmann::json j = {{"name", "홍길동"}};
std::cout << j.dump() << std::endl;
return 0;
}
# 컴파일
g++ -I./include -I./external/json src/main.cpp -o myapp
CMake로 같은 구성 만들기
cmake_minimum_required(VERSION 3.10)
project(MyApp)
set(CMAKE_CXX_STANDARD 17)
include_directories(
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/external/json
)
add_executable(myapp src/main.cpp)
이 예제는 단일 헤더 라이브러리를 external/에 복사해 두는 방식입니다. 설치 과정 없이 저장소만 받으면 빌드된다는 장점이 있지만, 라이브러리를 업데이트할 때 파일을 직접 교체해야 합니다. nlohmann json의 공식 배포 구조를 그대로 따르려면 external/nlohmann/json.hpp에 두고 -I./external과 #include <nlohmann/json.hpp>를 쓰는 편이 나중에 vcpkg나 find_package(nlohmann_json)로 옮길 때 소스 코드를 고치지 않아도 됩니다. 또 여기서도 include_directories 대신 target_include_directories(myapp PRIVATE ...)를 쓰면 타깃이 늘어났을 때 경로가 섞이지 않습니다.
인클루드 경로 정리
- <> vs "": 시스템 vs 사용자 헤더
- -I 옵션: 경로 추가
- 검색 순서: 포함하는 파일의 디렉토리 → -I → 시스템
- 상대 경로: 이식성 좋음
- CMake:
include_directories(),target_include_directories()
이어서 볼 글
같이 보면 좋은 글
- C++ Header Files
- #ifndef vs #pragma once
- C++ include 에러
- C++20 Modules
- C++ path
- Google Test로 C++ 단위 테스트 시작하기
- C++ 기본 초기화: 지역 변수가 쓰레기 값을 갖는 이유와 전역·정적 변수와의 차이
자주 묻는 질문 (FAQ)
Q. 같은 이름의 헤더가 여러 경로에 있으면 어느 파일이 포함되나요?
A. 컴파일러는 -I로 지정한 순서대로 경로를 검색하고 처음 발견한 파일을 사용하며, #include "..."는 그보다 먼저 현재 파일이 있는 디렉터리를 찾습니다. 그래서 config.h나 logging.h처럼 흔한 이름이 서드파티 경로에도 있으면 의도와 다른 헤더가 포함될 수 있습니다. GCC와 Clang에서는 -H 옵션으로 실제 포함된 헤더 경로를 확인할 수 있고, 프로젝트 헤더는 #include "myproj/config.h"처럼 디렉터리 접두사를 붙여 충돌을 피하는 것이 좋습니다.