Jujutsu(jj)를 사용해 큰 풀 리퀘스트를 로컬에서 단계적으로 리뷰하고 진행 상황을 버전 관리 히스토리로 추적하는 워크플로를 소개한다.
2026-03-02 · Ben Gesoff
지난 6개월 정도 Git의 대체재로 Jujutsu(jj)를 사용해 왔다. 이미 이에 대해 많은 이야기가 나와 있어서 반복하고 싶진 않으니, 이렇게만 말하겠다. 완전히 빠져들었다. 명확하고 리뷰 가능한 pull request를 만드는 데 있어 팔이 하나 더 생긴 느낌이고, 동료들이 굳이 jj를 채택하지 않아도 나만 도입해서 쓸 수 있었다.
내 전반적인 코딩 워크플로는 내가 하는 변경의 크기 같은 맥락에 따라 조금 달라지지만, 자주 squash workflow와 매우 비슷하게 돌아간다. 아직 jj를 살펴보지 않았다면, 추천하고 싶은 정말 좋은 글들이 많이 있다.
최근에는 코드를 리뷰할 때의 새 워크플로를 가다듬기 시작했다. 코딩 에이전트 사용이 늘면서 pull request의 크기도 커지는 듯하다. 그게 좋은 일인지 아닌지는 별개의 문제지만, 어쨌든 새로 들어오는 코드를 계속 따라가야 하고, 따라서 지금으로서는 리뷰를 해야 한다.
회사에서는 현재 Bitbucket Data Centre를 Git “forge”로 사용하고 있다. 실제로는 jj와 궁합이 꽤 좋다. 변경을 수정하고 다시 push하면 PR에서 interdiff를 보여주기 때문이다. 하지만 GitHub 같은 것과 비교했을 때 조금 아쉬운 점이 하나 있는데, 이미 확인했고 만족했던 파일이 무엇인지 추적하기가 그리 쉽지 않다는 것이다. 이 때문에 파일 사이를 자주 오가야 하는 큰 변경에서는 마찰이 꽤 커진다.
작은 pull request라면 변경의 전체 맥락을 머릿속에 넣어두고 비교적 선형적으로 훑어가기가 쉽다. 하지만 큰 변경, 특히 코드의 전체 구조를 아직 이해하지 못한 상태에서는 탐색이 매우 까다로워지고, 만족한 부분을 점진적으로 승인해 나가기가 어려워진다. 게다가 이미 어디까지 봤고 어디를 아직 안 봤는지의 맥락을 계속 머릿속에 유지해야 한다는 걸 알게 되면, 충분한 시간을 확보할 수 있을 때까지 리뷰를 미루게 되기 쉽다.
이를 해결하기 위해 내가 придум어낸 새 워크플로는 다음과 같다:
jj duplicate로 복제하고 jj edit로 "체크아웃"한다jj new --no-edit --insert-before @로 만든다이렇게 하면 익숙한 jj diff --stat 같은 명령으로 리뷰 진행 상황을 추적할 수 있고, 만족한 hunk나 파일 전체를 squash할 수 있다. 또한 언제든 다른 일을 하러 전환했다가 돌아와도, 돌아왔을 때 모든 것이 그대로 남아 있다는 점에서 마음이 편하다.
이 방법의 장점은 리뷰를 하는 동안 자연스럽게 익숙한 코딩 환경 안에 있게 된다는 것이다. pull request로 다시 돌아가 "파일을 확인함" 같은 표시를 하느라 컨텍스트 스위칭을 할 필요 없이, 내가 코드를 작성할 때와 동일한 IDE, 도구, 명령을 사용한다. 물론 코멘트를 남기기 위해서는 어느 시점에 웹 UI로 돌아가야 하지만, 언젠가 그 부분도 개선하고 싶다.
아래는 이 워크플로가 실제로 동작하는 예시다. 동료 Bill이 big-change라는 브랜치로 새 애플리케이션을 만들었다며 pull request를 보냈다고 하자. immutable change임을 나타내는 다이아몬드 기호에 주목하라.
/jj-reviewing-demo # jj git fetch -b big-change
bookmark: big-change@origin [new] untracked
/jj-reviewing-demo # jj log -r 'main..big-change@origin'
◆ yykxruwv bill@example.com 2026-03-01 11:54:39 big-change@origin dcf940aa
│ feat: add new Python project
~
Copy
Git에서처럼 --stat 옵션으로 show해 보면 우리가 상대해야 하는 변경의 규모를 알 수 있다. 물론 여기서는 개념을 보여주기 위한 매우 작은 변경이라, 실제 상황은 상상력을 발휘해 주길 바란다.
/jj-reviewing-demo # jj show y --stat
Commit ID: dcf940aabd45c210a175eacaa7477da6afe04136
Change ID: yykxruwvkwwtttqotzyrtqkoxmymytxl
Bookmarks: big-change@origin
Author : Bill <bill@example.com> (2026-03-01 11:47:12)
Committer: Bill <bill@example.com> (2026-03-01 11:54:39)
feat: add new Python project
.python-version | 1 +
main.py | 10 ++++++++++
pyproject.toml | 7 +++++++
uv.lock | 8 ++++++++
4 files changed, 26 insertions(+), 0 deletions(-)
Copy
첫 단계는 이를 조작할 수 있는 새 mutable change로 복제하는 것이다.
/jj-reviewing-demo # jj duplicate big-change@origin
Duplicated dcf940aabd45 as kowrummo 6ffd7580 feat: add new Python project
/jj-reviewing-demo # jj edit kow
Working copy (@) now at: kowrummo 6ffd7580 feat: add new Python project
Parent commit (@-) : romwosqw 718f5479 main | Initial commit
Added 4 files, modified 0 files, removed 0 files
/jj-reviewing-demo # jj log -r 'all()'
@ kowrummo bill@example.com 2026-03-01 12:28:21 6ffd7580
│ feat: add new Python project
│ ◆ yykxruwv bill@example.com 2026-03-01 11:54:39 big-change@origin dcf940aa
├─╯ feat: add new Python project
◆ romwosqw ben@example.com 2026-03-01 11:37:40 main 718f5479
│ Initial commit
◆ zzzzzzzz root() 00000000
Copy
그다음 현재 변경의 부모로서 새 빈 변경을 만든다. --no-edit 플래그는 kow 변경을 계속 편집 상태로 유지하게 해 주고, --insert-before @ 플래그는 현재 변경 @와 그 부모 사이에 변경을 삽입하며, --message 플래그는 커밋 메시지를 지정해 나중에 다른 일을 하러 전환했다가 돌아와도 쉽게 찾을 수 있게 해 준다.
/jj-reviewing-demo # jj new --no-edit --insert-before @ --message 'review: big-change'
Created new commit ltqpxklq 92fd29a9 (empty) review: big-change
Rebased 1 descendant commits
Working copy (@) now at: kowrummo b07bd9ec feat: add new Python project
Parent commit (@-) : ltqpxklq 92fd29a9 (empty) review: big-change
/jj-reviewing-demo # jj log -r 'all()'
@ kowrummo bill@example.com 2026-03-01 12:42:39 b07bd9ec
│ feat: add new Python project
○ ltqpxklq ben@example.com 2026-03-01 12:42:39 92fd29a9
│ (empty) review: big-change
│ ◆ yykxruwv bill@example.com 2026-03-01 11:54:39 big-change@origin dcf940aa
├─╯ feat: add new Python project
◆ romwosqw ben@example.com 2026-03-01 11:37:40 main 718f5479
│ Initial commit
◆ zzzzzzzz root() 00000000
Copy
이제는 내가 가장 편한 도구로 코드를 리뷰하면서, 머릿속에서 정리가 된 파일부터 review 커밋으로 옮길 수 있다.
/jj-reviewing-demo # jj status
Working copy changes:
A .python-version
A main.py
A pyproject.toml
A uv.lock
Working copy (@) : kowrummo b07bd9ec feat: add new Python project
Parent commit (@-): ltqpxklq 92fd29a9 (empty) review: big-change
/jj-reviewing-demo # cat .python-version
3.14
/jj-reviewing-demo # jj squash .python-version
Rebased 1 descendant commits
Working copy (@) now at: kowrummo 6aba8a2f feat: add new Python project
Parent commit (@-) : ltqpxklq b8225061 review: big-change
Copy
잠깐 자리를 비워 다른 일을 하고 돌아와도 되고, 지금까지 무엇을 봤는지의 기록은 버전 관리 히스토리에 담긴다. 마침내 모든 것을 다 보면, (이 예시에서는 ID가 kow인) 복제된 변경은 비게 되고, 대신 새 빈 변경으로 대체된다.
/jj-reviewing-demo # jj log -r 'all()'
@ vpmrpvpw ben@example.com 2026-03-01 12:49:28 9e957556
│ (empty) (no description set)
○ ltqpxklq ben@example.com 2026-03-01 12:49:16 5ed4a951
│ review: big-change
│ ◆ yykxruwv bill@example.com 2026-03-01 11:54:39 big-change@origin dcf940aa
├─╯ feat: add new Python project
◆ romwosqw ben@example.com 2026-03-01 11:37:40 main 718f5479
│ Initial commit
◆ zzzzzzzz root() 00000000
Copy
이 시점에서, 내가 남긴 코멘트나 변경 제안이 있는지 보기 위해 리뷰한 버전을 원본 변경과 비교할 수 있고, 이를 pull request에 반영하면 된다.
/jj-reviewing-demo # jj interdiff --from big-change@origin --to ltq
Modified commit description:
1: review: big-change
2:
1 3: feat: add new Python project
Modified regular file main.py:
1 1: def main():
2 2: print("Hello from jj-reviewing-demo!")
3: # PR: Hello Bill!
3 4: print(hello("Bill and Ben"))
4 5:
5 6:
...
Copy
이게 전부다. 코드를 단계적으로 처리할 수 있었고, 진행하면서 변경에 대한 진행 상황을 계속 남길 수 있었다.
내게 jj를 쓰는 명백한 장점은 도구의 강력함과 직관성이다. 멘탈 모델이 내재화되고 나면, 원하는 것을 달성하는 데 많은 명령이 필요하지 않다. Git에 매우 익숙하더라도, 여기저기 오가며 일관된 변경 묶음을 만드는 데는 여전히 몇 단계가 필요한 경우가 많다. 앞서 말한 리뷰 워크플로도 Git으로 흉내 낼 수는 있겠지만, 훨씬 번거롭고 stash, staging area, 되돌리기 고통스러운 실수를 하지 않기 같은 것들을 추적하느라 더 많은 인지 부하가 든다. 이는 모두 리뷰 자체에서 인지 자원을 빼앗아 간다.
처음에 코드를 작성할 때도, 더 미묘한 장점이 있다고 생각한다. 변경을 반복해서 다듬을 수 있고, 모든 것이 뒤에서 스냅샷으로 남고 유지된다는 감각은 다른 사람과 미래의 나 자신에게 변경을 어떻게 제시할지 더 의도적으로 생각하게 만든다. Git 도구는 거대한 한 방 변경이나, 사후에 squash해야 하는 무작위 커밋 연속을 만들도록 은근히 유도하는 반면, jj는 내가 만드는 변경을 생각하도록 이끌고 그 과정을 조용히 기록한다. diff를 그대로 던져주는 것보다 변경의 "이야기"를 들려주는 편이 리뷰어 입장에서는 분명 더 낫다.
여러 면에서 이 워크플로는 저장소에서 리뷰 상태를 추적하고 로컬에서 코드를 리뷰하는 방법을 다룬 matklad의 TigerBeetle 글에서 설명한 것과 유사하다. 그 글에서 말하듯, Git에서는 soft reset과 staging area 사용이 필요해 다소 번거롭고, (별도의 worktree를 쓰지 않는 한) 잠깐 멈추고 다른 일을 해야 할 때 까다롭다. 그들이 그 아이디어를 포기한 몇 가지 이유는, 충돌 처리가 더 쉽고 커밋 변경을 추적할 수 있는 안정적인 change ID가 있는 jj를 쓰면 실제로 개선될 수도 있다.
또한 Jane Street의 Iron 코드 리뷰 시스템에 나오는 “brain” 개념과도 어느 정도 비슷한 점이 있다. “brain”은 리뷰어가 이미 리뷰한 변경의 일부를 나타내는 diff에 가깝다. 내가 squash해 넣던 빈 커밋이 Iron 용어로는 사실상 내 “brain”이다. 그들의 시스템에는 여기서 설명한 것보다 훨씬 더 많은 요소가 있으니, 아직 보지 않았다면 살펴보길 추천한다.
이 두 워크플로는 모두 코드에 인라인으로 리뷰 코멘트를 남기는 것을 수반한다. 실제로 matklad가 여기서 Jane Street에서 영감을 받았다고 생각하는데, 나도 이 부분을 실험 중이다. 현재는 내 로컬 리뷰 변경에 인라인 코멘트와 나 자신에게 남기는 메모를 남긴 뒤, jj interdiff로 이를 보여주고 리뷰가 끝났을 때 웹 UI에서 코멘트로 수동으로 옮기고 있다. 이런 식으로 코멘트를 한 번에 적용하면, 모든 코드를 리뷰할 때까지 웹 UI로 돌아갈 필요가 줄어든다.
언젠가 로컬 interdiff(리뷰 변경과 원본 변경의 비교)를 바탕으로 리뷰 코멘트를 PR로 자동 전송하는 스크립트를 대충이라도 만들 생각이다. 다만 지금은, 리뷰 초반에 전체 맥락이 없어서 남겼을지도 모르는 코멘트를 나중에 다시 다듬을 수 있게 해 주기 때문에, 이 두 번째 수동 패스가 꽤 마음에 든다.
인라인 코멘트가 포함된 커밋을 pull request로 다시 push해서 팀원들이 보게 하는 것도 흥미로운 아이디어다. 하지만 그러려면 팀 전체가 새 워크플로를 채택해야 하므로, 세부 사항을 충분히 다듬기 전까지는 여기까지 나아가진 않으려 한다.
아직 pull request에 대한 후속 업데이트를 통합하는 과정은 제대로 정립하지 못했다. 다행히 업데이트는 원본 변경보다 훨씬 작은 편이라 diff를 그냥 보면 충분한 경우가 많다. 앞으로는 새 버전의 변경을 복제해 내가 리뷰한 버전 위로 rebase해서 차이를 보는 방식 같은 것을 생각해 볼 수도 있겠지만, 지금까지는 과하다는 느낌이 들었다.
Git 대신 jj를 사용할 때의 단점 중 하나는 IDE 통합이다. 나는 JetBrains IDE를 쓰는데, Selvejj 플러그인이 있긴 하지만 프리릴리스 버전이고 문서에 따르면 기능이 완전하지 않다. 나는 이를 우회하기 위해 jj를 "colocated" 모드로 사용한다. 즉 IDE는 여전히 내가 Git을 쓴다고 생각한다(어떤 의미에서는 맞다. git CLI로 상호작용하지 않을 뿐이다). jj edit으로 변경 편집을 시작하면 IDE는 작업 복사본에 있는 기존 변경을 커밋되지 않은 변경으로 보여 주고, Git 커밋을 준비하듯이 둘러볼 수 있게 해 준다. 이 방식은 대체로 잘 동작하지만, 다른 jj 워크스페이스(= Git worktree와 비슷함)에서 작업할 때는 예외다. .git 디렉터리가 다른 워크스페이스에는 복제되지 않으므로 IDE가 이를 인식하지 못한다. Git 통합이 필요할 때는 기본 워크스페이스를 쓰는 것을 기억해야 한다.
전반적으로 나는 이 워크플로와 jj 사용 전반에 만족한다. 도구가 방해하지 않아, 코드에 대해 생각하는 데 필요한 인지 용량을 온전히 쓸 수 있다. 팀 전체에 강요하지 않고도 워크플로 개선을 떠올리고 시도해 볼 수 있다는 점이 정말 좋다. 생태계가 계속 성장하는 모습과, 협업 방식을 계속 다듬어 나가는 과정이 무척 기대된다.