# 최고의 예제와 함께 Makefile 배우기

> Clean Markdown view of GeekNews topic #21572. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=21572](https://news.hada.io/topic?id=21572)
- GeekNews Markdown: [https://news.hada.io/topic/21572.md](https://news.hada.io/topic/21572.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2025-06-21T10:09:11+09:00
- Updated: 2025-06-21T10:09:11+09:00
- Original source: [makefiletutorial.com](https://makefiletutorial.com/)
- Points: 6
- Comments: 1

## Summary

**의존성 관리**와 **자동화 빌드**에 최적화된 도구인 Makefile은 타임스탬프 기반 검출과 **고급 패턴 규칙, 변수 확장** 등 다양한 기능을 예제 중심으로 실용적으로 설명합니다. **실전 템플릿**과 함께 확장성 및 효율적 관리를 강조하며, GNU Make에 맞게 구체적 사용법과 구문 구조, **자동 변수 활용법**을 심도 있게 다룹니다. 영역별 예시와 대안 빌드 시스템 안내를 통해 스타트업 및 개발 조직의 프로젝트 생산성을 크게 높일 수 있음을 알립니다.

## Topic Body

- **Makefile**은 C/C++ 빌드 자동화 및 의존성 관리를 간소화하는 도구임  
- **타임스탬프를 활용한 변경 파일 검출** 방식으로, 필요한 경우에만 컴파일 작업을 실행함  
- 규칙(rule), 명령어(command), 의존성(prerequisite) 등 **핵심 구조**를 예제와 함께 설명함  
- **자동 변수, 패턴 규칙, 변수 확장** 같은 고급 기능도 실용적으로 다룸  
- 중간 규모 프로젝트용 **실전 Makefile 템플릿**을 통한 확장성과 관리의 중요성 소개함  
  
---  
  
### Makefile 튜토리얼 가이드 소개  
  
- **Makefile**은 프로젝트 빌드 자동화와 의존성 관리를 담당하는 핵심 도구임  
- 다양한 숨은 규칙과 기호로 인해 처음 접할 때 복잡하게 느낄 수 있으나, 이 가이드는 주요 내용을 간결하고 직접 실행 가능한 예제로 정리함  
- 각 섹션별로 실습 기반 예시를 통한 이해가 가능  
  
---  
  
### 시작하기  
  
#### Makefile의 존재 목적  
  
- Makefile은 대형 프로그램에서 **변경된 부분만 재컴파일**하는 데 활용됨  
- C/C++ 이외에도 여러 언어별 전용 빌드 도구가 존재하지만, Make는 일반적인 빌드 시나리오 전반에 활용됨  
- **변경된 파일을 감지해 필요한 작업만 실행**하는 로직이 핵심임  
  
#### Make의 대안 빌드 시스템  
  
- C/C++ 계열: SCons, CMake, Bazel, Ninja 등 여러 선택지가 있음  
- Java 계열: Ant, Maven, Gradle 등  
- Go, Rust, TypeScript 등도 자체 빌드 도구 제공  
- 파이썬, Ruby, JavaScript 등 **인터프리터 언어**는 컴파일이 필요 없어 Makefile과 같은 별도 관리 필요성 낮음  
  
#### Make의 버전과 종류  
  
- 다양한 Make 구현체가 있으나, 본 가이드는 **GNU Make**(주로 Linux, MacOS에서 사용)에 최적화되어 있음  
- 예제는 GNU Make 3, 4 버전에 모두 호환  
  
#### 예제 실행 방법  
  
- 터미널에서 make 설치 후, 각 예시를 `Makefile` 파일로 저장 및 `make` 명령어 실행  
- Makefile 내의 명령어 줄은 반드시 **탭 문자**로 들여쓰기 필요  
  
---  
  
### Makefile 기본 구문  
  
#### 규칙(Rule)의 구조  
  
- `타겟: 의존성(들)`  
	-	명령어  
	-	명령어  
  
- **타겟**: 빌드 결과 파일명(보통 하나)  
- **명령어**: 실제 동작하는 쉘 스크립트(탭으로 시작)  
- **의존성**: 타겟이 빌드되기 전에 반드시 준비되어야 할 파일 목록  
  
---  
  
### Make의 본질  
  
#### Hello World 예제  
  
```  
hello:  
	echo "Hello, World"  
	echo "This line will print if the file hello does not exist."  
```  
- 타겟 `hello`는 의존성이 없고, 커맨드 2개를 실행함  
- `make hello` 실행 시, 파일 `hello`가 존재하지 않으면 명령어가 실행됨. 이미 파일이 있다면 실행하지 않음  
- 일반적으로 타겟=파일명이 일치하도록 작성됨  
  
#### C 파일 컴파일 기본 예제  
  
1. `blah.c` 파일 생성(`int main() { return 0; }` 내용)  
2. 다음 Makefile 작성  
```  
blah:  
	cc blah.c -o blah  
```  
- `make` 실행 시, `blah` 타겟이 없다면 컴파일이 실행되어 `blah` 파일 생성됨  
- `blah.c` 변경 후에도 자동 재컴파일 X → 의존성 추가 필요  
  
##### 의존성 추가 방식  
  
```  
blah: blah.c  
	cc blah.c -o blah  
```  
- 이제 `blah.c`가 새로 변경됐다면, `blah` 타겟이 다시 빌드됨  
- 파일 **타임스탬프**를 변경 검출의 기준으로 사용함  
- 타임스탬프를 임의로 조작하면 의도와 다르게 작동할 수 있음  
  
---  
  
### 예제 추가  
  
#### 연결된 타겟 및 의존성 예제  
  
```  
blah: blah.o  
	cc blah.o -o blah   
  
blah.o: blah.c  
	cc -c blah.c -o blah.o   
  
blah.c:  
	echo "int main() { return 0; }" > blah.c   
```  
- 트리 구조로 의존성을 따라가며 각 단계별 생성 과정이 자동화됨  
  
#### 반드시 실행되는 타겟 예제  
  
```  
some_file: other_file  
	echo "This will always run, and runs second"  
	touch some_file  
  
other_file:  
	echo "This will always run, and runs first"  
```  
- `other_file`이 실제 파일로 생성되지 않으므로, `some_file` 명령이 매번 실행됨  
  
---  
  
### Make clean  
  
- `clean` 타겟은 빌드 산출물을 삭제하는 용도로 자주 사용됨  
- Make에서 특별한 예약어는 아니며, 직접 명령어로 정의 필요  
- 만약 파일명이 `clean`이면 혼동될 수 있으므로, `.PHONY` 사용을 권장  
  
예시:  
```  
some_file:   
	touch some_file  
  
clean:  
	rm -f some_file  
```  
  
---  
  
### 변수 처리  
  
- 변수는 항상 **문자열**.   
- 보통 `:=`을 권장하며, `=`, `?=`, `+=` 등의 다양한 대입 방식 존재  
- 사용 예시:  
```  
files := file1 file2  
some_file: $(files)  
	echo "Look at this variable: " $(files)  
	touch some_file  
  
file1:  
	touch file1  
file2:  
	touch file2  
  
clean:  
	rm -f file1 file2 some_file  
```  
  
- 변수 참조 방식: `$(variable)` 또는 `${variable}`  
- Makefile 내 따옴표는 Make 자체에서는 의미 없음(단, 쉘 명령어에서는 필요)  
  
---  
  
### 타겟 관리  
  
#### all 타겟  
  
- 여러 타겟을 한꺼번에 실행하려면, 첫 번째(디폴트) 타겟에 속성 부여  
  
```  
all: one two three  
  
one:  
	touch one  
two:  
	touch two  
three:  
	touch three  
  
clean:  
	rm -f one two three  
```  
  
#### 다중 타겟 및 자동 변수  
  
- 다수 타겟에 대해 각자 개별 명령 실행 가능. `$@`는 현재 타겟명을 가짐  
  
```  
all: f1.o f2.o  
  
f1.o f2.o:  
	echo $@  
```  
  
---  
  
### 자동 변수와 와일드카드  
  
#### * 와일드카드  
  
- `*`는 파일 시스템상 이름을 직접 탐색  
- 반드시 `wildcard` 함수로 감싸서 사용 권장  
  
```  
print: $(wildcard *.c)  
	ls -la  $?  
```  
  
- 변수 정의에서 직접 `*` 사용 금지  
```  
thing_wrong := *.o  
thing_right := $(wildcard *.o)  
```  
  
#### % 와일드카드  
  
- 주로 **패턴 규칙**에서 사용, 지정 패턴을 추출하여 확장 가능  
  
---  
  
### Fancy Rules  
  
#### 암시적(Implicit) 규칙  
  
- Make는 C/C++ 빌드와 관련된 **여러 숨은 기본 규칙**을 내장함  
- 대표 변수: `CC`, `CXX`, `CFLAGS`, `CPPFLAGS`, `LDFLAGS` 등  
- C 예제:  
```  
CC = gcc   
CFLAGS = -g   
  
blah: blah.o  
  
blah.c:  
	echo "int main() { return 0; }" > blah.c  
  
clean:  
	rm -f blah*  
```  
  
#### Static Pattern Rules  
  
- 동일한 패턴을 따르는 다수 규칙을 간결하게 작성 가능  
```  
objects = foo.o bar.o all.o  
all: $(objects)  
	$(CC) $^ -o all  
  
$(objects): %.o: %.c  
	$(CC) -c $^ -o $@  
  
all.c:  
	echo "int main() { return 0; }" > all.c  
  
%.c:  
	touch $@  
  
clean:  
	rm -f *.c *.o all  
```  
  
#### Static Pattern Rules + filter 함수  
  
- filter를 활용하면 특정 확장자 패턴에 맞는 대상만 선택 가능  
```  
obj_files = foo.result bar.o lose.o  
src_files = foo.raw bar.c lose.c  
  
all: $(obj_files)  
.PHONY: all  
  
$(filter %.o,$(obj_files)): %.o: %.c  
	echo "target: $@ prereq: $

## Comments



### Comment 40456

- Author: neo
- Created: 2025-06-21T10:09:11+09:00
- Points: 1

###### [Hacker News 의견](https://news.ycombinator.com/item?id=44325611) 
* 1985년에 Boston University Graphics 랩에서 한 사람이 Makefile을 이용해 애니메이션용 3D 렌더러를 만들던 모습을 직접 본 경험이 있음. 그 사람은 Lisp 프로그래머로, 초기 프로시저 생성 및 3D 액터 시스템을 진행 중이었고, 10줄 정도로 구성된 정말 우아한 Makefile을 만듦. 단순한 파일 날짜 의존성으로 수백 개의 애니메이션을 자동 생성하는 구조였음. 각 프레임의 3D 형태를 Lisp로 만들고, Make가 프레임을 생성하는 방식이었음. 1985년 당시 3D와 애니메이션을 당연하게 여긴 요즘과 달리 모두가 놀라워하는 상황이었고, 그가 이후 Iron Giant와 Coraline의 3D 렌더러를 담당했던 Brian Gardner라는 점 기억

  * 혹시 이 사람 [3d-consultant.com/bio.html](http://3d-consultant.com/bio.html)에 나오는 사람인지 궁금증 표현

  * Coraline이라는 영화를 말한 거 맞는지 확인

* Make를 쓸 때 잘 알려지지 않은 유용한 플래그 몇 가지 소개
    - `--output-sync=recurse -j10`: 의미는 각 타겟 작업이 끝날 때까지 stdout/stderr를 모아서 출력하는 플래그로, 그렇지 않으면 로그가 섞여서 분석이 어려움
    - 바쁜 시스템이나 다중 사용자 환경에서는 `-j` 대신 `--load-average`를 활용하여 병렬 처리 시 시스템 부하를 조절할 수 있음 (`make -j10 --load-average=10`)
    - 빌드 타겟 스케줄을 무작위로 섞는 `--shuffle` 옵션은 CI 환경에서 Makefile 내 의존성 문제를 잡아내는 데 유익

  * make의 다양한 옵션을 공식적으로 정리해 텍스트나 문서 형태로 프로그램에 포함시키면 사용 접근성이 높아진다는 아이디어 언급

  * 본인이 자주 쓰는 옵션은 전체 강제 빌드에 사용하는 `-B` 플래그 설명

  * ‘make -j’로 인해 도스 머신에서 발생한 문제를 자주 봤기 때문에 그 현상을 버그로 인식

  * 바쁜 시스템이나 다중 사용 환경에서 병렬화 문제는 OS 스케줄러가 처리해야 하는 일 아닌지 질문

  * 유용한 플래그지만 이 옵션들은 포터블하지 않기 때문에, 본인만을 위한 비공개 프로젝트 외에는 쓰지 말 것을 권장

* .PHONY를 사용하지 않는다는 이유로 튜토리얼에서 건너뛰는 건 약한 변명이란 생각. 툴을 제대로 쓰는 방법을 가르치는 게 맞다는 의견
    - 팀에서는 Make를 태스크 러너로 사용하면서 모든 레시피에 .PHONY를 추가 및 유지한 것 때문에 논쟁을 겪음
    - Clark Grubb의 Makefile 스타일 가이드([clarkgrubb.com/makefile-style-guide](https://clarkgrubb.com/makefile-style-guide)) 추천
    - .PHONY 선언을 레시피별로 하는 것과 파일 상단에 한 번에 모으는 것 사이에서 다양한 스타일 경험 공유 및 linter로 강제했으면 좋겠다는 바람

  * 읽어본 결과 괜찮은 문서지만 몇 가지 동의하지 않는 점 있음
      - -o pipefail을 맹목적으로 적용하는 건 문제이고, 파이프에서 grep 등을 쓸 때 깨질 수 있으니 상황별로 적용할 것 추천
      - 비파일 타겟에 .PHONY를 마크하는 게 엄밀하긴 하지만 거의 불필요하고 Makefile만 장황해져서 필요시만 적용이 낫다는 관점
      - 여러 개의 아웃풋 파일을 만드는 레시피는 예전에는 더미 파일을 썼지만, 최근 GNU Make 4.3부터 그룹 타겟 공식 지원([여기서 확인](https://www.gnu.org/software/make/manual/html_node/Multiple-Targets.html))이 가능

* Make는 대형 C 코드베이스의 빌드에 특화된 툴이라는 주장
    - 누군가 프로젝트별 잡 러너(업무 실행기)로 즐겨쓰는데, Make는 잡 러너로서는 적합하지 않고 조건문 같은 것도 어렵게 만드는 구조임
    - Terraform 같은 도구와 래핑하려다 실패한 경우도 본 경험

  * Make는 잡 러너라기보다 선형 쉘 스크립트를 선언형 의존성 형태로 변환하는 범용 쉘 도구라는 의견

  * C 코드베이스만의 빌드 툴로서 Make를 보는 관점은 더 이상 맞지 않다는 입장. 지난 20년간 더 견고하고 명확한 빌드 시스템이 개발된 현실을 언급. 업데이트 필요성 제기

  * 좋은 잡 러너에 대한 질문. (본인이 잡 러너 의미 혼동했다는 사과 추가)

* Makefile이 복잡해지는 부분을 현대적으로 대체해주는 툴로 [just](https://github.com/casey/just) 추천

  * just는 shell 스크립트 목록 대체에는 좋지만, ‘재실행이 필요한 룰만 실행’하는 Make의 본질적인 기능은 대체하지 못함

  * 그 밖에 대안으로
      - Task(Go) [go-task/task](https://github.com/go-task/task)
      - Cake(C#) [cake-build/cake](https://github.com/cake-build/cake)
      - Rake(Ruby) [ruby/rake](https://github.com/ruby/rake)
      - 완전히 다른 개념의 Makedown: [HN 토론](https://news.ycombinator.com/item?id=41825344)에서 논의

  * 대안 도구들은 자신을 Make 대체라고 하지만 본인은 완전히 다르고 비교 자체가 어렵다고 생각. Make의 핵심은 산출물 생성과 이미 빌드한 것의 미재빌드에 있음. 반면 just는 단순 커맨드 실행기 역할

  * Make를 명령 실행기로 쓸 때의 장점은 거의 모든 곳에 설치되어 있는 표준 도구란 안정성. 대안들이 더 잘 만들어졌어도 별도 설치 부담이 있어 굳이 쓸 필요성을 못 느낌

  * Task는 내가 C로 하는 간단한 취미 프로젝트엔 잘 쓰고 있지만, 대형 프로젝트에도 적합한진 아직 판단 어려움([Task 공식 홈페이지](https://taskfile.dev/))

* 최근 CMake가 Makefile이 C++20 모듈 지원에는 적합하지 않다고 보고 ninja를 기본으로 택했다는 점 흥미로움([CMake 가이드](https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules.7.html))
    - 실제로는 타겟 의존성을 정적으로 정의하기가 불가능에 가까워서 `clang-scan-deps` 같은 도구로 동적으로 분석하는 방식 채택([기술 슬라이드](https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang-scan-deps_Fast_dependency_scanning_for_explicit_modules.pdf))

  * 실제로 이 제약은 CMake 쪽 결정이거나 Makefile generator에 지원자가 없는 문제라 생각. ninja도 C++ 모듈 직접 지원하지 못하고([관련 이슈](https://github.com/ninja-build/ninja/issues/2457)), ninja는 오히려 Make보다 기능이 적고 모든 의존성을 정적으로 명시해야 한다는 문제 지적

  * 모듈 도입 자체가 복잡하고 혼란스럽다는 의견

* tup 사용 경험이 있는지 질문. ([공식 문서](https://gittup.org/tup/ex_dependencies.html))
    - tup은 파일 시스템 접근을 기반으로 자동으로 의존성을 파악해 어떤 컴파일러/툴에도 적용 가능한 빌드 시스템임

* 본인이 Task라는 Make 대안 툴의 창시자이자 메인테이너임을 소개. 8년 넘게 개발 중이고 계속 발전함
    - 새로운 경험을 원한다면 한 번 써 보길 권유, 궁금한 건 언제든 질문 환영
    - [Task 공식 홈페이지](https://taskfile.dev/), [GitHub 저장소](https://github.com/go-task/task) 링크

  * just 역시 또 다른 Make 대안으로 추천([just GitHub](https://github.com/casey/just))

  * 재미있는 우연으로, 본인은 Task를 자주 쓰고 오늘 아침에도 [이슈 올림](https://github.com/go-task/task/issues/2303)

* 이 튜토리얼에는 위험하고 미묘한 문제가 있음
    - MAKEFLAGS에서 옵션 파싱 시, 긴 옵션이나 빈 짧은 옵션 다루려면 다음처럼 해야 함  
      `ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))`
    - OS X 기본 제공 구버전 make 호환이 필요하다면 상당수 기능이 미비하거나 미묘하게 다름
    - 이외의 문제는 대부분 오타나 최선의 스타일 위반이므로 생략
    - 참고로 load는 guile보다 포터블하며, 크로스 컴파일 환경에서는 컴파일러 지정을 정확히 해야 함
    - Paul’s Rules of Makefiles([여기](https://make.mad-scientist.net/papers/rules-of-makefiles/))와 GNU make 매뉴얼([여기](https://www.gnu.org/software/make/manual/)), 관련 매뉴얼을 꼭 읽어볼 것 추천
    - 간단한 데모용 Makefile 프로젝트도 운영 중([데모 github](https://github.com/o11c/makefile-demo))

* 각 GitHub 레포에 항상 Makefile을 포함하는 습관
    - 매번 명령어를 까먹기 쉬워서 Makefile로 저장하면 손쉽게 복잡한 Step도 추가해둘 수 있고, `make`만 돌리면 따로 기억하지 않아도 프로젝트별 기대하는 동작 바로 실행 가능
