본문 바로가기
Tools2026년 7월 30일6분 읽기

Makefile 제대로 쓰기 — .PHONY, 자동 변수, 증분 빌드

YS
김영삼
조회 6
Makefile 제대로 쓰기 — .PHONY, 자동 변수, 증분 빌드

Makefile은 "무엇이 무엇으로부터 만들어지는가"라는 의존 관계를 선언하면, make파일 수정 시각을 비교해 바뀐 부분만 다시 빌드해주는 도구다. C 빌드용이라는 이미지가 강하지만, 요즘은 언어 무관한 프로젝트의 명령 런처 겸 증분 빌드 엔진으로 더 많이 쓴다.

솔직히 처음엔 npm run 스크립트나 셸 스크립트면 충분하지 않냐고 생각했다. 그런데 "이미 만들어진 건 다시 안 만든다"는 증분 빌드의 감각을 한 번 맛보면 생각이 바뀐다. 그리고 그 감각을 제대로 쓰려면 .PHONY와 자동 변수를 알아야 한다.

규칙의 3요소

모든 규칙은 타깃, 선행조건, 레시피로 이뤄진다. 레시피 줄은 반드시 탭으로 들여쓴다. 스페이스로 넣으면 missing separator 에러가 나는데, 입문자가 십중팔구 한 번은 겪는다.

dist/app.js: src/index.ts src/util.ts
	esbuild src/index.ts --bundle --outfile=dist/app.js

이 규칙의 힘은 여기 있다. dist/app.js가 선행조건인 두 소스보다 새것이면 make는 "할 일 없음"이라며 넘어간다. 소스를 하나라도 건드리면 그때만 다시 빌드한다. 이게 증분 빌드의 전부다. 파일 시각 비교, 그 이상도 이하도 아니다.

.PHONY — 파일이 아닌 타깃

여기서 함정. build, test, clean처럼 실제 파일을 만들지 않는 타깃이 있다. 그런데 디렉터리에 우연히 test라는 파일이 있으면, make는 "test 파일이 최신이네" 하며 레시피를 건너뛴다. 이걸 막는 게 .PHONY 선언이다.

.PHONY: build test clean
build:
	npm run build
test:
	npm test
clean:
	rm -rf dist

.PHONY로 등록된 타깃은 make가 "파일 존재 여부와 무관하게 항상 실행"으로 취급한다. 명령 런처로 쓰는 모든 타깃에는 반드시 붙여야 한다. 나는 이걸 빼먹어서 make clean이 조용히 아무것도 안 하는 걸 30분간 노려본 적이 있다.

자동 변수 — 반복을 줄인다

규칙 안에서 타깃과 선행조건을 반복해 쓰면 지저분하다. make는 이를 대신하는 특수 변수를 준다.

변수
$@타깃 이름
$<첫 번째 선행조건
$^모든 선행조건(중복 제거)
$?타깃보다 새로운 선행조건들
CC = gcc
CFLAGS = -O2 -Wall
# 패턴 규칙: 모든 .c를 .o로
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@
app: main.o util.o
	$(CC) $^ -o $@

%.o: %.c는 패턴 규칙이다. "어떤 이름이든 .o가 필요하면 같은 이름의 .c에서 만들어라"는 일반화. 파일이 100개여도 규칙 하나로 끝난다.

언제 쓰고, 언제 안 쓰나

Makefile을 만능으로 밀 생각은 없다. 내 기준은 이렇다.

  • 쓸 만할 때: 산출물이 파일이고 증분 빌드 이득이 클 때(코드 생성, 에셋 변환, 문서 빌드). 언어가 여러 개 섞인 프로젝트의 통일된 진입점이 필요할 때. 의존성이 파일 관계로 자연스럽게 표현될 때.
  • 피할 때: 복잡한 조건 분기나 반복 로직이 많으면 make 문법이 금세 난해해진다. 그럴 땐 차라리 스크립트를 부르는 얇은 타깃으로 감싸라. 크로스플랫폼(특히 Windows) 지원이 1순위라면 make 의존은 부담이다.
개인적으로는, 로직은 셸 스크립트나 언어 도구에 두고 Makefile은 "이름 붙은 진입점 + 파일 의존 관계"만 담는 얇은 층으로 쓸 때 가장 오래 유지보수하기 좋았다.

자주 하는 실수 하나 더

레시피의 각 줄은 독립된 서브셸에서 실행된다. 그래서 이렇게 쓰면 cd가 다음 줄에 영향을 주지 않는다.

# 틀림: 두 줄이 서로 다른 셸
deploy:
	cd build
	./upload.sh   # ← build가 아니라 원래 위치에서 실행됨
# 옳음: 한 줄로 이어서
deploy:
	cd build && ./upload.sh

자주 묻는 질문

레시피에서 "missing separator" 에러가 나요.

레시피 줄을 스페이스로 들여썼기 때문입니다. make는 레시피 앞에 반드시 탭 문자를 요구합니다. 에디터에서 탭을 스페이스로 자동 변환하는 설정을 켜두면 이 에러가 잦으니, Makefile에 한해 탭을 유지하도록 예외를 두세요.

매번 전체가 다시 빌드돼요.

타깃이 실제로 파일을 만드는지 확인하세요. 명령 런처용 타깃이라면 .PHONY라서 항상 실행되는 게 정상입니다. 반대로 파일 타깃인데 매번 재빌드된다면, 선행조건 파일의 수정 시각이 타깃보다 계속 새롭거나 타깃 파일이 실제로 생성되지 않는 경우입니다.

make와 npm scripts 중 뭘 써야 하나요?

단순히 명령 몇 개를 이름으로 부르는 정도면 npm scripts로 충분합니다. 파일 산출물의 증분 빌드가 필요하거나, JS 생태계 밖의 도구까지 하나의 진입점으로 묶고 싶을 때 make가 유리합니다. 둘을 섞어 make가 npm scripts를 호출하게 해도 됩니다.

변수를 = 와 := 중 뭘로 정의하나요?

=는 사용 시점에 값을 펼치는 재귀 변수라 나중 정의가 반영되고, :=는 정의 시점에 즉시 평가되는 단순 변수입니다. 명령 치환처럼 무거운 연산은 :=로 한 번만 평가되게 하는 편이 예측 가능하고 빠릅니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록