C++ include 에러 No such file or directory: 헤더 경로·대소문자·순환 include 해결
이 글의 핵심
Windows에서 잘 되던 빌드가 Linux에서 헤더를 못 찾거나, 두 헤더가 서로를 include해 영문 모를 타입 에러가 나는 경우가 흔합니다. 꺾쇠와 따옴표 include의 탐색 순서 차이, 전방 선언이 가능한 경우와 불가능한 경우, pragma once와 ifndef 가드의 차이를 정리해 경로 문제와 구조 문제를 구분할 수 있게 했습니다. 외부 라이브러리와 프로젝트 내부 헤더 사례도 함께 봅니다.
들어가며: “헤더 파일을 못 찾는다는 에러가 나요”
C++에서 include 에러는 초보자가 가장 자주 겪는 컴파일 에러 중 하나입니다. 파일 경로, 순환 include, 전방 선언 등 여러 원인이 있습니다.
// ❌ 에러 코드
#include <iostrem> // 오타
int main() {
std::cout << "Hello\n";
}
// fatal error: iostrem: No such file or directory
include 에러는 크게 두 종류로 나뉩니다. 하나는 파일을 못 찾는 경로 문제로, 전처리기가 멈추면서 fatal error: ...: No such file or directory라는 비교적 명확한 메시지를 냅니다. 다른 하나는 파일은 찾았지만 include 순서나 구조가 꼬인 문제로, 에러 메시지가 include가 아니라 엉뚱한 타입 에러('Player' does not name a type, incomplete type)로 나타나 원인을 찾기 어렵습니다. 이 글은 두 종류를 차례로 다루고, 각각을 어떻게 구분하고 고치는지 정리합니다.
include 에러 5가지 원인
원인 1: 파일명 오타
// ❌ 오타
#include <iostrem> // iostream 아님!
// fatal error: iostrem: No such file or directory
해결: 파일명을 정확히 확인. 전처리기는 첫 번째 fatal error에서 바로 멈추므로, 이 에러는 항상 에러 목록의 맨 위에 하나만 나옵니다. IDE의 자동 완성으로 include를 입력하면 이런 오타를 대부분 피할 수 있습니다.
// ✅ 올바른 코드
#include <iostream>
원인 2: 파일이 없음
// ❌ 파일이 없음
#include "myheader.h" // myheader.h 파일이 없음
// fatal error: myheader.h: No such file or directory
해결: 파일 생성 또는 경로 확인.
# 파일 존재 확인
ls myheader.h
# 또는 find로 검색
find . -name "myheader.h"
파일이 분명히 있는데도 찾지 못한다면, 컴파일러가 실제로 어느 디렉터리를 뒤지는지 확인해 보는 것이 가장 빠릅니다. g++ -v -E main.cpp -o /dev/null을 실행하면 #include "..." search starts here:와 #include <...> search starts here: 아래에 탐색 경로 목록이 출력됩니다. 기대한 디렉터리가 목록에 없다면 원인 3, 목록에는 있는데 못 찾는다면 원인 1이나 4일 가능성이 높습니다.
원인 3: include 경로 미설정
// ❌ include 경로 없음
#include "utils/helper.h" // utils/ 디렉토리가 include 경로에 없음
// fatal error: utils/helper.h: No such file or directory
해결: include 경로 추가.
# GCC/Clang
g++ -I./src -I./include main.cpp
# CMake
include_directories(src include)
#include "utils/helper.h"처럼 하위 경로가 들어간 include는 -I로 준 디렉터리 기준으로 해석됩니다. -I./src를 주면 ./src/utils/helper.h를 찾습니다. 그래서 -I./src/utils를 주고 #include "utils/helper.h"를 쓰면 ./src/utils/utils/helper.h를 찾게 되어 실패합니다. include 문의 경로와 -I 경로를 이어 붙였을 때 실제 파일 위치가 되는지 확인하면 됩니다.
원인 4: 대소문자 불일치 (Linux/macOS)
// ❌ 대소문자 다름
#include "MyHeader.h" // 실제 파일: myheader.h
// fatal error: MyHeader.h: No such file or directory (Linux)
// Windows·macOS 기본 설정에서는 OK (대소문자 구분 안 함)
해결: 파일명 대소문자를 정확히 맞추기.
이 문제는 개발은 Windows나 macOS에서 하고 빌드 서버나 Docker 이미지는 Linux인 팀에서 흔히 생깁니다. 로컬에서는 절대 재현되지 않고 CI에서만 실패하기 때문에 “내 컴퓨터에서는 되는데”라는 대화가 오갑니다. git도 기본적으로 대소문자만 바뀐 파일 이름 변경을 감지하지 못하는 경우가 있어, 파일 이름을 myheader.h에서 MyHeader.h로 바꿀 때는 git mv myheader.h MyHeader.h로 명시적으로 옮겨야 원격 저장소에도 반영됩니다. 대소문자 구분이 없는 환경에서는 컴파일러 경고로도 잡히지 않으므로, PR마다 Linux 빌드를 돌리는 것이 가장 확실한 방어입니다.
원인 5: 순환 include
// ❌ 순환 include
// A.h
#include "B.h"
class A {
B b;
};
// B.h
#include "A.h"
class B {
A a;
};
// 컴파일 에러: 'B' does not name a type 또는 field has incomplete type
해결: 전방 선언 사용 (다음 섹션). 다만 이 예제처럼 A가 B를 값으로, B가 A를 값으로 가진다면 전방 선언으로도 풀리지 않습니다. A 안에 B가 있고 B 안에 A가 있으면 객체 크기가 무한해지기 때문입니다. 적어도 한쪽은 포인터·참조·스마트 포인터로 바꿔야 하고, 이것은 include 문제가 아니라 설계 문제입니다.
include 경로 설정
<> vs ""
// <> : 시스템 include 경로에서 검색
#include <iostream>
#include <vector>
// "" : include 문이 있는 파일의 디렉터리 → -I 경로 → 시스템 경로 순서로 검색
#include "myheader.h"
#include "utils/helper.h"
규칙:
- 표준 라이브러리:
<> - 프로젝트 헤더:
""
""의 “현재 디렉터리”는 컴파일 명령을 실행한 작업 디렉터리가 아니라 include 문을 쓴 파일이 있는 디렉터리라는 점이 헷갈리기 쉽습니다. src/main.cpp에서 #include "helper.h"를 쓰면 src/helper.h를 먼저 찾고, src/utils/a.h 안의 #include "b.h"는 src/utils/b.h를 먼저 찾습니다. 같은 이름의 헤더가 여러 디렉터리에 있으면 어느 파일이 선택될지 include하는 파일 위치에 따라 달라지므로, 프로젝트 안에서 헤더 이름을 겹치지 않게 하거나 "project/utils/helper.h"처럼 프로젝트 루트 기준 경로를 쓰는 규칙을 두면 혼란이 줄어듭니다.
GCC/Clang: -I 옵션
# 프로젝트 구조
project/
├─ src/
│ ├─ main.cpp
│ └─ utils/
│ └─ helper.h
└─ include/
└─ config.h
# include 경로 추가
g++ -I./src -I./include src/main.cpp
# main.cpp에서 사용
#include "utils/helper.h" // src/utils/helper.h
#include "config.h" // include/config.h
CMake
# CMakeLists.txt
include_directories(
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/include
)
# 또는 타겟별 설정
target_include_directories(myapp PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/include
)
현대 CMake에서는 include_directories()보다 target_include_directories()를 권장합니다. 전자는 그 디렉터리 아래의 모든 타겟에 경로를 추가해 의존 관계가 흐려지고, 후자는 타겟마다 경로를 지정하면서 PUBLIC/PRIVATE/INTERFACE로 전파 범위까지 정할 수 있습니다. 라이브러리 타겟에 PUBLIC으로 include 경로를 걸어 두면 그 라이브러리를 target_link_libraries로 링크하는 타겟이 경로를 자동으로 물려받으므로, “라이브러리는 링크했는데 헤더를 못 찾는다”는 문제가 사라집니다.
Visual Studio
프로젝트 속성 → C/C++ → 일반 → 추가 포함 디렉터리
예: $(ProjectDir)src;$(ProjectDir)include
순환 include 해결
문제 상황
// ❌ 순환 include
// Player.h
#ifndef PLAYER_H
#define PLAYER_H
#include "Weapon.h"
class Player {
Weapon weapon_; // 완전한 타입 필요
};
#endif
// Weapon.h
#ifndef WEAPON_H
#define WEAPON_H
#include "Player.h"
class Weapon {
Player* owner_; // 포인터 → 전방 선언 가능
};
#endif
// 컴파일 에러 (main.cpp가 Player.h를 먼저 include한 경우):
// Weapon.h: error: 'Player' does not name a type
이 에러가 헷갈리는 이유는 헤더 가드가 정상적으로 동작한 결과이기 때문입니다. main.cpp가 Player.h를 include하면 PLAYER_H가 정의되고, 그 안에서 Weapon.h를 include합니다. Weapon.h는 다시 Player.h를 include하지만 PLAYER_H가 이미 정의되어 있어 내용이 통째로 건너뛰어지고, 그 상태에서 class Weapon이 Player를 쓰려 하니 아직 선언되지 않은 이름이 됩니다. 그래서 어느 헤더를 먼저 include하느냐에 따라 에러가 나는 파일이 바뀌고, include 순서를 바꾸면 “고쳐진 것처럼” 보였다가 다른 곳에서 다시 터집니다.
해결법 1: 전방 선언
// ✅ Player.h
#ifndef PLAYER_H
#define PLAYER_H
class Weapon; // 전방 선언
class Player {
Weapon* weapon_; // 포인터 → 전방 선언으로 충분
void setWeapon(Weapon* w); // 선언만
};
#endif
// ✅ Player.cpp
#include "Player.h"
#include "Weapon.h" // 여기서 완전한 정의 include
void Player::setWeapon(Weapon* w) {
weapon_ = w;
w->setOwner(this); // Weapon 멤버 함수 호출 가능
}
해결법 2: 포인터/참조로 변경
// ❌ 값 저장 (완전한 타입 필요)
class Player {
Weapon weapon_; // include "Weapon.h" 필요
};
// ✅ 포인터 저장 (전방 선언 가능)
class Player {
std::unique_ptr<Weapon> weapon_; // 전방 선언으로 충분 (단, 소멸자 위치 주의)
public:
~Player(); // 선언만 하고, Weapon.h를 include한 Player.cpp에서 = default로 정의
};
unique_ptr 멤버와 전방 선언을 함께 쓸 때 가장 흔히 만나는 함정이 소멸자입니다. 소멸자를 선언하지 않으면 컴파일러가 헤더 안에서 인라인 소멸자를 만들고, 그 소멸자는 delete를 위해 Weapon의 완전한 정의를 요구합니다. 그 결과 Player.h를 include한 모든 파일에서 invalid application of 'sizeof' to incomplete type 'Weapon'(GCC) 또는 can't delete an incomplete type(MSVC 경고 C4150) 같은 메시지가 unique_ptr.h 내부를 가리키며 나옵니다. 헤더에 ~Player();를 선언하고 Player.cpp에서 Player::~Player() = default;로 정의하면, 소멸자는 Weapon의 정의가 보이는 곳에서만 만들어집니다. 이동 생성자와 이동 대입도 같은 이유로 .cpp에서 = default로 정의해야 할 수 있습니다. std::shared_ptr은 삭제자를 생성 시점에 기록하므로 이 문제가 없습니다.
전방 선언
전방 선언 가능한 경우
// ✅ 포인터
class MyClass;
MyClass* ptr;
// ✅ 참조
class MyClass;
MyClass& ref = ...;
// ✅ 함수 매개변수/반환 타입
class MyClass;
void foo(MyClass* obj);
MyClass* bar();
전방 선언 불가능한 경우
// ❌ 값 저장 (크기를 알아야 함)
class MyClass;
MyClass obj; // 컴파일 에러: incomplete type
// ❌ 멤버 함수 호출
class MyClass;
MyClass* ptr = ...;
ptr->foo(); // 컴파일 에러: incomplete type
// ❌ sizeof
class MyClass;
size_t size = sizeof(MyClass); // 컴파일 에러
// ❌ 상속
class MyClass;
class Derived : public MyClass {}; // 컴파일 에러
규칙은 한 가지로 정리됩니다. 컴파일러가 그 타입의 크기나 멤버를 알아야 하는 코드에는 완전한 정의가 필요하고, 타입의 이름만 알면 되는 코드에는 전방 선언으로 충분합니다. 포인터와 참조는 가리키는 대상과 무관하게 크기가 정해져 있으므로 이름만 알면 됩니다. 함수 선언에서 값으로 받거나 돌려주는 경우(void foo(MyClass obj);)도 선언만이라면 전방 선언으로 충분하고, 그 함수를 정의하거나 호출하는 곳에서 완전한 정의가 필요합니다.
전방 선언 패턴
// ✅ 헤더: 전방 선언 + 포인터
// Manager.h
#ifndef MANAGER_H
#define MANAGER_H
#include <memory>
class Worker; // 전방 선언
class Manager {
std::unique_ptr<Worker> worker_;
public:
void setWorker(std::unique_ptr<Worker> w);
Worker* getWorker() const;
};
#endif
// ✅ 소스: 완전한 정의 include
// Manager.cpp
#include "Manager.h"
#include "Worker.h" // 여기서 완전한 정의
void Manager::setWorker(std::unique_ptr<Worker> w) {
worker_ = std::move(w);
}
Worker* Manager::getWorker() const {
return worker_.get();
}
// ⚠️ Manager.h에 ~Manager();를 선언하고 여기서 정의해야 함 (위 "해결법 2" 참고)
// Manager::~Manager() = default;
이 패턴의 가치는 컴파일 시간에서 드러납니다. Manager.h가 Worker.h를 include하지 않으므로, Worker.h를 고쳐도 Manager.h만 include하는 수많은 파일은 다시 컴파일되지 않습니다. 대형 프로젝트에서 헤더 하나를 고쳤을 때 전체 빌드가 도는 문제는 대개 헤더가 불필요하게 다른 헤더를 include하는 데서 옵니다. 이 방식을 더 밀고 나간 것이 PIMPL 관용구이고, 어느 헤더가 불필요하게 include되는지는 include-what-you-use 도구로 찾을 수 있습니다.
헤더 가드
#ifndef 방식
// MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H
class MyClass {
// ...
};
#endif // MYCLASS_H
장점: 표준, 모든 컴파일러 지원. 단점: 타이핑 많음, 파일명 바뀌면 수동 변경.
#pragma once 방식 (권장)
// MyClass.h
#pragma once
class MyClass {
// ...
};
장점: 간결, 파일명 충돌 없음. 단점: 비표준 (하지만 모든 주요 컴파일러 지원).
비교
| 항목 | #ifndef | #pragma once |
|---|---|---|
| 표준 | 표준 | 비표준 (사실상 표준) |
| 타이핑 | 많음 | 적음 |
| 파일명 충돌 | 가능 | 없음 |
| 컴파일 속도 | 주요 컴파일러는 가드 패턴을 인식해 최적화 | 비슷함 |
권장: #pragma once (모든 주요 컴파일러 지원).
#ifndef 방식의 실제 위험은 매크로 이름 충돌입니다. 서로 다른 디렉터리의 utils.h 두 개가 모두 UTILS_H를 쓰면, 먼저 include된 쪽만 보이고 나머지는 조용히 무시되어 “분명히 include했는데 선언이 없다”는 에러가 납니다. 가드 이름에 프로젝트와 경로를 넣는(MYPROJECT_SRC_UTILS_H) 관례가 이 때문입니다. #pragma once는 파일 자체를 기준으로 하므로 이 문제가 없지만, 같은 헤더가 심볼릭 링크나 복사본으로 두 경로에 존재하면 두 파일로 인식해 두 번 include될 수 있습니다. 컴파일 속도는 GCC·Clang·MSVC 모두 전형적인 가드 패턴을 인식해 파일을 다시 열지 않으므로 차이가 거의 없습니다.
실전 사례 분석
사례 1: 외부 라이브러리 include
에러 코드:
// ❌ 에러 코드
#include <boost/asio.hpp>
// fatal error: boost/asio.hpp: No such file or directory
해결:
# 1. 라이브러리 설치
sudo apt install libboost-all-dev
# 2. /usr/include는 기본 탐색 경로라서 -I 없이 바로 됨
g++ main.cpp -pthread
# 다른 위치(예: /opt/boost)에 설치했다면 그 경로를 지정
g++ -I/opt/boost/include main.cpp -L/opt/boost/lib -lboost_system
외부 라이브러리의 No such file 에러는 대부분 개발용 패키지(헤더)가 설치되지 않은 경우입니다. Debian/Ubuntu에서 libfoo만 설치하면 실행용 .so만 들어오고 헤더는 libfoo-dev에 따로 들어 있습니다. /usr/include는 컴파일러의 기본 경로이므로 -I/usr/include를 명시할 필요가 없고, 오히려 시스템 헤더의 탐색 순서를 바꿔 표준 라이브러리 헤더의 #include_next가 깨지는 드문 문제를 일으킬 수 있습니다. Boost처럼 pkg-config 파일(.pc)을 제공하지 않는 라이브러리도 많으므로, CMake를 쓴다면 find_package(Boost REQUIRED)처럼 CMake의 탐색 기능을 쓰는 편이 이식성이 좋습니다. vcpkg나 Conan 같은 패키지 관리자를 쓰면 이런 경로 문제를 대부분 피할 수 있습니다.
사례 2: 프로젝트 내부 헤더
에러 코드:
// 프로젝트 구조
project/
├─ src/
│ ├─ main.cpp
│ └─ utils.cpp
└─ include/
└─ utils.h
// main.cpp
#include "utils.h" // ❌ include/ 디렉토리를 못 찾음
// fatal error: utils.h: No such file or directory
해결:
# include 경로 추가
g++ -I./include src/main.cpp src/utils.cpp
# 또는 상대 경로
#include "../include/utils.h" # 비권장 (경로 변경 시 깨짐)
같이 보면 좋은 글
- C++ Header Files
- C++ 전방 선언 | Forward Declaration 가이드
- C++ 컴파일 과정 | 전처리·컴파일·링크 단계
- C++ multiple definition 에러 해결
- C++ 시리즈 전체 보기
자주 묻는 질문 (FAQ)
Q. Windows에서는 빌드되던 코드가 Linux에서 No such file 에러를 내는 이유는 무엇인가요?
A. Windows와 macOS의 기본 파일 시스템은 대소문자를 구분하지 않지만 Linux는 구분하기 때문에, #include "MyHeader.h"로 쓰고 실제 파일명이 myheader.h라면 Linux에서만 헤더를 찾지 못합니다. #include "utils\helper.h"처럼 백슬래시를 경로 구분자로 쓴 경우도 같은 증상을 냅니다. 파일명과 include 문을 정확히 맞추고 경로에는 항상 /를 쓰며, CI에 Linux 빌드를 하나 두면 이런 문제를 병합 전에 잡을 수 있습니다.