L07 — 기존 프로젝트에 최신 standarda-template 반영하기 (수정중)
standarda-template 의 공통 자산을 안전하게 골라 반영할 수 있고, 무엇을 절대 건드리면 안 되는지 — 특히 기존 Django 모델(User 등) 은 현행화하지 않는다 — 를 판단할 수 있다.1. 오늘 풀 문제 — "템플릿은 계속 자라는데, 이미 만든 프로젝트는?"
같은 설계도로 지은 집이 여러 채 있다고 하자. 설계도를 나중에 더 좋게 고쳐도, 이미 지어 사람이 사는 집은 저절로 바뀌지 않는다. 우리 standarda-template(새 프로젝트를 찍어내는 공용 골격)도 그렇다.
standarda-template 은 멈춰 있지 않다. 훅(정해진 순간에 자동으로 끼어드는 작은 검사 스크립트)이 개선되고, 스킬이 좋아지고, 코딩 규칙이 추가된다. 문제는 이거다:
cookiecutter(프로젝트를 찍어내는 도구) 템플릿 수정은 "이후 새로 생성되는 프로젝트" 에만 적용된다. 이미 만든 프로젝트엔 아무 일도 안 일어난다.
즉 golden·popax 처럼 이미 cookiecutter 로 찍어낸 프로젝트는, 그 뒤로 템플릿에 쌓인 개선을 자동으로는 절대 못 받는다. 누군가 가져와야 한다.
이 "가져오기" 를 표준 절차로 만든 스킬이 /update-from-template 이다. (사실 이 강의를 만든 golden 작업 그 자체가 스킬로 정리된 것이다.) 이름이 비슷한 짝 스킬과 방향을 헷갈리지 말 것:
/sync-template 프로젝트 → 템플릿 (내 공통 변경을 템플릿으로 거꾸로 올림)
/update-from-template 템플릿 → 프로젝트 (템플릿 최신을 내 프로젝트로 — 오늘 주제)
함정:
/sync-template은 SKILL.md 첫 줄에 "동기화는 프로젝트 → 템플릿 단방향입니다" 라고 적혀 있다. 오늘 하려는 방향이 아니다. 반드시/update-from-template을 쓴다.
그런데 스킬이 있다고 생각 없이 돌리면 안 된다. 이 스킬은 "전부 자동 복사" 가 아니다 — diff(두 파일이 어디가 다른지 비교) 를 떠서 위험도로 분류하고 → 너에게 뭘 가져올지 묻고 → 고른 것만 반영 한다. 즉 판단은 여전히 사람 몫이다. 오늘 배우는 건 그 스킬이 속으로 뭘 하는지, 그리고 네가 내려야 하는 판단(특히 절대 건드리면 안 되는 것)이다.
2. 왜 스킬이 "통째 복사" 를 안 하나 — 방향이 핵심이다
| 방향 | 스킬 | 어떻게 동작 |
|---|---|---|
| 프로젝트 → 템플릿 | /sync-template |
내 공통 변경을 골라 템플릿으로 올림 |
| 템플릿 → 프로젝트 | /update-from-template |
diff → 위험도 분류 → 확인 → 선별 반영 (통째 복사 안 함) |
/update-from-template 이 그냥 cp -r(통째 복사) 로 덮지 않는 이유는 단순하다. 기존 프로젝트는 살아 있고, 이미 자기 방향으로 자랐다. 템플릿이 같은 파일을 고쳤고 프로젝트도 같은 파일을 고쳤으면, 템플릿 버전으로 덮는 순간 프로젝트가 쌓아온 게 사라진다. 그래서 스킬조차 "전부 복사" 는 안 하고, 파일별로 판단하도록 너에게 묻는다.
핵심 사고방식 한 줄:
"템플릿이 앞서 있다" 와 "프로젝트가 앞서 있다" 는 파일마다 다르다. 일괄로 어느 쪽이 최신이라고 단정하지 말 것.
3. 먼저 diff 로 현황부터 — 추측하지 말고 본다
템플릿 실제 경로(cookiecutter 렌더 디렉토리):
/home/ubuntu/standarda-template/{{cookiecutter.project_slug}}/src/
⚠️ 폴더 이름이 진짜로
{{cookiecutter.project_slug}}다(중괄호 포함).golden/src같은 게 아니다. 셸에서 따옴표로 감싸야 한다.
비교가 생각보다 쉬운 이유: cookiecutter.json 의 _copy_without_render 에 공통 앱·utils·docs·static·.claude 가 들어 있다. 이 디렉토리들은 cookiecutter 변수 치환 없이 그대로 복사되므로, 프로젝트 파일과 1:1 diff 가 된다({{ cookiecutter.xxx }} 노이즈 없음).
실제로 golden 에서 돌린 비교:
T="/home/ubuntu/standarda-template/{{cookiecutter.project_slug}}/src"
G="/home/ubuntu/golden/src"
# 디렉토리 단위
diff -rq "$T/.claude" "$G/.claude"
diff -rq "$T/accounts" "$G/accounts"
# 단일 파일
diff "$T/.env.example" "$G/.env.example"
diff -rq 는 "어떤 파일이 다른지" 만 빠르게 보여주고, 따옴표 없는 diff 는 "어떻게 다른지" 줄 단위로 보여준다. 먼저 어디가 다른지 빠짐없이 훑은 다음 판단으로 넘어간다.
/update-from-template을 돌리면 이 diff 훑기를 스킬이 대신 해서 등급별 표로 정리해 보여준다. 그래도 위 명령을 손으로 한 번 떠보는 건 가치가 있다 — 스킬이 무엇을 근거로 분류했는지 네가 읽을 수 있어야 "반영/보류" 를 제대로 판단한다.
4. 위험도 4단계로 분류 — 이게 오늘의 핵심 도구
diff 결과를 위험도 로 나눈다. 그래야 "뭘 가져오고 뭘 두는지" 가 명확해진다.
| 등급 | 무엇 | 왜 그 등급 | 기본 방침 |
|---|---|---|---|
| A. 워크플로우/도구 | .claude/(hooks·settings·skills), .githooks/, docs/structures 류 |
코드가 실제로 돌 때는 영향 없음. 개발 편의·강제장치일 뿐 | 적극 반영 |
| B. 환경/설정 파일 | .env.example, .env.production.example, .gitignore |
안전하지만 프로젝트가 더 최신일 때가 많음 | diff 보고 판단 (덮지 말 것) |
| C. 공통 규칙 문서 | CLAUDE.md 의 공통 규칙 섹션 |
프로젝트 전용 내용과 섞여 있음 | 공통 규칙만 손으로 병합 |
| D. 공통 Django 앱 | accounts profiles emails sms projects 의 모델·마이그레이션(DB 구조를 바꾼 기록) |
DB 스키마·기존 데이터에 직결 | ❌ 일괄 반영 금지 (다음 절) |
A 부터 D 로 갈수록 "건드렸을 때 터지는 범위" 가 커진다. A 는 잘못돼도 개발 도구가 좀 이상해질 뿐이지만, D 는 Production DB 와 마이그레이션 히스토리 가 걸려 있다.
5. ⭐ D — 기존 프로젝트의 Django 모델은 현행화하지 않는다
오늘 가장 중요한 규칙. 한 문장으로:
이미 돌아가는 프로젝트의 Django 모델(특히
accounts의 User 모델)은 템플릿이 앞서 있어도 그걸로 현행화하지 않는다.
golden 에서 실제로 본 차이. 템플릿 쪽 User 모델(accounts/models.py)이 golden 보다 앞서 있었다 — 필드가 더 있었다:
# 템플릿 accounts/models.py 에는 있고, golden 에는 없던 것
import uuid
PLAN_CHOICES = (('free', 'Free'), ('pro', 'Pro'), ('enterprise', 'Enterprise'))
...
company_name = models.CharField(max_length=255, blank=True, default='')
plan = models.CharField(max_length=20, choices=PLAN_CHOICES, default='free')
license_key = models.CharField(max_length=255, unique=True, blank=True, null=True)
"템플릿이 최신이네? 그럼 가져와야지" 가 틀린 직관이다. 이유는 셋:
- 마이그레이션이 갈라진다. golden 엔 자기만의
accounts/migrations/0001_initial.py가 이미 있고, 그 위에서 Production DB 가 만들어졌다. 모델 파일만 덮으면 모델과 DB 가 어긋나고,makemigrations가 golden 컨텍스트에 안 맞는 새 마이그레이션 을 만든다. Production DB 에plan·license_key컬럼을 강제로 추가하는 꼴. - 프로젝트 커스텀이 사라진다. golden 의 User 모델엔 golden 사정에 맞게 들어간/빠진 필드가 있다. 템플릿으로 덮으면 그게 날아간다.
- 그 필드가 필요한지는 프로젝트가 결정할 일이다.
plan/license_key는 과금 모델이 있는 프로젝트용이다. golden 이 그걸 원하는지는 기능 결정이지, "템플릿 동기화" 로 슬쩍 들어올 게 아니다.
그래서 golden 작업에서 D 는 통째로 제외했다. 모델은 한 줄도 안 건드렸다.
판단 기준: 그 파일을 바꿨을 때
makemigrations가 새 마이그레이션을 만들 가능성이 있나? → 있으면 D 다. 자동 반영 대상 아님. 정말 그 필드/모델이 필요하면, 그건 "템플릿 동기화" 가 아니라 별도 기능 작업으로/develop태우고 마이그레이션·데이터 백필(기존 데이터를 새 칸에 채워넣기)까지 설계해서 한다.
같은 논리가 모델뿐 아니라 admin·serializer·view 가 프로젝트에서 커스텀된 경우에도 적용된다. 공통 앱이라고 다 같지 않다 — 프로젝트가 그 위에서 자랐으면 그건 이제 그 프로젝트 코드다.
6. 실제로 golden 에 반영한 것 (A + C) — 그리고 B 는 왜 안 했나
아래는
/update-from-template이 아직 없던 시점에 golden 에서 손으로 돌린 결과다. 이 한 번의 작업이 그대로/update-from-template스킬로 정리됐다 — 즉 지금 스킬을 돌리면 속에서 똑같이 이 판단을 거친다.
위험도 분류대로 고른 결과, golden 엔 A 와 C 만 반영했다 (커밋 4b7561c).
A. 워크플로우/도구 — 적극 반영
- git hook 이전: .githooks/{commit-msg, pre-commit, pre-push} 도입 + git config core.hooksPath .githooks. 커밋 규칙 강제를 Claude 쪽 훅이 아니라 진짜 git hook 으로 옮긴 것 (자세한 건 M3 hooks 강의).
- 신규 강제 훅: except 블록에서 logger.error 대신 logger.exception 을 쓰게 막는 check-except-logger-exception.sh + check_except_logger.py(AST 기반 — 코드 구조를 뜯어 검사).
- 스킬 최신화: dev(하드코딩 IP → server.conf 조회, 안전한 포트기반 종료), release(huey 컨슈머 재시작 단계), sync-template(깨진 경로 정정).
C. 공통 규칙 — 손으로 병합
- CLAUDE.md 의 ### Error Handling 에 logger.exception 규칙 한 줄 추가. CLAUDE.md 전체를 덮지 않았다 — 프로젝트 전용 내용(리포트 크롤러, standarda-core 미채택 예외 등)이 가득해서, 공통 규칙 줄만 골라 끼웠다.
B. 환경/설정 — 안 했다. 왜냐면 golden 이 더 최신이었으니까.
이게 §2 에서 말한 "방향은 파일마다 다르다" 의 실제 예다. diff 를 떠보니:
| 파일 | 누가 앞섰나 | 근거 |
|---|---|---|
.env.example |
golden | golden-specific AWS/크롤러 섹션이 더 있음 |
.gitignore |
golden | credentials/*+README 보존, downloads/·*.log 무시가 더 있음 |
여기서 템플릿으로 덮었으면 개선이 아니라 후퇴 였다. 그래서 B 는 손대지 않았다. — "최신 템플릿 반영" 이 항상 "템플릿으로 덮기" 가 아니다.
7. 흔한 함정 두 개 (실제로 golden 에서 겪음)
① core.hooksPath 는 clone 에 안 따라온다.
.githooks/ 파일은 git 에 추적되지만, "그 폴더를 hook 으로 쓰라" 는 설정(core.hooksPath)은 로컬 git config 라 clone 하면 안 따라온다. 새 환경에서 repo 를 받으면 훅이 조용히 비활성 된다. 한 번 켜줘야 한다:
cd src && git config core.hooksPath .githooks
② commit-msg 훅의 키워드 오탐(엉뚱하게 걸림).
새로 들어온 commit-msg 훅은 메시지에 (수정|fix|버그|bug|crash|오류|에러) 가 있으면 docs/issues/ 문서를 강제한다. golden 첫 커밋이 막혔는데, 진짜 버그 수정이 아니라 커밋 메시지에 적은 파일명 check-bugfix-issue-doc.sh 의 bug 글자 때문이었다. 훅은 정상 작동한 것 — 메시지 문구를 바꿔 자연 통과시켰다(불가피하면 git commit --no-verify).
오늘 정리 + 다음
- 정리: 템플릿은 계속 자라지만 기존 프로젝트엔 자동 반영이 안 된다.
/update-from-template으로 가져오되, 스킬은 마법이 아니라 ① diff 전수 파악 → ② 위험도 A/B/C/D 분류 → ③ 너에게 확인 → ④ 등급별 선별 반영 을 대신 해줄 뿐이다. "최신 템플릿 적용" 은 "템플릿으로 통째 덮기" 가 절대 아니다 — 파일마다 누가 앞섰는지 보고 고르는 판단은 여전히 사람 몫이다. (반대 방향은/sync-template.) - 흔한 함정: 기존 프로젝트의 Django 모델(User 등) 을 "템플릿이 최신이라" 며 현행화하는 것. 마이그레이션이 갈라지고 Production DB·커스텀이 깨진다. 모델/마이그레이션이 걸리면 무조건 D — 동기화로 건드리지 말고, 정말 필요하면 별도 기능 작업으로.
- 다음 시간: M3 — 오늘 가져온
.githooks3종(commit-msg/pre-commit/pre-push)이 어떻게 강제·자동화 를 거는지 훅 메커니즘을 판다. - 자습 권장: 자기 프로젝트에서
diff -rq "$T/.claude" "$G/.claude"를 직접 떠보고 차이를 A/B/C/D 로 분류해 본 뒤,/update-from-template을 돌려 스킬의 분류와 내 분류를 비교해 보기. (확인 단계까지만 가고 실제 반영은 하지 않아도 된다.)