본문으로 건너뛰기 C++ Makefile 완벽 가이드 | 10분만에 배우는 빌드 자동화 (실전 예제)

C++ Makefile 완벽 가이드 | 10분만에 배우는 빌드 자동화 (실전 예제)

C++ Makefile 완벽 가이드 | 10분만에 배우는 빌드 자동화 (실전 예제)

이 글의 핵심

C++ Makefile의 C++, Makefile, Make, 1.

들어가며

Makefile은 Make가 사용하는 빌드 자동화 스크립트입니다. 컴파일과 링킹을 자동화하여 개발 생산성을 높입니다. 증분 빌드·병렬(make -j)은 CMake가 생성하는 Makefile/Ninja와도 같은 맥락입니다. 언어 수준에서는 Rust Cargo·npm 스크립트·Go 빌드가 각각 다른 철학으로 캐시·병렬을 다룹니다. 전체 스택 비교는 C++ 빌드 시스템 완전 비교를 보세요.

1. Makefile 기본

가장 간단한 Makefile

# Makefile
myapp: main.cpp
	g++ main.cpp -o myapp
clean:
	rm -f myapp
# 빌드
make
# 정리
make clean

출력:

g++ main.cpp -o myapp

기본 문법

# 타겟: 의존성
#	명령어 (반드시 탭으로 시작!)
target: dependencies
	command
# 예시
main.o: main.cpp
	g++ -c main.cpp -o main.o

구성 요소

  • 타겟(target): 생성할 파일 또는 작업 이름
  • 의존성(dependencies): 타겟을 만들기 위해 필요한 파일들
  • 명령어(command): 실행할 쉘 명령어 (반드시 탭으로 시작)

2. 변수 사용

기본 변수

# 변수 정의
CXX = g++
CXXFLAGS = -std=c++17 -Wall -O2
INCLUDES = -I./include
LIBS = -lpthread -lm
# 변수 사용
myapp: main.cpp
	$(CXX) $(CXXFLAGS) $(INCLUDES) main.cpp -o myapp $(LIBS)
clean:
	rm -f myapp
.PHONY: clean

자동 변수

CXX = g++
CXXFLAGS = -std=c++17 -Wall
# 자동 변수
# $@: 타겟 이름
# $<: 첫 번째 의존성
# $^: 모든 의존성
myapp: main.o util.o
	$(CXX) $^ -o $@
	# $^ = main.o util.o
	# $@ = myapp
%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@
	# $< = main.cpp (첫 번째 의존성)
	# $@ = main.o (타겟)

자동 변수 정리

변수의미예시
$@타겟 이름myapp
$<첫 번째 의존성main.cpp
$^모든 의존성main.o util.o
$?타겟보다 새로운 의존성main.o

3. 실전 예제

예제 1: 간단한 프로젝트

# 컴파일러 설정
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
# 타겟
TARGET = myapp
# 빌드
$(TARGET): main.cpp
	$(CXX) $(CXXFLAGS) main.cpp -o $(TARGET)
# 실행
run: $(TARGET)
	./$(TARGET)
# 정리
clean:
	rm -f $(TARGET)
# 파일이 아닌 타겟
.PHONY: clean run

사용법:

make           # 빌드
make run       # 빌드 후 실행
make clean     # 정리

예제 2: 여러 파일 프로젝트

CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
TARGET = myapp
# 오브젝트 파일
OBJS = main.o calculator.o utils.o
# 링킹
$(TARGET): $(OBJS)
	$(CXX) $(OBJS) -o $(TARGET)
# 컴파일
main.o: main.cpp calculator.h utils.h
	$(CXX) $(CXXFLAGS) -c main.cpp
calculator.o: calculator.cpp calculator.h
	$(CXX) $(CXXFLAGS) -c calculator.cpp
utils.o: utils.cpp utils.h
	$(CXX) $(CXXFLAGS) -c utils.cpp
# 정리
clean:
	rm -f $(OBJS) $(TARGET)
.PHONY: clean

예제 3: 패턴 규칙 (자동화)

CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
TARGET = myapp
# 모든 .cpp 파일 찾기
SRCS = $(wildcard *.cpp)
OBJS = $(SRCS:.cpp=.o)
# 링킹
$(TARGET): $(OBJS)
	$(CXX) $^ -o $@
# 패턴 규칙: 모든 .cpp → .o
%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
	rm -f $(OBJS) $(TARGET)
.PHONY: clean

패턴 규칙 설명

  • %.o: %.cpp: 모든 .cpp 파일을 .o로 컴파일
  • $<: 첫 번째 의존성 (.cpp 파일)
  • $@: 타겟 (.o 파일)

예제 4: 라이브러리 링크

CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
INCLUDES = -I./include
LDFLAGS = -lpthread -lm
TARGET = myapp
SRCS = $(wildcard src/*.cpp)
OBJS = $(SRCS:src/%.cpp=obj/%.o)
# 링킹
$(TARGET): $(OBJS)
	$(CXX) $^ -o $@ $(LDFLAGS)
# 컴파일
obj/%.o: src/%.cpp
	@mkdir -p obj
	$(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@
clean:
	rm -rf obj $(TARGET)
.PHONY: clean

4. 고급 기능

조건부 컴파일

CXX = g++
TARGET = myapp
# DEBUG 변수에 따라 플래그 변경
ifeq ($(DEBUG),1)
    CXXFLAGS = -std=c++17 -Wall -g -DDEBUG
else
    CXXFLAGS = -std=c++17 -Wall -O2 -DNDEBUG
endif
$(TARGET): main.cpp
	$(CXX) $(CXXFLAGS) main.cpp -o $(TARGET)
clean:
	rm -f $(TARGET)
.PHONY: clean

사용법:

make              # Release 빌드
make DEBUG=1      # Debug 빌드

함수 사용

CXX = g++
CXXFLAGS = -std=c++17 -Wall
# wildcard: 파일 패턴 매칭
SRCS = $(wildcard src/*.cpp)
# patsubst: 패턴 치환
OBJS = $(patsubst src/%.cpp,obj/%.o,$(SRCS))
# shell: 쉘 명령 실행
$(shell mkdir -p obj)
myapp: $(OBJS)
	$(CXX) $^ -o $@
obj/%.o: src/%.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
	rm -rf obj myapp
.PHONY: clean

의존성 자동 생성

CXX = g++
CXXFLAGS = -std=c++17 -Wall
SRCS = $(wildcard *.cpp)
OBJS = $(SRCS:.cpp=.o)
DEPS = $(OBJS:.o=.d)
myapp: $(OBJS)
	$(CXX) $^ -o $@
# -MMD: 의존성 파일 생성
%.o: %.cpp
	$(CXX) $(CXXFLAGS) -MMD -c $< -o $@
# 의존성 파일 포함
-include $(DEPS)
clean:
	rm -f $(OBJS) $(DEPS) myapp
.PHONY: clean

설명

  • -MMD: 헤더 의존성을 .d 파일로 생성
  • -include: 의존성 파일을 포함 (없어도 에러 안남)
  • 헤더 파일 변경 시 자동으로 재컴파일

5. 자주 발생하는 문제

문제 1: 탭 vs 스페이스

# ❌ 스페이스 사용 (에러!)
target:
    command
# ✅ 탭 사용
target:
	command

에러 메시지:

Makefile:2: *** missing separator. Stop.

해결 방법

  • 에디터 설정에서 탭을 스페이스로 변환하지 않도록 설정
  • Makefile 모드 사용 (자동으로 탭 삽입)

문제 2: 의존성 누락

# ❌ 헤더 의존성 없음
main.o: main.cpp
	g++ -c main.cpp
# util.h가 변경되어도 재컴파일 안 됨!
# ✅ 헤더 포함
main.o: main.cpp util.h config.h
	g++ -c main.cpp
# util.h나 config.h 변경 시 자동 재컴파일

문제 3: .PHONY 누락

# ❌ clean이라는 파일이 있으면 실행 안 됨
clean:
	rm -f *.o
# ✅ .PHONY 사용
.PHONY: clean
clean:
	rm -f *.o
# clean 파일이 있어도 항상 실행됨

문제 4: 병렬 빌드

# 순차 빌드 (느림)
make
# 병렬 빌드 (4개 작업 동시)
make -j4
# CPU 코어 수만큼 병렬 빌드
make -j$(nproc)

실전 팁

  • 병렬 빌드로 컴파일 시간 단축
  • 의존성이 올바르게 설정되어야 병렬 빌드 가능

6. 실전 예제: 완전한 프로젝트

프로젝트 구조

project/
├── Makefile
├── include/
│   ├── calculator.h
│   └── utils.h
├── src/
│   ├── main.cpp
│   ├── calculator.cpp
│   └── utils.cpp
└── obj/
    └── (빌드 시 생성)

완전한 Makefile

# 컴파일러 설정
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra
INCLUDES = -I./include
LDFLAGS = -lpthread
# 디렉토리
SRC_DIR = src
OBJ_DIR = obj
INC_DIR = include
# 파일
SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJS = $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)
# 타겟
TARGET = myapp
# 디버그 빌드
ifeq ($(DEBUG),1)
    CXXFLAGS += -g -DDEBUG
else
    CXXFLAGS += -O2 -DNDEBUG
endif
# 기본 타겟
all: $(TARGET)
# 링킹
$(TARGET): $(OBJS)
	$(CXX) $^ -o $@ $(LDFLAGS)
	@echo "빌드 완료: $(TARGET)"
# 컴파일 (의존성 자동 생성)
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
	@mkdir -p $(OBJ_DIR)
	$(CXX) $(CXXFLAGS) $(INCLUDES) -MMD -c $< -o $@
# 의존성 파일 포함
-include $(DEPS)
# 실행
run: $(TARGET)
	./$(TARGET)
# 정리
clean:
	rm -rf $(OBJ_DIR) $(TARGET)
# 전체 재빌드
rebuild: clean all
# 도움말
help:
	@echo "사용 가능한 타겟:"
	@echo "  make          - Release 빌드"
	@echo "  make DEBUG=1  - Debug 빌드"
	@echo "  make run      - 빌드 후 실행"
	@echo "  make clean    - 정리"
	@echo "  make rebuild  - 전체 재빌드"
	@echo "  make -j4      - 병렬 빌드 (4개)"
.PHONY: all run clean rebuild help

사용 예시:

make              # Release 빌드
make DEBUG=1      # Debug 빌드
make -j4          # 병렬 빌드
make run          # 실행
make clean        # 정리
make rebuild      # 재빌드
make help         # 도움말

정리

핵심 요약

  1. 기본 문법: 타겟, 의존성, 명령어
  2. 변수: CXX, CXXFLAGS, 자동 변수 ($@, $<, $^)
  3. 패턴 규칙: %.o: %.cpp로 자동화
  4. 함수: wildcard, patsubst로 파일 처리
  5. 의존성 자동 생성: -MMD로 헤더 의존성 관리

Makefile vs CMake

특징MakefileCMake
복잡도낮음높음
크로스 플랫폼제한적우수
학습 곡선완만가파름
직접 제어높음낮음
적합한 프로젝트작은 프로젝트큰 프로젝트

실전 팁

  1. 효율적인 빌드
    • 병렬 빌드 사용 (make -j4)
    • 의존성 자동 생성 (-MMD)
    • ccache로 컴파일 캐싱
  2. 유지보수
    • 변수로 설정 중앙화
    • .PHONY로 타겟 명확히
    • 주석으로 복잡한 규칙 설명
  3. 디버깅
    • make -n: 명령어만 출력 (실행 안함)
    • make -d: 디버그 정보 출력
    • @echo 변수 값 확인

Make 명령어

명령어설명
make기본 타겟 빌드
make cleanclean 타겟 실행
make -j44개 작업 병렬 빌드
make -n명령어만 출력 (dry-run)
make -B모든 타겟 강제 재빌드

다음 단계


관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ Makefile의 모든 것: 기본 문법부터 변수, 패턴 규칙, 자동 의존성 관리까지. 증분 빌드와 병렬 컴파일(-j) 실전 예제로 빌드 자동화를 완벽하게 마스터하세요. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, makefile, make, build, 빌드 등으로 검색하시면 이 글이 도움이 됩니다.