나의 첫 오픈소스 기여

벌벌 떨며 단 코멘트 하나에서 머지까지

Overview

학부생 시절을 포함하여 개발을 시작한지 4년만에 처음으로 오픈소스에 기여를 했다. 이 글은 그 과정에서 깨진 선입견에 대한 것이다. 오픈소스 기여는 실력이 엄청난 개발자들의 영역이라고 생각했는데, 직접 부딪혀보니 그 자리에 실제로 있었던 건 다른 것들이었다.

멀게만 느껴졌던 세계

솔직히 오픈소스에 그렇게 관심이 있진 않았다. 전세계 수많은 개발자들이 쓰는 라이브러리에 기여한다는 건 분명 멋있는 행위였지만, 나에겐 너무 동떨어진 세계라고 느꼈으며 실력이 엄청나게 좋아야 할 수 있다는 선입견이 있었기 때문이다.

그런 생각에 균열을 낸 친구가 한 명 있다. 이 친구가 자주 하던 말 중 하나가 "개발자로서 비영리적으로 세상에 도움을 주고 싶다"였다. 처음 들었을 때 그 말이 참 멋있다고 생각했고, 그 친구는 그 말을 증명하듯 꾸준히 오픈소스 활동을 이어갔으며 결국 메인테이너 자리까지 올라갔다. 그 과정을 옆에서 지켜보며 받은 영향이 컸다. 나도 오픈소스라는 걸 조금 더 진지하게 고민해보고 싶어졌다.

그래도 의문은 남아 있었다. 내 실력으로 저 세계에 들어갈 수 있긴 한 건가.

어디에 기여할 것인가

막상 해보려고 마음을 먹고 나니 어떤 오픈소스를 골라야 할지 몰랐다. 일단 내가 직접 써본 것들 위주로 여러 라이브러리를 후보에 두고 에이전트와 함께 분석해봤다. 언어나 프레임워크의 코어급 프로젝트들은 엄두가 안 났고, 내가 코드를 읽고 소화할 수 있는 규모인지, 고친 결과가 화면에서 바로 보이는 도구인지를 기준으로 추렸다. 그렇게 좁히다가 발견한 게 Storybook이었다. 컨트리뷰션 가이드가 잘 정리되어 있었고, 기여 문화도 활발하게 돌아가는 듯했다.

친구가 알려준 꿀팁 중 하나가 "이슈 라벨이 알록달록하면 활발한 오픈소스"라는 거였는데, 이슈 목록에 들어가 보니 그 말대로였다. 라벨이 다양했고, 최근 이슈까지 꾸준히 관리되고 있었다. 그렇게 Storybook으로 정하고 이슈를 찾기 시작했다.

라이브러리를 정하는 것까지는 꽤 순탄했지만, 이슈를 고르는 과정은 그렇지 않았다. 후보 몇 개를 추려서 보니 이미 원인 분석을 끝낸 사람이 있거나, 경쟁이 붙었거나, 곧 닫힐 예정인 것들이 걸러졌다. 그렇게 남은 게 #32106이었다. 아무도 클레임하지 않았고, 범위가 좁고, 재현 링크가 있고, 무엇보다 이틀 전에 메인테이너가 재트리아지를 했다. 살아있는 이슈라는 신호였다.

이슈 하나의 1년사

이슈를 잡기 전에 타임라인을 전부 조사해봤는데, 이 이슈에는 1년치의 역사가 쌓여 있었다.

2025년 7월에 제기된 이 이슈에는 이미 죽은 PR이 두 개나 있었다. 하나는 메인테이너가 실험 삼아 Copilot 코딩 에이전트에게 맡겨 자동 생성된 PR이었는데, 원인 분석이 빗나간 채로 두 달간 방치되다 폐기됐다. 다른 하나는 코드가 거의 정답에 가까웠는데도 리뷰 시그널을 받지 못해 열흘 만에 작성자가 스스로 닫아버렸다.

두 번째 PR은 특히 인상 깊었다. 코드가 좋다고 해서 머지되는 게 아니었다. 메인테이너의 눈에 닿지 못한 PR은 그냥 조용히 죽는다. 그래서 나는 코드를 올리기 전에 먼저 이슈에 코멘트를 달아 시그널을 확보하기로 했다.

그 코멘트 하나 다는 게 생각보다 무서웠다. 아는 사람은 한 명도 없고, 나를 제외하면 평생 볼 일도 거의 없을 외국인들만 있는 곳에 짧은 영어 문장 하나를 다는 건데 손이 벌벌 떨렸다. 문장을 몇 번이나 고쳐 쓰다가 결국 친구한테 보여주고 평가와 수정안까지 받고 나서야 겨우 등록 버튼을 눌렀다.

모르져 존

그렇게 다듬어서 등록한 나의 첫 코멘트다.

덜덜덜덜

지금 다시 보면 별것도 아닌 두 문장인데, 그때는 이게 그렇게 큰 용기가 필요한 일이었다.

그리고 여기까지 오는 동안 코드는 한 줄도 짜지 않았다. 살아있는 이슈를 고르고, 1년치 히스토리를 읽고, 댓글 하나를 다듬는 일. 기여의 절반은 사실 이런 것들이었다.

무엇을 고쳤나

버그 자체를 짧게 정리하면 이렇다. React/Vite/TS 환경에서 'red' | 'blue' | null 같은 nullable 유니온 prop을 쓰면, 컨트롤 패널에서 null이 옵션에 표시되지 않는다는 리포트였다.

그런데 파고들어보니 실체는 더 컸다. Storybook은 소스 코드를 파싱해 prop 타입을 추출한 뒤, 유니온의 요소가 전부 리터럴이면 enum으로 분류해서 라디오나 셀렉트 컨트롤을 만들어준다. 문제는 null이 문법상 리터럴이 아니라 별도의 키워드 타입으로 파싱된다는 점이었다.

