Python 프로젝트에 Makefile 도입하기

 

Python 프로젝트를 관리하다 보면 반복적으로 실행하는 명령어들이 생긴다.

uv run ruff check .
uv run ruff format .
uv run pytest tests/

매번 타이핑하거나 히스토리를 뒤지는 건 번거롭다. 이때 Makefile을 쓰면 make lint, make test 한 줄로 해결된다.

Makefile이란

Makefile은 원래 C/C++ 프로젝트의 빌드 자동화 도구다. 소스 파일을 컴파일하고 링크하는 과정을 자동화하기 위해 만들어졌다. 하지만 본질적으로는 "이 이름으로 이 명령어를 실행해라" 를 정의하는 도구이기 때문에, Python 프로젝트의 태스크 러너로도 잘 작동한다.

make 명령은 macOS와 대부분의 Linux 배포판에 기본으로 설치되어 있다. Windows라면 WSL이나 Git Bash를 사용한다.

기본 문법

Makefile의 기본 구조는 다음과 같다.

타겟이름:
    실행할 명령어

들여쓰기는 반드시 탭(Tab)이어야 한다. 스페이스를 쓰면 에러가 난다. 에디터 설정을 확인하자.

터미널에서 make 타겟이름을 실행하면 해당 명령어가 실행된다.

Python 프로젝트용 Makefile 작성

실제로 자주 쓰는 패턴으로 작성한 예시다.

.PHONY: install lint format test clean

install:
	uv sync

lint:
	uv run ruff check .

format:
	uv run ruff format .

test:
	uv run pytest tests/ -v

clean:
	find . -type d -name __pycache__ -exec rm -rf {} +
	find . -type d -name .pytest_cache -exec rm -rf {} +
	find . -type d -name .ruff_cache -exec rm -rf {} +

이제 다음과 같이 실행할 수 있다.

make install   # 의존성 설치
make lint      # 린트 검사
make format    # 코드 포맷
make test      # 테스트 실행
make clean     # 캐시 파일 삭제

.PHONY가 뭔가

Makefile은 원래 파일 빌드 도구라서, 타겟 이름과 같은 파일이 존재하면 "이미 최신 상태"라고 판단하고 명령을 실행하지 않는다.

예를 들어 프로젝트 루트에 test라는 디렉토리가 있으면 make test가 동작하지 않을 수 있다. .PHONY에 타겟을 등록하면 파일 존재 여부와 관계없이 항상 명령을 실행한다.

Python 프로젝트에서는 대부분의 타겟이 파일 빌드와 무관하므로, 만든 타겟은 전부 .PHONY에 넣는 게 안전하다.

타겟 연결하기

타겟 이름 오른쪽에 다른 타겟을 나열하면 먼저 실행된다.

.PHONY: check

check: lint test

make check를 실행하면 lint → test 순서로 실행된다. CI에서 한 번에 검증할 때 유용하다.

변수 사용

반복되는 값은 변수로 빼둔다.

PYTHON = uv run python
TEST_DIR = tests/

.PHONY: test

test:
	$(PYTHON) -m pytest $(TEST_DIR) -v

$(변수명) 형태로 참조한다. 환경에 따라 Python 경로나 옵션이 달라질 때 한 곳만 수정하면 된다.

도움말 타겟

팀 프로젝트라면 help 타겟을 추가해두면 편하다.

.PHONY: help

help:
	@echo "사용 가능한 명령어:"
	@echo "  make install  - 의존성 설치"
	@echo "  make lint     - 린트 검사"
	@echo "  make format   - 코드 포맷"
	@echo "  make test     - 테스트 실행"
	@echo "  make clean    - 캐시 삭제"

명령어 앞에 @를 붙이면 명령어 자체를 출력하지 않고 결과만 출력한다. 붙이지 않으면 실행 전에 명령어가 그대로 출력된다.

최종 예시

.PHONY: install lint format test check clean help

PYTHON = uv run

install:
	uv sync

lint:
	$(PYTHON) ruff check .

format:
	$(PYTHON) ruff format .

test:
	$(PYTHON) pytest tests/ -v

check: lint test

clean:
	find . -type d -name __pycache__ -exec rm -rf {} +
	find . -type d -name .pytest_cache -exec rm -rf {} +
	find . -type d -name .ruff_cache -exec rm -rf {} +

help:
	@echo "사용 가능한 명령어:"
	@echo "  make install  - 의존성 설치"
	@echo "  make lint     - 린트 검사"
	@echo "  make format   - 코드 포맷"
	@echo "  make test     - 테스트 실행"
	@echo "  make check    - 린트 + 테스트"
	@echo "  make clean    - 캐시 삭제"

정리

Makefile은 설정 파일이 따로 필요 없고, make 명령어 한 줄로 복잡한 커맨드를 실행할 수 있다. pyproject.toml의 [tool.taskipy]나 justfile 같은 대안도 있지만, Makefile은 추가 도구 설치 없이 어느 환경에서도 바로 쓸 수 있다는 장점이 있다.

핵심만 정리하면:

  • 들여쓰기는
  • 파일 이름과 충돌 방지를 위해 .PHONY 등록
  • 타겟 오른쪽에 의존 타겟 나열로 순서 실행