# change-review — diff·PR·커밋 범위 리뷰 **리뷰 경로**에서 연다. 화면 전체가 아니라 **git diff·PR·커밋 범위로 정의된 변경**을 스코프로 잡는 요청("이 PR 봐줘", "이 브랜치 확인해줘", "방금 커밋 리뷰해줘", "이거 고친 거 회귀 있는지 봐줘")이 트리거다. 화면 전체를 감사할 때는 이 문서가 아니라 [preflight.md](preflight.md)·[audit-gate.md](audit-gate.md)를 쓴다. 이 문서가 소유하는 것은 **스코프를 어떻게 잡는가**와 **finding에 어떤 상태를 매기는가** 두 가지뿐이다. 무엇이 얼마나 나쁜가(심각도·에스컬레이션·저비용 수정 사다리)는 [critique.md](critique.md) 소관이며, 이 문서는 그것을 다시 정의하지 않는다. 정확성·테스트·보안·성능은 프로젝트의 일반 코드 리뷰 소관이다 — 한 번만 짚고 넘어간다[SKILL-INTERFACE-REVIEW]. 이 문서의 **필수 절차**는 리뷰를 진행하는 과정의 규칙이다. 하드 게이트·프로젝트 계약·관찰 후보라는 판정 위계는 finding 자체에만 붙인다 — 절차 규칙을 하드 게이트라 부르면 심각도 매핑(하드 게이트 = Blocking)과 섞인다. ## 이 문서를 읽는 법 | 상황 | 읽을 곳 | |---|---| | 리뷰 대상 자체가 뭔지부터 정해야 한다 | §2 | | 락파일·스냅샷 같은 게 스코프에 잡혀 노이즈가 커진다 | §3 | | 토큰 파일 하나 고쳤는데 몇 화면에 퍼지는지 모른다 | §4 | | "고치다가 실수로 뭔가 지운 것 같다"를 확인해야 한다 | §5 | | "이 버그 우리가 만들었나 원래 있었나"를 가려야 한다 | §6 | | PR이 주장한 걸 다 했는지 확인해야 한다 | §7 | | detached HEAD·shallow clone·리베이스 도중이다 | §8 | | 파일이 이동+편집됐다 | §9 | | GitHub·Forgejo·Gitea의 PR을 가져와야 한다 | §10 | | 리포트를 어떻게 쓰는지 | §11 | | 심각도를 어디서 가져오는지 | §12 | --- ## 0. 언제 이 문서를 연다 트리거: 사용자가 브랜치·PR·커밋 범위·미커밋 변경을 구체적으로 지목한다. "이번에 뭐가 바뀌었나"를 묻는 요청이지 "이 화면이 지금 맞나"를 묻는 요청이 아니다. 두 질문은 다르다 — 전자는 diff의 `-`쪽과 `+`쪽을 함께 읽고 회귀를 찾는 것이고, 후자는 렌더된 최종 상태만 본다. 리뷰 경로는 0(스코프 확정) → 이 문서의 절차 → [critique.md](critique.md) 형식의 보고로 끝나며, 구현이 필요하면 사용자 요청에 따라 국소·연장 경로로 올라간다. 0단계 브리프 인터뷰(무엇을 만들지 확인)와 이 문서의 "스코프가 불분명하면 묻고 기다린다"(§2, 무엇을 검토할지 확인)는 목적이 다르다. 둘을 섞지 않는다. 검증 못 한 항목을 "미검증"으로 표시하고 진행하는 것(§11)도 0단계 인터뷰와는 별개 축이다 — 인터뷰는 시작 전에, 미검증 표시는 끝난 뒤에 쓴다[SKILL-INTERFACE-REVIEW]. --- ## 1. 읽기 전용 계약 **필수 절차.** 리뷰 대상 브랜치·PR·커밋을 체크아웃·switch·stash·reset하지 않는다. 작업자가 열어 둔 파일을 리뷰 스킬이 덮어쓰거나 버리게 만드는 사고를 막는 안전 계약이며, 예외 없이 지킨다. - **허용**: `git fetch`(원격 ref를 `.git` 안에만 쓴다), `git show :`로 파일 읽기, `git blame`, `git diff`, `git log -L`, `git grep `. - **금지**: `git checkout `, `git switch `, `git stash`, `git reset --hard`, 포지 CLI의 `pr checkout`류 명령. - **파일은 워킹트리 사본이 아니라 `git show :`로 읽는다.** 특히 포크에서 온 PR은 워킹트리와 다른 파일일 수 있다. - **렌더 검증이 필요하면**: 옵트인이다. 프로젝트가 저렴한 프리뷰(스테이징 배포 등)를 이미 제공하거나 사용자가 명시적으로 요청하지 않는 한, 시각·런타임 주장은 **"미검증"**으로 남긴다(§11). 격리가 필요하면 저장소 안 디렉터리에 `git worktree add`로 임시 워크트리를 만들고 끝나면 `git worktree remove`로 정리한다 — `rm -rf`로 지우지 않는다. 리뷰 대상 저장소가 무엇이든 같은 위생을 지킨다[SKILL-INTERFACE-REVIEW]. --- ## 2. 스코프 계산 **필수 절차.** 리뷰 대상을 지어내지 않는다 — 무엇을 봤다고 주장하는지 먼저 확정하고, 그 확정 근거를 스코프 블록(§11)에 남긴다. ### 대상 미지정일 때의 우선순위 사용자가 타깃을 지목하지 않으면 아래 순서로 시도하고 **첫 매치에서 멈춘다**. 1. `HEAD`가 `git merge-base origin/ HEAD` 대비 앞서 있다 → 그 범위 **+** 미커밋 변경. 커밋 수와 미커밋 파일 수를 **따로** 보고한다. 2. 워킹트리가 dirty(추적되는 파일이든 아니든)하다 → 미커밋 변경만. 3. 둘 다 아니다 → "리뷰할 변경이 없음"(아래 함정 참고). **작업 트리를 먼저 확인해야 하는 이유**: 순서를 뒤집으면 한 줄짜리 포매팅 수정이 열두 커밋짜리 브랜치를 가리면서도 "전체를 봤다"고 보고하는 사고가 난다. 추적되지 않은 새 파일도 포함한다 — 새로 추가한 컴포넌트가 "전체 커버"라 주장하는 스코프에서 조용히 빠지는 것을 막는다[SKILL-INTERFACE-REVIEW]. ### 대상이 명시됐을 때 | 타깃 | 의미 | |---|---| | `working` | 워킹트리 변경(추적+미추적) | | `staged` | 스테이지된 변경만 | | `branch` | 현재 브랜치 vs 기본 브랜치, merge-base 기준 | | `pr ` / `mr ` | §10 | | 단독 `` | 그 커밋 하나 | | `..` | 두 점 — 끝점끼리 직접 비교 | | `...` | 세 점 — `merge-base(,)`와 `` 비교 | **두 점과 세 점을 임의로 바꾸지 않는다.** 사용자가 쓴 점 개수를 그대로 존중한다. `release..feature`를 세 점으로 바꾸면 `release`와 merge-base 사이의 변경분을 통째로 놓칠 수 있고, 그게 바로 사용자가 물어본 것일 수 있다. **브랜치 diff는 merge-base(세 점) 기준이다.** 두 점 diff는 기본 브랜치에 이미 올라간 업스트림 커밋까지 변경으로 잡아버린다. ### 함정 — `git diff HEAD`가 놓치는 것 `git diff HEAD`는 **추적된 변경만** 본다. 미커밋 작업이 포함될 수 있는 타깃(`working`, `branch` + 워킹트리 dirty)에서는 반드시 `git ls-files --others --exclude-standard`와 짝지어 새로 추가된 파일을 잡는다. 안 그러면 새 컴포넌트가 "전체 커버"라 주장하는 스코프에서 빠진다. ### 리뷰할 게 없을 때 — 임의로 대체하지 않는다 **필수 절차.** 클린 트리 + 기본 브랜치 대비 앞선 커밋이 없으면, 이것은 "존재하지 않는 변경을 리뷰해 달라는 요청"이다. **마지막 커밋(`HEAD~1..HEAD`)으로 임의 대체하지 않는다.** 마지막 커밋은 우연히 거기 있게 된 무엇(머지 커밋이거나 남의 작업일 수 있다)이고, 그것에 대한 보고는 사용자가 실제로 의미한 것과 구분되지 않는다[SKILL-INTERFACE-REVIEW]. 발명하는 대신 사실을 먼저 모으고 제시한 뒤 **대기**한다. 1. 현재 브랜치, 클린 여부, 기본 브랜치 대비 커밋 수, 마지막 커밋의 짧은 SHA와 제목을 모은다. 2. 현재 브랜치에 열린 PR/MR이 있는지 확인하고, 있으면 **가장 먼저** 제시한다 — 커밋이 이미 머지돼 "변경 없음"으로 보여도 그 PR은 여전히 사용자가 의미한 것일 수 있다. 3. 정확히 3가지 경로만 제시한다: (a) 마지막 커밋 — SHA와 제목을 구체적으로 밝혀 뭘 받을지 보이게 함, (b) 사용자가 지정하는 타깃, (c) 저장소 전체 인터페이스 감사(이건 change review가 아니라 [critique.md](critique.md)의 화면 감사로 그대로 핸드오프 — 스코프 블록·상태·Pre-existing 섹션 없이). 제외 규칙(§3) 적용 후 스코프가 비었을 때도 같은 방식으로 묻는다. **"아무것도 볼 게 없는 상태"를 통과로 보고하지 않는다.** --- ## 3. 제외 경로와 예외 **프로젝트 계약.** 아래는 리뷰 스코프에서 제외해 노이즈를 줄이는 기본값이다 — 프로젝트가 다른 생성물 경로를 쓰면 맞춰 조정한다. | 카테고리 | 패턴 | |---|---| | 락파일 | `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `bun.lockb`, `Cargo.lock`, `composer.lock`, `Gemfile.lock`, `poetry.lock`, `uv.lock` | | 스냅샷·픽스처 | `__snapshots__/`, `*.snap`, `*.approved.*`, `test-results/`, `playwright-report/` | | 생성 산출물 | `dist/`, `build/`, `out/`, `.next/`, `.turbo/`, `coverage/`, `*.min.js`, `*.min.css`, `*.map` | | 생성 소스 | `*.gen.ts`, `*.generated.*`, 빌드가 뱉은 `*.d.ts` | | 벤더 코드 | `vendor/`, `third_party/`, `node_modules/` | | 바이너리·미디어 | 원본 자산 바이트 자체(폰트·이미지 예외는 아래 참고) | **무엇을 제외했는지 리포트에 이름으로 밝힌다.** 제외 후 스코프가 비면 §2의 "리뷰할 게 없음" 처리와 같다. **예외 2가지는 스코프에 남는다** — 바이트 자체가 아니라 그것을 참조하는 코드를 본다. - 추가·교체된 **폰트 파일**: [typography.md](typography.md)가 다루는 로딩·서브셋 계약이 바뀌었을 수 있다. - 컴포넌트에 추가된 **이미지**: `alt` 텍스트 분류와 파일 조달·크롭 모두 [images.md](images.md) 소관. **언더익스클루전 함정 2가지** 1. `*.lock`은 `yarn.lock`·`Cargo.lock`은 잡지만 `package-lock.json`·`pnpm-lock.yaml`은 못 잡는다 — 표의 확장자를 각각 커버해야 한다. 2. `**`는 glob 매직이 필요하다. 없으면 `*`가 `/`를 못 건너뛰어 `**/dist/**`가 루트 레벨 `dist/`를 놓친다. **검증**: 제외 pathspec 유무로 diff를 두 번 돌려, 파일 수가 정확히 이름 붙인 만큼 줄었는지 확인한다[SKILL-INTERFACE-REVIEW]. --- ## 4. 파급 범위 — diff는 표면이 아니다 **프로젝트 계약.** 변경 파일은 증거일 뿐 리뷰 대상 전부가 아니다. 실제로 리뷰할 것은 그 파일이 렌더되는 **표면 전체(blast radius)**다. - **기본은 1홉 확장**: 변경된 컴포넌트·함수를 직접 import·호출하는 것까지. - **2홉 확장은 디자인 토큰·테마 값·공유 프리미티브에 한정**한다 — 한 줄이 제품 전체에 닿을 수 있는 파일이라서다. [tokens.md](tokens.md)가 유일 원본인 토큰 파일이 여기 해당한다. - **고정 개수 상한을 두지 않는다.** 대신 위험 기준으로 고르고 선언한다: 사용자에게 노출되는 경로(라우트·레이아웃 진입점) 먼저, 그다음 importer 수가 많은 순, 동률이면 같은 패키지·기능 디렉터리 근접성. 프로젝트에 라우트 개념 자체가 없으면(컴포넌트 라이브러리 등) 1순위를 건너뛰고 바로 importer 수로 정렬한다. **몇 개를 확장했고 몇 개를 확장하지 않았는지 리포트에 명시한다** — 상한 없는 스윕은 주장할 수 있는 커버리지를 지지하지 못하고, 컷오프를 밝히지 않으면 완전한 것처럼 읽힌다. ### 컨슈머 검색 명령 ```bash # 리뷰 중인 ref를 대상으로 검색한다 — 워킹트리를 검색하면 # 이 변경 자체가 추가한 importer를 놓친다(특히 PR에서) git grep -n "ComponentName" "$REVIEWED_REF" -- '*.tsx' '*.astro' # 패턴이 -로 시작하면(예: 토큰명 --color-*) -e로 옵션 파싱을 막는다 git grep -n -e "--color-brand" "$REVIEWED_REF" ``` 결과는 `:path/to/file` 형태로 오며 `git show`로 읽는다(워킹트리 사본 금지, §1). **토큰·테마 값이 바뀐 경우 파일명이 아니라 토큰 이름으로 검색한다** — 컨슈머는 토큰을 import하지 않고 이름으로 참조하기 때문이다[SKILL-INTERFACE-REVIEW]. --- ## 5. 제거된 쪽 읽기 **필수 절차다.** 회귀는 변경 후 상태에서는 보이지 않는다. `+`쪽만 읽고 끝내지 않는다 — 모든 헝크의 `-`쪽을 아래 표와 대조한다. 이 절차 자체(제거된 쪽을 읽는다)는 건너뛰지 않는 필수 단계다. 다만 **표에 걸린 각 항목의 실제 심각도**는 그 항목을 소유하는 문서(accessibility.md·typography.md 등)의 판정 위계를 따른다 — 여기서 다시 정의하지 않는다. | `-`쪽에서 제거된 것 | 소유 문서 | 확인할 것 | |---|---|---| | `aria-label`, `aria-labelledby`, `aria-describedby`, `aria-live`, `role=` | [accessibility.md](accessibility.md) | 컨트롤·영역이 접근 가능한 이름·설명·알림을 잃었는가 | | ``, ``, `