
"Good First Issue"는 오픈 소스 프로젝트에서 초보 기여자가 쉽게 참여할 수 있도록 표시된 이슈로, 문서 작성이나 테스트 추가 등 진입 장벽이 낮은 작업인 경우가 대부분이다.
오늘은 오픈소스 기여에 관한 내 경험을 끄적여보겠다.
Github 공개 레포지토리에 기여하는 과정은 프로젝트마다 다르겠지만 대략적으로
이슈 -> 레포지토리 포크 -> 코드 수정 -> PR
이정도이다. 당신이 Git과 Github 조금만 능숙하게 다룬다면 이런 원론적인 설명은 이제 지겨울 정도로 쉽게 느껴질 것이다.
필자와 같은 뉴비 개발자들이 이름 있는 오픈소스에 기여해보겠답시고 무작정 이슈나 PR들을 살펴보면, 프로필에 10+년어치의 잔디가 누적되어있는 시니어 개발자들이 있는지도 몰랐던 기능에 대한 이슈에서 토론하는 경우가 다반사이다. 그것도 영어로.
경험상 good first issue 라벨이 붙은 이슈들이 진입 장벽이 낮아서 그런가, 경쟁률이 높아서 역설적으로 더 기여하기 힘들었다.
분명 오픈 소스인데 오픈이 아닌 것 같았다
수많은 개발자들이 보는 곳에서 전세계 1%도 쓸까 말까 한 한국어를 쓰겠다는게 좀 이기적인 심보인 것도 맞으나, 호기롭게 들어간 이슈창에서 당장 눈 앞에 언어라고는 영어밖에 안보인다면 막막할 수밖에 없다는게 사실이다. Git과 Github조차도 영어만을 지원하는 서비스이다.
당장 3년 전, 브라우저 번역이나 구글 번역기에 의존하던 우리에게 이런 상황이 닥친다면 우리는 돌아서야만 했을 것이다. 하지만??? 우리 손에는 LLM이라는 흉기가 들려있지 않은가. 필자는 Perplexity나 ChatGPT에게 링크를 주고 요약을 시키거나, 아얘 Comet나 DIA같은 AI 기반 브라우저를 활용하는 방법을 선택했다. 이 방법은 영어로 된 공식 문서나 블로그를 읽을 때도 유용하게 쓰일 수 있다.
우리가 많이 쓰는 개발 관련 용어 대부분은 이미 영어이고, 소통을 중요시하는 개발자들이 자신이 개발한 기능이나 발견한 이슈를 설명할 때 수능 지문처럼 해괴한 문장을 쓰지는 않는다. 관련 기술에 대해 공부가 어느정도 되어 있다면 LLM을 쓰지 않아도 어느정도 직독직해가 되는 경우도 많다.
여기서 '익숙해지기'는 두 가지 의미를 내포한다.
1번이 안되면 대부분 2번도 힘들다. 때문에 자신이 직접 사용해 본 프로젝트에 기여하는 것을 절대적으로 추천한다. 그게 더 목적성도 뚜렷하고, 코드를 보고 '이 기능이 이렇게 구현되어있구나' 하며 인사이트를 얻기도 수월하다고 생각한다.
이 과정에서 개인적으로 프로젝트에서 패키지를 사용할 때 굉장히 편협하다는 것을 체감했다. 약간만 큰 패키지가 되어도 우리가 빈번히 사용하는 것보다 훨씬 많은 기능을 제공하는 경우가 대부분이다. 자주 쓰는 패키지도 이정도인데, 한번도 안 써본 패키지의 코드를 보면 이해하기 몇 배는 힘겨울 것이다.
필자는 종종 관심 있는 패키지의 코드를 직접 보며 공부하기도 한다. 실제로 React나 Next.js 코드를 보고 많은 것을 배웠던 경험이 있기도 했고, 책이나 포스팅만을 보고는 뭔가 궁금증이 다 해소되지 않는 찝찝한 느낌도 있기 때문인데... 이렇게 패키지의 코드를 눈에 익힌 것이 기여하는 것에 도움이 된 것 같기도 하다.
영어에 자신감이 생겼고 프로젝트에도 익숙하다면 이제 실제로 기여할 이슈를 찾으면 된다. 이슈를 직접 생성해도 되는데, 라이브러리를 사용하다가 버그를 찾고 '이거 이슈 넣어야겠다!' 라고 생각하면 대부분 알고보니 버그가 아니였거나, 이미 같은 내용의 이슈가 있거나 둘 중 하나였다. 경험이 쌓이다보면 언젠가 할 수 있지 않을까?
이슈 발견
typescript-eslint라는 패키지를 둘러보다가 이와 같은 이슈를 발견했다. 이슈 링크

