<aside> 🎯
한 줄 핵심 — 일하는 건 언제나 컴파일러(gcc) 하나뿐이다. make는 그 gcc를 대신 쳐주는 손, CMake는 그 손이 읽을 대본을 대신 써주는 작가.
셋은 대체재가 아니라 위아래로 쌓인 층이고, 실무에서 시간을 잡아먹는 건 맨 위(CMake)가 아니라 맨 아래의 링크와 툴체인이다.
</aside>
| 층 | 하는 일 | 리눅스 | 윈도우(VS) 대응물 |
|---|---|---|---|
| ① 빌드 생성기 | 레시피를 만들어 준다. 직접 컴파일은 안 함 | CMake (CMakeLists.txt) |
없음 — 사람이 GUI로 프로젝트 설정 |
| ② 레시피 | "무엇이 무엇에 의존하고, 바뀌면 무슨 명령을 실행하라" | makefile |
.vcxproj (XML) |
| ③ 빌드 실행기 | 레시피를 읽고 바뀐 것만 골라 명령 실행 | make |
MSBuild |
| ④ 컴파일러 · 링커 | 실제로 소스를 기계어로 바꾸고 하나로 묶음 | gcc / g++ , ld |
cl.exe , link.exe |
<aside> 💡
VS만 써 왔다면 이 층이 안 보이는 게 정상. F5 한 번에 ②③④가 전부 들어 있었을 뿐이다.
리눅스는 같은 걸 분해해서 손으로 다루는 것이지, 새로운 개념이 아니다.
</aside>
<aside> ⚠️
자주 하는 오해 — "gcc로 빌드하냐, CMake로 빌드하냐"는 질문 자체가 성립하지 않는다.
CMake를 써도 마지막엔 gcc가 돈다. CMake가 있고 없고는 ①층의 유무일 뿐이다.
</aside>
이걸 모르면 위의 도구들이 왜 필요한지 안 와닿는다. .cpp 하나가 실행파일이 되는 길은 네 단계다.
main.cpp
│ ① 전처리 (Preprocess) #include 를 펼치고 #define 을 치환 → main.i
│ ② 컴파일 (Compile) C++ 문법을 어셈블리로 → main.s
│ ③ 어셈블 (Assemble) 어셈블리를 기계어 목적파일로 → main.o
│ ④ 링크 (Link) 여러 .o 와 라이브러리를 하나로 묶음 → 실행파일
| 단계 | 손으로 해보는 법 | 여기서 나는 에러 |
|---|---|---|
| ① 전처리 | g++ -E main.cpp -o main.i |
헤더를 못 찾음 — No such file (해결: -I 로 경로 추가) |
| ②③ 컴파일 | g++ -c main.cpp → main.o |
문법·타입 에러. 한 파일 안에서 판정 가능한 것들 |
| ④ 링크 | g++ main.o util.o -o app |
undefined reference — 선언은 봤는데 몸통이 어디에도 없음 (해결: -l 로 라이브러리 추가, 또는 그 .o 를 빼먹음) |
<aside> 🔑
첫 갈림길은 "컴파일 에러냐 링크 에러냐"다.
error: 로 파일·줄번호가 찍히면 ②③ — 그 파일만 보면 된다.
undefined reference to ... 가 뜨면 ④ — 소스는 멀쩡하고 빌드 구성이 틀린 것이다. 이걸 구분 못 하면 엉뚱한 파일을 몇 시간 들여다본다.
</aside>
<aside> 📐
왜 굳이 .o 로 나누나 — 파일 100개짜리 프로젝트에서 한 줄 고쳤을 때 그 파일 하나만 다시 컴파일하고 나머지 99개의 .o 는 재활용하기 위해서다.
이 "바뀐 것만 다시" 를 자동으로 판정해주는 게 다음 장의 make 다.
</aside>
makefile 은 의존 관계와 명령을 나열한 텍스트 파일이다. 문법은 딱 세 줄짜리 구조다.
CXX = g++
CXXFLAGS = -O2 -g -Wall -I./include
OBJS = main.o util.o session.o
# 목표파일 : 이것들에 의존한다
app: $(OBJS)
$(CXX) -o $@ $(OBJS) -lpthread # 링크 (④)
# .cpp 하나를 .o 하나로 바꾸는 일반 규칙
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@ # 컴파일 (②③)
clean:
rm -f $(OBJS) app
| 기호 | 뜻 |
|---|---|
목표: 의존들 |
"목표를 만들려면 이것들이 먼저 필요하다" |
$@ |
목표 이름 (위 예에서 app 또는 main.o) |
$< |
첫 번째 의존 파일 (위 예에서 main.cpp) |
% |
와일드카드. %.o: %.cpp = 모든 cpp를 같은 방식으로 처리 |
| 명령 줄 들여쓰기 | 반드시 탭. 스페이스면 에러 — makefile 입문자의 1번 함정 |
<aside> 💡
make의 존재 이유는 한 문장 — 목표파일과 의존파일의 수정 시각을 비교해서, 의존이 더 최신일 때만 명령을 실행한다.
그래서 아무것도 안 고치고 make 를 또 치면 make: 'app' is up to date. 만 뜬다. 이게 증분 빌드다.
</aside>
<aside> 🧭
실무에서 makefile을 건드리는 순간은 딱 정해져 있다 — ① 소스 파일을 새로 추가했을 때(목록에 등록) ② 컴파일 옵션·매크로를 추가할 때 ③ 라이브러리를 새로 링크할 때.
처음부터 쓸 일은 거의 없고, 읽고 고칠 줄만 알면 된다.
</aside>