글 목록으로
2026년 10월 16일
9분 소요

오픈소스 기여가 일상이 됐다: Claude Code와 함께 올린 PR 18개, 머지 3개

예전에 내가 오픈소스에 올린 PR은 대부분 문서 번역이었다. 처음 보는 코드베이스를 읽고 버그를 고쳐 메인테이너 리뷰를 통과하는 일은 퇴근 후 시간으로는 엄두가 안 났다. 올해 4월부터 10월까지 Claude Code와 함께 10개 레포에 PR 18개를 올렸고, opik·penpot·haystack에서 3개가 머지됐다. 머지된 세 PR이 리뷰를 어떻게 통과했는지, 닫힌 PR 10개에서 배운 레포 규칙, 그리고 에이전트와 처음 기여를 시작하려는 사람이 따라 할 순서를 내 PR 기록으로 정리했다.

GitHub에 남은 내 예전 오픈소스 PR은 2018년 nuxt 문서의 한국어 번역, 2019년 Kubernetes 문서의 한국어 번역이 대부분이다. 코드를 고치는 PR을 올리고 싶은 마음은 늘 있었다. 하지만 처음 보는 코드베이스에서 버그가 난 곳을 찾고, 레포가 쓰는 빌드 도구와 테스트를 돌리고, 영어로 리뷰를 주고받는 일을 퇴근 후 몇 시간으로 해내기는 어려웠다. 번역은 그 시간 안에 끝낼 수 있는 기여였다.

올해 4월 18일부터 10월 16일까지 Claude Code와 함께 10개 레포에 PR 18개를 올렸다. comet-ml/opik, penpot, deepset-ai/haystack에서 3개가 머지됐고, 5개는 리뷰를 기다리고 있고, 10개는 닫혔다. Python 평가 지표, SCSS 레이아웃, Rust로 짠 로그 압축기, Vite 개발 서버의 미들웨어까지, 혼자서는 손대지 않았을 코드였다.

이 글은 그 반년의 기록이다. 머지된 세 PR이 리뷰를 어떻게 통과했는지, 닫힌 PR 10개가 알려 준 레포 규칙, 그리고 에이전트와 함께 첫 기여를 해 보려는 사람이 따라 할 수 있는 순서를 정리했다. 근거는 GitHub에 공개된 내 PR과 리뷰 기록, 각 레포의 기여 정책 문서다. 메인테이너가 리뷰 없이 머지하는 쪽의 이야기는 #81에서 다뤘고, 이번 글은 외부 기여자가 리뷰를 받는 쪽에서 본 이야기다.

무엇이 쉬워졌나

예전에 코드 기여를 포기하게 만든 일은 코드 수정 자체보다 그 앞뒤에 있었다. 이슈를 재현할 환경을 만들고, 관련 코드가 어디 있는지 찾고, 레포가 정한 포매터와 린터와 타입 체커를 맞추고, 테스트를 추가하는 일이다. 에이전트와 일하면서 이 부분이 크게 줄었다.

  • 재현: 이슈에 적힌 재현 스크립트를 실제로 돌려 버그를 먼저 확인했다. redis-vl-python은 redis:8.4 컨테이너를 띄워 재현했다.
  • 레포 도구: haystack은 hatch로, headroom은 cargo와 pytest로, penpot은 prettier와 stylelint로 검사했다. penpot은 로컬 pnpm이 깨져 있어서 검사 도구만 따로 설치해 돌렸다. 이런 우회는 혼자였다면 거기서 멈췄을 일이다.
  • 테스트: 새 테스트가 수정 전 코드에서는 실패하고 수정 후에는 통과하는지 매번 확인했다. 이 확인은 뒤에서 보듯 리뷰어가 실제로 들여다본 항목이었다.
  • PR 본문: 레포마다 다른 템플릿과 제목 형식을 채우고, 변경 이유와 검증 결과를 적었다.

머지된 세 PR의 크기는 각각 22줄 추가 6줄 삭제(penpot), 79줄 추가 9줄 삭제(opik), 17줄 추가 3줄 삭제(haystack)다. 작게 고칠 수 있는 버그를 고르고, 고친 이유를 리뷰어가 바로 확인할 수 있게 만드는 일에 시간을 썼다.

머지된 세 PR

opik #8528: 이슈에 적힌 해법을 리뷰가 바꿨다

opik의 ConversationDegenerationMetric은 대화형 AI의 답변이 같은 말을 반복하며 무너지는지를 점수로 매기는 지표다. 이슈 #8519는 “…”, “???“처럼 단어가 없는 답변이 점수 계산에서 통째로 빠진다고 지적했고, 이런 답변을 최대 위험(1.0)으로 매기자고 제안했다. 첫 구현은 그 제안을 그대로 따랐다.

