IT/작업기록

black은 왜 훅에 없을까 — 포매터와 검증 도구는 다르다

aram 2026. 8. 31. 16:56

발단

개발 중인 project 디버깅 결과를 리뷰하다가 조치사항에 이런 항목이 있었다.

  1. 컨테이너에서 black 실행 — 이번 수정분 스타일 정리 + 2-space 들여쓰기 자동 교정
  2. 컨테이너에서 pytest 실행 — 정적 리뷰만 하고 직접 못 돌렸음
  3. ...
  4. python manage.py check 실행

pytest, mypy는 이미 pre-receive 훅에 걸려 있어서 push할 때마다 자동으로 도는데, black은 없었다. "왜 하나만 빠졌지?"에서 시작한 확인이었다.

black과 pytest/mypy는 애초에 카테고리가 다르다

겹치는 도구가 아니라 서로 다른 역할을 나눠 맡는다.

  • black — 코드 포매터. 들여쓰기, 따옴표, 줄바꿈, 공백 같은 스타일만 자동으로 고쳐준다. 테스트도 안 하고 타입도 안 본다.
  • pytest — 테스트 러너. 코드가 실제로 의도한 대로 동작하는지 검증한다.
  • mypy — 정적 타입 체커. 타입 annotation이 일관되고 타입 에러가 없는지 검증한다.

즉 black은 스타일 교정, pytest/mypy는 정확성/동작 검증이라는 별개 축이다. 그래서 pre-receive 훅에 pytest/mypy만 있고 black이 빠진 게 이상한 구성은 아니다. 오히려 흔한 패턴이다: black은 에디터에서 저장할 때(format on save) 자동으로 돌게 해두고, 훅에는 "동작을 깨뜨리는지"를 확인하는 pytest/mypy만 남겨두는 식.

VS Code에 black을 line-length 100으로 물려두고 저장 시점에 처리해왔기 때문에, 굳이 훅에도 넣을 필요가 없다고 판단했던 것 같다. 그런데 이번엔 컨테이너 안에서(에디터 저장 경로를 안 거치고) 수정이 이뤄지면서 black이 자동으로 안 걸렸고, 그 결과가 조치사항 1번으로 남았다.

실제로 돌려보니 벌어진 일

컨테이너에서 black을 그냥 옵션 없이 돌렸더니:

84 files would be reformatted, 143 files would be left unchanged.

숫자가 너무 많아서 의심했는데, 원인은 간단했다 — 프로젝트에 pyproject.toml이나 setup.cfg 같은 black 설정 파일이 없었다. 옵션 없이 돌리면 기본값인 line-length 88이 적용되는데, 이 프로젝트 컨벤션(CLAUDE.md에 명시)은 line-length 100. 88자 기준이 100자 기준보다 훨씬 많이 걸리는 게 당연했다.

옵션을 정확히 넣고 다시 돌리자:

black apps/ config/ --line-length 100 --exclude "/migrations/"
51 files reformatted, 77 files left unchanged.

여전히 적지 않은 숫자지만, 원인이 명확해지니 납득이 됐다. (참고: 위 명령은 --check 없이 돌린 것이라 실제로 파일이 수정된다. --check(또는 --check --diff)를 붙이면 "would be reformatted"라는 문구와 함께 미리보기만 하고 파일은 건드리지 않는다.)

훅에 넣을까, 계속 수동으로 할까

black --check --line-length 100을 pytest/mypy와 같은 자리(pre-receive)에 추가하는 건 어렵지 않다. 다만:

  • 훅에 넣으면 강제된다. 포맷 안 맞는 커밋은 push 자체가 거부된다.
  • --check는 거부만 할 뿐 자동으로 고쳐주지는 않는다. 서버측 훅이 파일을 수정해서 다시 커밋해줄 수는 없기 때문. 그러니 훅에 넣어도 "까먹고 안 돌렸을 때 push가 막혀서 그제서야 알아차리는" 안전망 역할일 뿐, 커밋 전에 수동으로 black을 돌리는 습관 자체는 여전히 필요하다.
  • 이 프로젝트는 이미 클라이언트측 pre-push 훅을 없애고 서버측 pre-receive로 통일한 이력이 있어서, 넣는다면 기존 pre-receive에 넣는 게 일관성 있는 방향.

결론: 일단은 훅에 넣지 않고, 커밋 전 컨테이너에서 아래 명령을 습관처럼 돌리기로 했다.

black apps/ config/ --line-length 100 --exclude "/migrations/"

pytest/mypy는 훅이 강제로 잡아주니 신경 쓸 필요가 없고, black만 기억하면 된다.

정리

  • black ≠ pytest/mypy. 스타일 교정 vs 정확성 검증, 겹치지 않는 별개 도구.
  • 훅에 없다고 이상한 구성이 아니다 — 에디터의 format-on-save가 담당하던 영역이었을 뿐.
  • 에디터 저장 경로를 안 거친 변경(컨테이너 내 직접 작업 등)은 black이 자동으로 안 걸린다는 걸 이번에 몸으로 배웠다.
  • black 돌릴 때는 프로젝트 컨벤션(line-length, exclude 경로)을 반드시 명시할 것 — 기본값(88)과 프로젝트 값(100)의 차이만으로 걸리는 파일 수가 확 달라진다.