{
  "name": "union",
  "raw": "'red' | 'blue' | null",
  "elements": [
    { "name": "literal", "value": "'red'" },
    { "name": "literal", "value": "'blue'" },
    { "name": "null" }
  ]
}

"전부 리터럴이면 enum"이라는 판정이 null 하나 때문에 통째로 무산되고, enum 분류에 실패하면 옵션이 생성되는 경로 자체를 못 탄다. 즉 증상은 "null이 옵션에서 빠짐"이지만 실제로는 null 때문에 red, blue까지 전부 죽는 버그였다.

재밌는 건 undefined는 멀쩡했다는 점이다. 과거에 누군가 같은 문제를 undefined에 대해서만 고치고 null을 빠뜨린 것이었다. 같은 성격의 문제가 절반만 고쳐진 채 1년을 버틴 셈이다.

수정은 판정 규칙을 "전부 리터럴 또는 null, 단 리터럴 최소 1개"로 바꾸는 것이었다.

const isLiteralEnum =
  nonUndefinedElements.some(isLiteral) &&
  nonUndefinedElements.every((element) => isLiteral(element) || isNull(element));

nonUndefinedElements는 유니온 요소에서 undefined를 미리 걸러낸 나머지다. some(isLiteral) 조건을 넣은 이유는, 이게 없으면 string | null처럼 자유 입력이어야 할 타입까지 enum으로 오분류되기 때문이다. 그리고 옵션에 들어가는 값은 문자열 'null'이 아니라 진짜 null이어야 했다. 문자열이면 컴포넌트에 backgroundColor="null"이 전달되는 또 다른 버그가 된다.

이렇게 정리하고 보면 수정의 본체는 몇 줄 되지 않는다. 시간이 들어간 곳은 따로 있었다. react-docgen이 실제로 뭘 내보내는지 직접 실측하고, 왜 undefined는 멀쩡한지 파고들고, 수정이 영향을 주는 다운스트림 코드를 전부 따라가 보던 시간이다. 엄청난 실력이 필요할 거라 생각했던 자리에 실제로 있었던 건 꼼꼼한 관찰이었다.

Amazingly elegant

그렇게 인생 첫 PR을 올렸다. 일요일이었다.

처음으로 올린 PR

그리고 월요일, 회사에서 일하다가 메일 알림이 하나 떴다.

엘레강트

Amazingly elegant contribution! Thanks! :)

솔직히 좀 놀랐다. 제출한 지 하루도 안 돼서 메인테이너가 라벨을 붙이고, 승인하고, 머지까지 한 번에 처리한 것이다. 1년 넘게 열려 있던, PR 두 개가 죽어나간 이슈가 그렇게 닫혔다.

며칠 뒤에는 또 알림이 떠서 들어가봤더니 10.6.0-alpha.4 릴리즈 노트에 내 PR이 포함되어 있었고, 메인테이너가 고맙다고 태그까지 걸어줬다.

solp

내가 고친 코드가 전 세계 개발자들이 쓰는 라이브러리에 실려 나간다는 게 아직도 좀 신기하다.

실력은 언제 쌓이는가

이번 기여에서 에이전트의 도움을 정말 많이 받았다. 오픈소스 작업용 워크스페이스를 따로 만들고, 기여 한 건마다 이슈 분석부터 배운 것까지 문서로 남기며 진행했다. 다만 역할은 나눠뒀다. 다운스트림 코드를 전수 추적하는 것처럼 손이 많이 가는 일은 에이전트에게 맡기고, react-docgen 실측과 봇 분석 검증은 직접 했다. 코멘트나 PR 같은 대외 행동도 반드시 내가 결정해서 하기로 했다. 그리고 원칙이 하나 더 있었다. 단순히 머지만을 목표로 하지 않는 것. 고치는 과정에서 배울 수 있는 걸 제대로 배우자는 거였다.

실제로 이 작은 수정 하나에서 꽤 많은 걸 건졌다. PR을 올리기 전 셀프 리뷰에서는 통과하던 테스트가 실제로는 아무것도 검증하지 못하고 있었다는 걸 발견했고, 다른 모델로 교차 리뷰를 돌려보니 TypeScript의 Extractnever로 무너지는 함정도 걸려 나왔다. 이슈에 달려 있던 AI 봇의 자동 분석이 파일은 맞게 짚었지만 메커니즘은 틀리게 설명하고 있었다는 것도 직접 실측해보고 나서야 알았다. 검증 없이 봇 말대로 갔다면 엉뚱한 수정을 했을 것이다. 실제로 그렇게 죽은 PR을 타임라인에서 이미 봤으니까.

그러다 문득 처음의 선입견이 떠올랐다. 나는 실력을 다 갖춘 다음에야 기여할 수 있다고 믿었는데, 순서가 반대였다. 기여하는 과정에서 실력이 쌓이고 있었다. 자격 같은 건 애초에 없었고, 들어가서 부딪히는 만큼 배우는 세계였다.

머지 이후

멀게만 보이던 세계는 별다른 게 없었다. 1년 된 이슈를 다시 분류하고, 낯선 사람의 첫 PR에 하루 만에 답을 주는 사람들이 있을 뿐이었다.

돌아보면 이번 기여는 "개발자로서 비영리적으로 세상에 도움을 주고 싶다"던 친구의 말 한마디에서 시작됐다. 기여 과정 내내 참 많이도 물어봤다. 코멘트 이렇게 달아도 되냐, PR 이런 식으로 올려도 되냐. 사소한 질문에 매번 답해준 덕분에 여기까지 올 수 있었다.

다음 PR은 이미 리뷰 대기 중이다.