리뷰어 aadereiko는 수정 요청과 함께 네 가지를 짚었다. 단어 없는 턴에서 이전 턴 기록을 비우면 “같은 문장 → … → 같은 문장”처럼 필러를 끼운 반복을 놓친다. 1.0으로 고정하면 다른 턴이 모두 거치는 네 요소 평균을 건너뛴다. 같은 문장을 그대로 반복한 턴도 0.25인데 “…” 하나가 1.0이 되고, 지표는 최댓값을 보고하므로 정상 답변 50개 사이의 “…” 하나가 대화 전체를 최악으로 만든다. 나머지 두 가지는 문서 미갱신과 테스트 누락이었다.

다시 들여다보니 기존 헬퍼 함수들이 빈 토큰 목록을 이미 처리하고 있었다. 단어 없는 턴을 건너뛰던 continue 한 줄을 지우자 그 턴은 한 단어짜리 답변과 같은 0.25가 됐다. 단어 없는 턴은 이전 턴 기록을 바꾸지 않게 했고, 필러를 사이에 둔 반복 테스트와 문서를 추가했다. 이후 리뷰 봇이 “1.0으로 되돌리자”고 제안했을 때는 메인테이너 리뷰를 근거로 들어 반영하지 않았다.

PR을 승인하고 머지한 awkoy의 코멘트는 짧았다. “필러를 사이에 둔 반복이 테스트되고 있고, 테스트가 main에서는 실패해야 할 때 실패한다(the tests fail on main as they should).” 수정 전 실패를 확인해 PR 본문에 적어 둔 것이 그대로 승인 근거가 됐다. 리뷰 사흘 뒤에 수정을 올렸고, 다시 사흘 뒤인 9월 30일에 머지됐다.

penpot #11487: 스크린샷 네 장으로 받은 리뷰

penpot의 타이포그래피 샘플은 영어 “Ag”에 맞춰 24px 칸에 들어가 있었다. 한국어 로케일에서는 샘플 문자열이 “가나다”이고 아랍어는 그보다 길어서, 샘플이 줄바꿈되며 행을 넘쳤다. 한국어 사용자라서 먼저 눈에 띈 버그다.

샘플 칸이 내용에 맞춰 늘어나게 고쳤는데, 리뷰어 EvaMarco가 스크린샷과 함께 네 곳을 짚었다. 예전에는 고정 너비가 만들어 주던 간격이 사라져서 샘플과 이름이 겹친다는 지적이었다. 대시보드 라이브러리 카드, 사이드바 상세 화면, 이름 변경 행을 하나씩 고친 커밋을 올리고 항목별로 답했다. penpot은 커밋에 DCO 서명(-s)과 :bug: 형식의 메시지를 요구한다. 전후 비교 스크린샷은 fork의 별도 브랜치에 올려 커밋 해시로 고정된 주소로 링크했다. 9월 3일에 열어 10월 1일에 머지됐다.

haystack #12908: 이슈에 없던 부작용을 하나 더 찾았다

이슈 #12893은 LinkContentFetcher._resolve_handler의 docstring이 실제 동작과 다르다는 내용이었다. 코드를 읽다가 이슈에 없던 문제를 하나 더 찾았다. 등록되지 않은 콘텐츠 타입이 들어오면 폴백 경로가 self.handlers["text/plain"]을 읽는데, handlers가 defaultdict라서 읽기만 해도 text/plain 키가 dict에 실제로 들어갔다.

>>> f = LinkContentFetcher()
>>> "text/plain" in f.handlers
False
>>> f._resolve_handler("font/woff")
<function _text_content_handler ...>
>>> "text/plain" in f.handlers
True

재현 결과를 PR 본문에 그대로 넣고, font/woff, multipart/form-data, 빈 문자열에서 폴백이 handlers를 바꾸지 않는지 확인하는 테스트를 추가했다. 테스트는 main에서 실패하고 수정 후 통과한다. haystack이 요구하는 release note를 넣고 CLA에 서명했고, PR 본문에는 “이 PR은 전부 AI 어시스턴트로 생성했고, 변경 내용을 검토하고 관련 테스트를 돌렸다”는 문장을 적었다. 리뷰 중에 main과 충돌이 나서 리뷰어 sjrl이 해결을 요청했고, 다음 날 테스트 위치를 옮겨 충돌을 풀었다. 10월 15일에 머지됐다.

