
x402: 메인 저장소에 PR이 머지되기까지 — 매우 활발한 프로토콜 repo에서 '통과'하기 위한 실록
요약
활발한 오픈소스 프로젝트인 x402 저장소에서 PR(Pull Request)을 성공적으로 머지하기 위해 거쳐야 하는 단계와 전략을 다룹니다. 단순한 코드 작성을 넘어 거버넌스 이해, 선행 사례 조사, 엄격한 기여 규칙 준수의 중요성을 실무 경험을 바탕으로 설명합니다.
핵심 포인트
- 머지는 기술력뿐만 아니라 저장소의 거버넌스에 적응하는 문제임
- PR 제출 전 이슈 및 과거 설계 논의에 대한 철저한 정찰 필수
- 중복 PR 방지를 위해 gh CLI 등을 활용한 선행 사례 검색 권장
- 프로젝트의 CONTRIBUTING 가이드라인과 커밋 규칙 엄격 준수
「코드를 작성할 수 있는 것」과 「머지(Merge)되는 것」은 별개의 기술입니다.
x402(HTTP 네이티브 에이전트 결제 프로토콜)의 메인 저장소 x402-foundation/x402는 이슈(Issue)가 약 2,700개, PR(Pull Request)이 2,000개가 넘는 매우 활발한 repo입니다. 저는 이곳에 수 주 동안 커밋(Commit)을 계속 보냈고, 2개의 PR이 머지되었으며, 1개는 중복되어 스스로 닫았고, 1개는 메인테이너(Maintainer)로부터 NACK(불채택)를 받았습니다.
이 기사는 그 「통과된 PR」과 「떨어진 PR」의 차이를 역산하여, **이슈를 발견한 시점부터 머지될 때까지 실제로 통과해야 하는 게이트(Gate)**를 실측치와 함께 나열한 것입니다. 결론부터 말씀드리면——머지는 기술력의 문제라기보다, repo의 거버넌스(Governance, 통치)에 자신을 올바르게 위치시킬 수 있는가의 문제였습니다.
전체상 — 머지까지의 파이프라인
먼저 지도입니다. 이슈를 발견한 후 머지될 때까지 실제로는 이만큼의 게이트가 있습니다.
이슈 발견
│
├─ ① 정찰 … 기존 PR / 선행 논의를 모두 조사
...
그리고 이 repo에서 제가 실측한 소요 시간은 다음과 같았습니다.
| 구간 | 중앙값 | 비고 |
|---|---|---|
| PR 생성 → 첫 LGTM | 1.5일 | p75 = 2.8일 / 최대 8일 |
| 승인 → 머지 | 6분 | 머지 담당자는 사실상 1명에게 집중 |
즉, 병목 구간은 「승인을 받기까지」이며, 승인만 나면 몇 분 안에 들어갑니다. 따라서 싸워야 할 지점은 리뷰(Review)에 올리기 전의 준비와 리뷰어(Reviewer)가 올바르게 찾아내도록 하는 움직임에 있습니다.
게이트 ① 정찰 — Claim 하기 전에 선행 사례를 모두 조사하기
첫 번째 게이트에서 곧바로 실패했습니다.
#2603
(FastAPI의 미들웨어가 settle 이후의 예외를 빈 바디(Empty body) 402로 삼켜버리는 버그)를 발견하여 수정 PR을 제출했습니다. 그런데 보고자 본인이 이미 수정 PR을 제출한 상태였습니다——거의 완벽한 중복이었습니다. 「실례했습니다」라는 말을 덧붙이며 제 PR을 닫게 되었습니다.
활발한 repo에서는 「이슈를 제기한 사람이 동시에 PR도 제출하는 것」이 일상적입니다. 게다가 closes #N 구문을 사용하지 않은 PR은 이슈의 "linked PR" 란에 나타나지 않습니다. 육안으로는 보이지 않는 것입니다.
따라서 claim 하기 전에 반드시 두 단계로 검색합니다.
gh pr list --repo OWNER/REPO --search "<issue번호>"
gh search prs --repo OWNER/REPO --author "<보고자의 계정>"
이것만으로는 부족하다는 것을 나중에 뼈저리게 느낀 사건이 하나 더 있습니다. 어떤 설계를 「이것이 옳다」고 제안하는 PR을 냈더니, 메인테이너로부터 NACK를 받았습니다. 이유는 동일한 메커니즘을 논의했던 선행 이슈와, 의도적으로 그 구현을 삭제했던 과거의 MERGED PR을 간과했기 때문입니다. 기존 PR의 체크만으로는 부족하며, 선행된 「논의」와 「과거의 설계 판단」까지 거슬러 올라갈 필요가 있었습니다.
대책으로서, 메인 저장소의 모든 PR(2,000개 초과)을 로컬에 색인화하여 grep으로 몇 초 만에 선행 사례를 찾을 수 있도록 했습니다. 키워드 하나만으로 찾고 있던 과거 PR이 나옵니다. claim 전의 정찰은 이슈 검색, PR 검색, 과거의 클로즈(Close)된 설계라는 세 측면에서 수행한다——이것이 첫 번째 교훈입니다.
게이트 ② 입장 조건 — CONTRIBUTING의 엄격함
x402의 CONTRIBUTING은 외부 PR에 대해 명확하게 엄격합니다. 다음 4가지를 명문으로 요구합니다.
- 커밋 서명(Commit Signature) 필수 (CI가 미서명 커밋을 거부함)
- AI 사용 신고
- Scope 외의 "덤으로 하는 개선" 금지
- 저품질의 양산형 PR은 close
그중에서도 서명은 처음에 부딪히는 물리적인 관문입니다. 저는 SSH 서명으로 통과했습니다. 요점만 정리하면:
gpg.format=ssh/user.signingkey=<공개키>/commit.gpgsign=true를 local 설정으로- 수중에 비밀키가 있는 키의 공개키를 GitHub에
type=signing
(다른 키를 웹에서 잘못 등록하여 돌아다닌 경우)- 커밋의 이메일이 GitHub 측에서 검증되지 않으면, 서명하더라도 unverified_email가 됩니다.
여기서 한 가지 팁은 여러 계정을 사용해 분리하는 사람들을 위한 함정입니다. global git config가 다른 이름으로 설정되어 있으면, 다른 사람 명의로 커밋이 올라갑니다. 저는 이 실수를 저질렀다가, 리포지토리(repo)마다 local config를 사용하여 이름을 덮어쓰는 방식으로 변경했습니다.
게이트③ 구현 — repo 고유의 로컬 함정
코드 자체보다 해당 repo에서만 발생하는 빌드/형식 지정 트랩에 시간을 많이 쓰게 됩니다. x402(Python 패키지 측)에서 겪었던 것을 공유합니다.
로컬 uv가 새로운 TOML 구문(uv.lock을 커밋에 포함하지 않음. exclude-newer)을 해석하지 못하고, uv run할 때마다 lock 파일을 재(再)생성해 버립니다. PR 전에 git checkout <path>/uv.lock으로 되돌려야 합니다. -
line-length = 100을 명시합니다. 위의 TOML 오류 때문에 형식 지정 도구가 설정을 읽지 못하고, 기본값인 88로 잘못 폴백(fallback)합니다. black --line-length 100을 명시하지 않으면 기존 줄까지 포함되어 차이점(diff)이 지저분해집니다. -
포매터와 린터의 역할 분담을 지킵니다 (black이 형식 지정, ruff가 lint, E501은 black 관할로 무시). -
changelog 조각이 필수적입니다(towncrier 형식의 changelog.d/<issue>.bugfix.md를 추가합니다). 이것이 없으면 리뷰가 멈춥니다.
핵심은 자동 형식 지정을 '자신의 추가된 줄'에만 국한하는 것입니다. repo 전체를 일괄적으로 형식 지정하면, 리뷰어 입장에서 '무엇을 변경했는지 알 수 없는 PR'이 되어 AI가 대량 생산한 저품질(low-quality) PR과 구별할 수 없게 됩니다.
게이트④ CI — 막는 빨간색과 막지 않는 빨간색
CI가 빨갛더라도 당황해서는 안 됩니다. 구조적으로 실패하는 것만 보여주는 무해한(benign) 빨간색이 있기 때문입니다.
제 PR에서 관찰한 패턴:
| 체크 | 결과 | 의미 |
|---|---|---|
| labeler | ✅ | 라벨 부여 |
| ... | ||
check-verified-commits의 빨간색은 진짜(서명 미비)이므로 반드시 고쳐야 합니다. 반면 Vercel의 빨간색은 포크(fork)에서 온 PR에 미리보기 환경 배포 권한이 없기 때문에 반드시 떨어지는 것이며, 유지보자(maintainer)도 알고 있는 부분입니다. 어떤 빨간색이 승인을 막는지를 처음에 파악해 두면, 헛된 수정에 시간을 쓰지 않아도 됩니다. |
게이트⑤ 리뷰 — 기다리는 대신, 감사관이 되기
여기가 가장 많이 배운 부분이었습니다.
활발한 결제 repo에서 사소한 버그 수정을 내놓아도 가치가 없습니다. 이유는 두 가지입니다. 첫째, 핵심(core) 영역은 이미 포화 상태이며, 게다가 유지보자들은 AI가 대량 생산하는 PR에 적대적이기 때문입니다. 자신의 작은 PR은 다른 다수 속에 묻히기 쉽습니다.
그래서 전략을 바꿨습니다. 내가 PR을 내고 기다리는 대신, 타인의 결제 PR을 면밀히 검토하여 허점을 찾는 감사관 역할을 하는 것. 이것은 AI-slop과 가장 구별하기 쉬운 첫 수이기도 합니다.
실제로 했던 일:
크레딧 선점(Credit claiming). 리뷰는 '좋은 구현이지만, 다만'으로 시작합니다. 경쟁자가 아닌 감사관처럼 행동하는 것입니다. -
cross-SDK의 parity gap을 지적합니다. 어떤 PR이 closes #2386 (malleability 거부)인데 Go에만 구현되어 있고, 이슈가 요구한 TS/Python은 떨어진 채 자동 종료되는 것을 지적했습니다. 또 다른 PR에서는, TS 측에만 Transfer 이벤트 검증을 추가하여 Go 쪽에 구멍이 남는 것을 발견했습니다. -
리뷰가 원 작업의 리드로 변모합니다. '그럼 Go 포팅(porting)은 후속 조치로 제가 하겠습니다'라고 밝히자, 누구와도 중복되지 않으면서 게다가 손안의 검증 도구 한가운데를 통과하는 구현이 리뷰에서 자연스럽게 탄생했습니다.
이 과정을 통해 repo의 거버넌스 구조도 보였습니다. 승인자는 2명, 병합 담당은 사실상 1명으로 집중되어 있습니다. 누구의 승인이 유효한지 알고 있으면, 리뷰를 '올바른 사람'에게 할 수 있습니다.
그리고 감사역(Auditor)으로서 움직이다 보면, 메인테이너(Maintainer) 측에서 "함께 하자"라는 제안이 오는 순간이 있습니다. 제가 지적한 내용을 상대방이 구현에 반영하여, 감사 인사를 곁들여 답장해 오는 것입니다. 이는 단순히 PR이 머지(Merge)되는 것 이상의 가치가 있는 신호였습니다(내가 작성한 코드가 포함되는 것보다, "필요로 해졌다"라는 체감이 더 컸습니다).
게이트 ⑥ 제출한 후 — 침묵을 견디고, 지적에는 전부 응답할 것
두 번째 PR은 14일 동안 아무런 반응이 없었습니다.
앞서 보여드린 표를 떠올려 보세요. 이 repo의 "PR 생성 → 첫 LGTM"은 중앙값(Median) 1.5일, 최대 8일입니다. 14일은 제가 직접 측정한 분포의 범위를 벗어난 수치였습니다. 재촉할 근거는 수치상으로 갖춰져 있었습니다.
그럼에도 기다렸더니, 메인테이너가 스스로 움직여 머지했습니다. 분포를 벗어났다는 사실 자체는 재촉의 이유가 되지 않습니다. 통계는 "언제 신경 쓰기 시작해야 하는가"는 알려주지만, "언제 말을 걸어도 되는가"는 알려주지 않습니다. 그래서 조건을 정했습니다——재촉해도 되는 때는 머지 판단을 바꿀 "새로운 사실"이 있을 때뿐(형제 PR이 먼저 들어왔거나, 사양이 변경되었거나 등)으로 말이죠. 단순히 날짜가 지났다는 이유로 재촉하는 것은 상대의 시간을 뺏는 것일 뿐입니다.
그리고 돌아온 반응은 "format/lint를 수정하면 넣을 수 있다"라는 한 문장이었습니다. 그 한 점을 수정해서 보내려고, 만약을 위해 CI를 전부 열어보니 언급되지 않은 빨간색(Error)이 하나 더 있었습니다. 의존성(Dependency)을 아무것도 추가하지 않았는데 uv.lock에 1,778행의 차분이 섞여 들어와 있었고, 다른 테스트를 떨어뜨리고 있었던 것입니다. 그대로 보냈다면 또 빨간색 상태로 돌아왔을 것이고, 왕복 횟수만 한 번 더 늘어났을 것입니다.
리뷰어(Reviewer)는 빨간색을 전부 나열하지 않습니다. 그들의 업무는 당신의 PR을 디버깅하는 것이 아니기 때문입니다. 지적을 받았다면, 지적된 항목만이 아니라 CI 전체를 초록색(Pass)으로 만든 뒤에 반환하십시오. 답장도 "무엇이 깨졌고 무엇을 고쳤는지"에 대해서만 합니다. 이 repo에서는 AI가 양산한 PR이 코드가 아닌 동작(Behavior) 때문에 close 되고 있었으며, 실제로 어떤 선행 PR은 mergeable 상태임에도 불구하고 "CI 실황 댓글의 연타"를 이유로 닫히기도 했습니다.
게이트 ⑦ 머지되는 PR / 떨어지는 PR의 차이
통과된 2개와 떨어진 2개를 나란히 놓고 보면 차이가 명확해집니다.
| PR | 결과 | 차이의 정체 |
|---|---|---|
| flask 동기 서버 fix | ✅ 머지 | 자기 발견 · 자기 수정 · scope 1점 · core 영역을 침범하지 않음 |
| flask 예외 핸들링 | ✅ 머지 | 보고자와 cross-review 완료 · 기존 구현과 shape를 맞춤 · CI 전체 초록색으로 반환 |
| FastAPI 예외 fix | ❌ 중복 close | 정찰 부족 (보고자 본인이 이미 보고함) |
| 어떤 설계 제안 | ❌ NACK | 선행 논의 · 과거 설계 판단의 간과 |
두 번째 PR에서는 사소하지만 치명적인 지적도 받았습니다. 에러 이유에 unexpected_settlement_error라는 직접 만든 문자열을 넣었는데, 그 값은 사양(Spec)의 enum에 없었기에 사양에 충실한 client 측 검증에서 걸러지게 됩니다. 정석인 unexpected_settle_error로 수정했습니다. 프로토콜 repo에서는 자유 문자열을 쓰는 순간 버그입니다.
나아가, 외부 컨트리뷰터(Contributor)로서 알아두어야 할 지형이 있습니다. 본체의 모든 PR을 색인화하여 경향을 살펴보면, 머지되기 쉬운 정도가 종류별로 편중되어 있습니다.
- fix(버그 수정) = 통과하기 쉬움 (약 69%)
- feat / docs = 중간 (약 45%)
- test = 통과하기 어려움 (약 25%)
- security 외부 PR = 소수 (약 17%)
해석하자면 이렇습니다. core가 외부에 열어두는 것은 작은 fix · spec과의 정합성 · docs의 기술적 수정입니다. 반대로, 아키텍처의 변경이나 보안의 근간은 core의 영역이며, 외부에서 투기적으로 발을 들이면 NACK를 받게 됩니다. 저의 NACK 또한 바로 설계 제안이라는 core 영역을 침범한 것이었습니다.
요약 — 머지는 통치(Governance)에 자신을 두는 것
x402 본체에서 PR을 통과시키기 위해 효과적이었던 것들을 차례로 정리하겠습니다.
- claim 전에 세 방향으로 정찰하기 (기존 PR · 선행 논의 · 과거의 설계 판단)
- 입장 조건을 기계적으로 충족하기 (서명 · AI 신고 · scope · 명의)
- repo 고유의 함정을 미리 제거하기 (lock · line-length · changelog 파편 · 사양의 enum)
- CI의 빨간색(Error)을 구분하기 (서명의 빨간색은 진짜 / preview의 빨간색은 benign)
- 기다리지 말고 감사역(Auditor) 역할을 수행하기 (크레딧 선행 · parity gap · 리뷰를 원작업으로 전환)
- 침묵에 재촉으로 응답하지 않기 (조건은 일수가 아니라 "새로운 사실"의 유무)
- 지적받은 항목이 아니라 CI 전체를 초록색(Success)으로 만들어 돌려주기 (리뷰어는 빨간색을 전부 열거하지 않는다)
- core 영역을 침범하지 않기 (통과되는 것은 소규모 fix · spec 정합성 · docs)
코드는 이 파이프라인의 아주 작은 한 구간일 뿐이었습니다. 머지된 PR과 되지 않은 PR을 가른 것은 구현의 숙련도가 아닙니다. 그 repo의 통치(Governance) 어디에 자신을 두었는가입니다.
게다가 그 "두는 방식"의 대부분은 기다리는 동안의 행동이었습니다. 14일간의 침묵을 견디는 것. 재촉의 조건을 자신에게 부과하는 것. 지적받은 범위 밖까지 청소한 뒤에 돌려주는 것. 실황을 쓰지 않는 것. 이 모든 것은 코드가 아닙니다. 초활발한 OSS에 기여한다는 것은 좋은 코드를 쓰는 것 이상으로, 그 커뮤니티의 의사결정 구조를 읽고 자신을 그 안의 "신뢰할 수 있는 한 수"로서 올바르게 배치하는 것임을 4개의 PR이 가르쳐 주었습니다.
(보충: 본 기사의 컨트리뷰션은 모두 공개 리포지토리상의 실적입니다. JPYC on Polygon 고유의 이야기—비 CDP facilitator에는 /health가 없으며, runtime의 402가 유일한 ground truth가 된다는 등의 논점—도 이 감사역으로서 움직이는 과정에서 실제로 꺼내든 재료였으나, 별도의 글로 남겨두겠습니다.)
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기