문제 정리
이슈 작성자가 Playground와 재현 코드를 제공해줘서 문제를 쉽게 이해할 수 있었다.
다음과 같이 no-floating-promises라는 규칙의 예외로 "randomAsyncFunction"이라는 함수를 지정했는데
"rules": {
"@typescript-eslint/no-floating-promises": [
"error",
{
"allowForKnownSafeCalls": [
"randomAsyncFunction"
]
}
]
}
지정하지 않았을 때와 동일하게 Promise를 처리하라고 나온다.

코드에서 유추할 수 있지만, no-floating-promises는 Promise를 생성만 하고 결과나 에러를 처리하지 않으면(floating, 떠 있다고 표현하는듯) 경고하는 규칙이다. Promise가 rejected 상태가 될 때 에러가 무시될 수 있기 때문이다.
AS IS: 기존 코드 확인
여기까지 확인하고 no-floating-promises가 구현된 부분을 찾아가서 확인해보았다. 파일명이 no-floating-promises.ts로 동일해서 찾기 수월했다.
function isKnownSafePromiseReturn(node: TSESTree.Node): boolean {
if (node.type !== AST_NODE_TYPES.CallExpression) {
return false;
}
const type = services.getTypeAtLocation(node.callee);
return typeMatchesSomeSpecifier(
type,
allowForKnownSafeCalls,
services.program,
);
}
typeMatchesSomeSpecifier 함수로 타입 매칭은 확인하고 있는데 이름 확인 관련 로직은 전혀 찾을 수 없었다.
이름 매칭용 함수를 만들어서 도입할 계획을 세우고 typeMatchesSomeSpecifier 함수 구현을 참고하려고 보고 있었다.

그런데 아니나 다를까 같은 파일 맨 마지막에 이름 매칭용으로 보이는 함수가 이미 구현되어 있었다. Yukihiro Hasegawa님 감사합니다...

TO BE: 지향점 구현
이게 내가 원하는 함수가 아닐 수도 있기 때문에, 일단 테스트코드를 먼저 작성하고 커밋했다.
우선 이슈 작성자분이 Playground에 작성한 코드를 그대로 케이스로 추가했고, arrow 함수를 일반 함수로 바꾸고 값을 String으로 바꿔서 유사한 테스트 케이스를 하나 더 만들었다. 테스트는 다다익선이라고 생각한다.
다음으로 실질적인 코드 수정을 했다.
function isKnownSafePromiseCall(node: TSESTree.Node): boolean {
if (node.type !== AST_NODE_TYPES.CallExpression) {
return false;
}
const type = services.getTypeAtLocation(node.callee);
// 추가한 부분!!
if (
valueMatchesSomeSpecifier(
node.callee,
allowForKnownSafeCalls,
services.program,
type,
)
) {
return true;
}
return typeMatchesSomeSpecifier(
type,
allowForKnownSafeCalls,
services.program,
);
}
이름 매칭용으로 추정했던 함수인 valueMatchesSomeSpecifier를 사용해서 if절을 하나 추가했다. Type으로 추상화가 잘 되어 있어서 어렵지 않았다.
여기까지 구현한 뒤 테스트를 돌렸다. 테스트 명령어같은건CONTRIBUTING.md라는 파일에 대부분 나와있다.
한가지 실수는 터미널에
yarn nx test ast-spec이렇게 쳐서 전체를 대상으로 테스트를 돌렸더니...
필자의 소중한 M4 pro 칩과 24GB 메모리가 약간 힘들어 보였다. FE 개발을 하면서 에뮬레이터 돌릴 때 제외하고 처음 보는 광경이였다.
첫 테스트 결과에서 실패가 떠서 확인해보니 Snapshot 갱신이 안되서 그런 것 같았다.
Snapshots 8 failed
Test Files 1 failed | 4 passed (5)
Tests 8 failed | 4134 passed | 7080 skipped (11222)
Type Errors no errors
Start at 21:22:31
Duration 3.48s (transform 339ms, setup 524ms, collect 1.03s, tests 1.37s, environment 0ms, prepare 63
ms, typecheck 766ms)
yarn nx test ast-spec --update-snapshots 이 명령어로 Snapshot을 갱신하며 돌려보니 새로 추가한 테스트 케이스까지 모두 시원하게 통과했다.