haystack의 CONTRIBUTING.md에는 규칙이 하나 더 있다. 커뮤니티 기여자는 이 레포에 PR을 한 번에 하나만 열어 둘 수 있고, 추가로 연 PR은 자동으로 닫힌다. 처음에는 첫 기여자에게만 적용됐는데, 내 PR이 리뷰를 기다리던 9월 29일에 모든 커뮤니티 기여자로 범위가 넓어졌다.

닫힌 PR 10개가 알려 준 것

닫힌 PR을 이유별로 묶으면 다음과 같다. 코드 품질 때문에 진 건 headroom 하나였고, 나머지는 리뷰어가 코드를 보기 전에 결정됐다.

이유 PR 무슨 일이 있었나
이슈에 할당되지 않음 langchain #36867, pydantic-ai #7307 이슈 담당자로 지정되지 않은 외부 기여자의 PR을 봇이 바로 닫았다
먼저 올라온 PR이 있음 pydantic-ai #5627, llama_index #21830, openclaw #68845 같은 이슈를 다루는 PR이 이미 있었다. llama_index는 내가 직접 닫았다
방향 합의가 먼저 marimo #9762, #9806, #10434 이슈에서 접근법을 먼저 합의하라는 답과, 보안상 지원하지 않는 기능이라는 답을 받았다
다른 PR로 해결됨 marimo #10435, headroom #3819 다른 PR이 같은 문제를 먼저 고쳐서 머지됐다

pydantic-ai #7307은 8월 8일 하루 동안 두 봇을 만났다. Macroscope 리뷰 봇은 “한 줄짜리 수정이고 테스트가 있다”며 승인 판정을 냈고, GitHub Actions 봇은 이슈 #7284가 나에게 할당되지 않았다는 이유로 PR을 닫았다. 코드가 괜찮아도 순서를 어기면 닫힌다는 것을 이때 확실히 알았다. langchain 봇의 안내문도 같은 규칙을 말한다. 외부 기여자는 PR을 열기 전에 이슈에서 할당을 요청하고, 할당되면 PR이 자동으로 다시 열린다.

headroom #3819는 코드에서 졌다. pytest 로그를 압축할 때 short test summary info 부분의 중간 FAILED 줄이 사라지는 이슈 #3814를 Rust 코어와 Python 구현 양쪽에서 고쳤고, 기존 테스트와 parity 픽스처는 모두 통과했다. 다음 날 다른 기여자가 같은 이슈로 #3828을 올렸다. 이 PR은 이슈에 적힌 두 방안(실패 줄 보존, 잘린 줄 이름 표시)을 모두 구현했고, CRLF 줄바꿈과 비정상 헤더까지 테스트했다. 협업자(collaborator) JerrettDavis는 내 PR에 두 가지 결함을 남겼다. === diagnostic: short test summary info unavailable처럼 문구만 포함한 줄에서도 요약 부분이 시작된다는 것, 그리고 전체 줄 수 상한이 걸리면 중간 줄이 여전히 이름 없이 버려진다는 것이다. 그리고 그날 #3828을 머지했다. 압축기나 파서처럼 입력이 다양한 코드는 정상 경로만이 아니라 엉뚱한 입력과 상한 경계까지 처음부터 테스트해야 한다는 교훈이 남았다.

marimo에서 연달아 닫힌 PR들도 배운 점이 컸다. marimo는 큰 변경을 PR로 올리기 전에 메인테이너의 승인을 받으라고 요구하고, CLA에 서명하기 전에는 CI가 통과하지 않는다. 첫 PR(#9762)은 연 날 설명 없이 닫혔고, 같은 수정을 다시 올린 #9806은 석 달 뒤 “이슈에서 접근법부터 논의하라”는 답과 함께 닫혔다.

에이전트와 첫 기여를 시작한다면

반년 동안 다듬은 순서를 정리하면 다음과 같다. 초반의 PR들이 닫히면서 하나씩 추가된 항목이고, 머지된 3개는 모두 이 순서가 거의 갖춰진 9월에 연 PR이다.

1. 레포의 AI 정책을 먼저 읽는다. CONTRIBUTING.md, AI_POLICY.md, AGENTS.md, PR 템플릿에서 에이전트 사용을 금지하는지, 공개를 요구하는지 확인한다. 레포마다 차이가 크다.

이번에 기여한 레포에서 직접 겪은 규칙만 추려도 이 정도로 다르다.

레포 겪은 규칙
langchain, pydantic-ai 이슈에 할당된 사람만 PR을 열 수 있고, 아니면 봇이 바로 닫는다
marimo 큰 변경은 이슈에서 메인테이너의 승인을 먼저 받는다. CLA가 필요하다
haystack 전부 AI로 만든 PR에는 짧은 disclaimer를 넣는다. 커뮤니티 기여자는 PR을 한 번에 하나만 열 수 있다
opik PR 본문의 AI-WATERMARK 블록에 사용한 도구·모델·범위를 적는다. CLA가 필요하다
penpot 커밋에 DCO 서명과 :bug: 형식의 메시지가 필요하다

더 엄격한 레포도 있다. polars의 AI_POLICY.md는 에이전트가 레포와 상호작용하는 것을 금지하고, AI로 만든 코드는 accepted 이슈에만 PR로 올릴 수 있게 한다. 그래서 기여 문서는 처음부터 끝까지 읽는다.

2. 이슈를 고를 때 연결된 PR이 있는지 확인한다. 이슈 타임라인에서 PR이 교차 참조된 횟수를 세면 된다. 0이 아니면 다른 사람이 이미 작업 중일 가능성이 높다.

gh api repos/<owner>/<repo>/issues/<num>/timeline \
  -q '[.[]|select(.event=="cross-referenced" and .source.issue.pull_request)]|length'

할당이 필요한 레포라면 이슈에 코멘트로 할당을 요청하고 기다린다. 메인테이너가 직접 올린 이슈는 방향이 분명하고 경쟁이 적어서 첫 기여로 고르기 좋았다. 이슈 작성자가 “PR을 열겠다”고 적었다면 그 사람의 PR 이력을 본다. 이력이 없으면 대개 인사말에 가깝다.

3. 수정 전에 실패하는 테스트를 먼저 만든다. 이슈의 재현 스크립트를 돌려 버그를 확인하고, 새 테스트가 main에서 실패하고 수정 후 통과하는 것을 PR 본문에 적는다. opik 리뷰어가 승인하며 짚은 것이 바로 이 부분이었다. 정상 입력만이 아니라 비정상 입력과 경계 조건도 테스트한다. headroom에서 놓친 부분이다.

4. 이슈 작성자의 제안을 그대로 구현하기 전에 기존 설계와 맞는지 따진다. opik의 1.0 고정은 이슈에서 나온 제안이었지만 기존 점수 체계와 맞지 않았다. 에이전트는 이슈에 적힌 해법을 충실히 구현하는 편이라, 이 질문은 사람이 따로 던져야 했다.

5. PR 템플릿을 빠짐없이 채우고, AI 사용 사실을 레포가 정한 방식으로 밝힌다. opik은 AI-WATERMARK 블록에 도구, 모델, 사용 범위, 사람이 검증한 내용을 적게 한다. CLA 봇이 코멘트를 달면 바로 서명한다.

6. 리뷰는 항목별로 반영하고 커밋 해시를 적어 답한다. 머지된 세 PR은 모두 리뷰 지적을 번호대로 반영하고, 어느 커밋에서 고쳤는지 적어 답했다. 리뷰 봇의 제안이 메인테이너 리뷰와 부딪히면 메인테이너를 따르고, 반영하지 않는 이유를 남긴다.

아직 진행 중인 PR

열려 있는 PR은 5개다. lancedb #4314, llama_index #22611, opentype.js #875는 리뷰를 기다리고 있다. vite #22559는 6월 15일에 수정 요청을 받았는데 내가 7월 29일에야 반영했고, 지금은 재리뷰를 기다린다. redis-vl-python #742도 9월 28일에 받은 인라인 리뷰 5건이 아직 내 차례로 남아 있다. 에이전트가 코드 작성 시간을 줄여 줘도, 리뷰에 답할 시간을 따로 잡지 않으면 PR은 내 쪽에서 멈춘다는 것을 이 두 PR에서 배우고 있다.

정리

반년 동안 올린 PR 18개 중 3개가 머지됐다. 숫자만 보면 작지만, 번역 PR이 기여의 전부였던 나에게는 처음 보는 Python 지표 코드와 SCSS 레이아웃과 defaultdict 부작용을 고쳐 메인테이너의 승인을 받은 경험이었다. 코드베이스를 읽고, 재현하고, 레포 도구를 맞추고, 테스트를 쓰는 일은 에이전트와 나눠서 할 수 있게 됐다. 남은 일은 레포 규칙을 먼저 읽는 것, 이슈의 제안을 기존 설계에 비춰 보는 것, 그리고 리뷰에 끝까지 답하는 것이었다. 다음은 redis-vl-python의 리뷰 5건에 답하고, PR이 머지돼 다시 PR을 열 수 있게 된 haystack에서 다음 이슈를 고를 차례다.