Commit
총 3개로 나눠 커밋했다. 커밋 메시지는 LLM에게 변경사항과 컨벤션을 주고 쓰라고 시켰다... 이 블로그에 임시저장하며 계속 기록하고 있었기에 프롬프트 만들기도 편했다.

첫번째가 테스트 케이스 작성 후 바로 한 커밋이다. 그리고 남은 변경사항이 내가 변경한 코드와 스냅샷 변경사항이였는데, 스냅샷 변경사항도 올려야 할 지 뭔가 애매해서 (CI 명령어에 스냅샷 업데이트가 포함되어 있을 것이라고 추측했었다) 두번째와 세번째로 나눠서 올렸다.
여기까지 작업하고 내가 포크한 레포지토리에 Push했다.
PR 작성 전에 내가 본 이슈에 PR을 올려도 된다는 라벨이 있는 지 봐야 한다. 라벨 이름은 프로젝트마다 다르니까 되도록 CONTRIBUTING.md와 같은 문서를 확인하자. 간혹 마일스톤으로 표시하는 경우도 있다.

만약 PR을 올려도 된다는 별다른 표식이 없으면 이런 식으로 PR을 올려도 되는지 확인받는 코멘트를 남기고 답장이나 이모지로 확인을 받은 뒤 작업하면 좋을 것 같다.

PR 작성하기
이 프로젝트는 PR 템플릿과 컨트리뷰트 가이드 문서가 제공되었기에 작성하는데 큰 어려움은 없었다. 템플릿에 있는 체크리스트를 확인하고 무슨 문제를 어떻게 고쳤는 지 설명했다.
한 가지 신경 쓴 것은, 앞서 언급했듯 스냅샷 변경 사항을 마지막 커밋으로 올렸는데 만약 이게 올리면 안 되는 것이였다면 메인테이너가 빠르게 확인 후 reset할 수 있도록 각 커밋마다 작업 내용을 추가로 명시했다는 점이다. 좀 쫄렸다.

그리고 컨트리뷰트 가이드 문서에 상당히 귀여운 요청이 있길래 이것도 반영했다 ㅋㅋㅋㅋ

CI 실패??
분명 테스트 성공 확인하고 올렸는데 냅다 실패 뜨길래 다급하게 확인했다.

그런데 main 브랜치도 테스트가 실패한 상태로 있길래 다른 분들 PR을 확인해봤는데

CI 명령어에 무슨 문제가 있는 것이였다. 몇시간을 혼란스러워하며 삽질했었는데 이래도 되나 싶었다...

며칠 후 저 문제가 고쳐진 것을 확인하고 나서 main 브랜치를 pull받고 commit하니 해결되었다.

PR을 올린 날은 7월 28일(필자의 생일이었다)이고 CI 실패를 해결한게 8월 초였다. 그 무렵까지만 해도 혹시 코멘트가 달렸나 싶어 생각날 때마다 깃허브 알림을 확인하곤 했다. 그러던 중 나보다 늦게 올린 PR이 승인되고 머지되는 것을 보며 묻힌 게 아닐까 하는 생각이 들던 찰나,

드디어 어프루브되었다. PR 업로드 후 자그마치 39일이 지난 9월 4일이었다.
프로젝트 메인테이너분들도 각자의 생업이 있는 만큼 성급하게 재촉하지 말라는 글을 어디선가 본 기억이 있다. 실제로 규모가 큰 오픈 소스 프로젝트일수록 PR 검토가 오래 걸리는 경우가 잦은 듯하다. 그러니 해외직구 상품을 기다리는 마음처럼 잠시 잊고 여유 있게 기다려보는 것이 좋겠다.
솔직히 이번 기여의 경우 약간 운이 좋았다고 생각한다. 하지만 기회는 준비된 자에게만 오는 법이라고 했던가.

9월 8일에 릴리즈된 8.43.0 버전에 수줍게 이름을 올렸다.

Playground에서 같은 조건으로 테스트해본 결과 성공하는 모습이다...
코드 몇십 줄 쓰고 얻을 수 있는 최고의 뿌듯함을 얻은 것 같다.