토스 오픈소스 도전기

이지호·2025년 6월 25일
37
post-thumbnail

지난 이야기

(지난 포스팅)
앞으로의 계획을 세워봤다: 오픈소스
큰 코드베이스를 이해하고 직접 기능까지 붙여본 건 이번이 처음이었다. 큰 코드를 읽어본 경험이 없다보니 처음에 많이 버벅였는데, 코드 읽는 훈련을 미리 해두면 좋겠다는 생각이 들었다. 커피챗에서도 많이 추천받은 만큼, 반드시 오픈소스 활동을 시작해보려고 한다.

이전 포스팅에서 인턴 회고 글을 작성하며, 위와 같이 오픈소스 계획을 세웠었다. 이번에는 오픈소스에 냅다 도전해본 후기를 적어본다.

왜 오픈소스 기여를 해보려 하는가?

인턴을 하면서 우와했던 순간이 있었다. 동료가 업무 중 사용하던 라이브러리에서 패닉 방지를 위한 추가 코드를 작성해 PR을 올리는 거였다. 나는 매번 내가 하는 업무에만 매달려 사용 중인 다른 것들을 돌아볼 겨를도 없이 달렸는데, 라이브러리 자체를 뜯어보고 개선까지 하는 모습을 보며 '이게 진짜 개발자구나' 하는 생각이 들었다.

그 사건 이후 우리 팀을 다시 돌아보니 다들 아무렇지 않게 오픈소스 기여를 하고 있었다. 버그 수정이든 문서 수정이든 상관없이 말이다.

이전까지 나에게 오픈소스 기여는 엄청난 각오와 준비가 필요한 '프로젝트'였다. 주말 하루를 온전히 비워두고, 커피 한 잔 딱 가져온 다음 '자, 오늘은 오픈소스 기여하는 날이야!' 하고 시작해야할 것 같은 느낌이랄까. 그런데 이 사람들에겐 오픈소스가 일상이었다. 나도 이런 자연스러운 기여를 일상으로 만들고 싶다는 욕심이 생겼다.

더 현실적으로는, 오픈소스 기여가 좋은 훈련이 될 것 같았다. 인턴을 하면서 어려웠던 것 중 하나가 거대한 코드베이스에서 내가 건드려야 할 부분을 찾아내는 것이었다. 어디서부터 시작해야 할지, 어떤 파일을 봐야 할지 막막했기 때문이다. 그러니 평소에 오픈소스 프로젝트들을 뜯어보고 기여하는 경험을 쌓으면, 나중에 새로운 회사에 가서도 빠르게 적응하는 데에 도움이 될 거라 생각했다.

오픈소스 고르기

무작정 '오픈소스 기여할래!' 하고 시작했으나, 어떤 오픈소스를 선택해야할지 막막했다.

일단 가장 먼저 떠올린 건 이전에 사용해본 오픈소스를 둘러보는 것이었다. 예전에 써봤던 mcp-go를 들여다봤는데, 그냥 코드만 봐서는 뭘 고쳐야 할지 전혀 감이 안 왔다.

그러던 중 'good first issue'를 찾아보라던 조언이 떠올랐다.

good first issue란?

오픈소스 프로젝트에서 초보자나 처음 기여하는 사람들을 위한 이슈에 'good first issue' 라벨을 붙여준다.
good first issue

실제로 이슈 탭을 들어가보니, good first issue 라벨이 달린 이슈들이 있었다!

good first issue

그 중 하나를 골라서 확인해보니, 아래와 같이 docs 메인 화면에 아이콘이 찌그러지는 문제가 발생하고 있었다.

issue

이거다 싶어서 도전을 시작했다! 하지만 아쉽게도 css 순서가 마음처럼 잘 조절되지 않았다. 며칠 동안 짬을 내 도전해보았지만, 도중에 다른 분이 먼저 깔끔하게 해결해주셔서 놓치고 말았다. (역시 세상엔 고수가 많다)

그래서 마음 가볍게 다른 레포를 더 둘러보기로 했다. 그러다 문득 최근에 지인이 토스 오픈소스에 기여해봤다는 이야기가 떠올랐다. 그래, 이번엔 토스로 가보자.

'토스 오픈소스'로 검색하니 slash가 나왔는데, 더이상 유지보수는 안 되고 있었다. slash에 기여를 원한다면 slash 대신 es-hangul, es-toolkit, suspensive, use-funnel 패키지에 기여해달라는 안내가 있었다.

그중에서 es-hangul을 클릭해 들어가 봤는데, 이게 꽤 재밌어 보였다. 고등학교 시절의 국어 공부가 떠오르는 테스트 케이스들이 있어서 자꾸 더 보게 되었다.

test code

(고등학교 국어 시간이 생각나는 테스트 코드ㅎㅎ)

기여 과정

기여할 지점 찾기

아쉽게도 es-hangul에는 'good first issue' 라벨이 달린 이슈는 없었다. 대신 코드를 둘러보며 기여할 지점을 찾아봤다.

한참을 코드를 둘러봐도 도저히 개선점을 찾기가 어려워서 문서 쪽으로 눈을 돌렸다. 처음엔 정말 뭘 고쳐야 할지 모르겠어서, contribute 가이드의 영어 부분이라도 한국어로 번역해볼까 생각했을 정도였다.

contribution guide
(컨트리뷰션 가이드 문서의 한 문장만 영어길래, 이거라도 번역해서 올려볼까 했었다...ㅎ 하지만 이건 너무 쪼잔한 것 같은 기분이 들어 포기했다.)

이후 가이드 문서에서 es-hangul 소개 문서로 눈을 돌렸다. 문서 페이지를 차근차근 읽어나가던 중, 문득 이상한 지점을 발견했다. 분명히 표가 있어야 할 자리에 아무것도 표시되지 않고 있었다..!

empty table

(앗싸 발견)

버그 발생 원인 찾기

분명 이전에는 정상적으로 작동했을 텐데, 어느 순간부터 동작하지 않고 있는 것 같았다. 중간에 어떤 변경이 있었던 모양이다.

구체적인 원인을 찾기 위해 이전 PR들을 살펴보았다. 특히 이때 VS Code의 git blame extension이 정말 유용했다. 해당 라인에 커서만 두면 회색으로 최신 PR 정보가 뜬다.

git blame

회색 글씨에 커서를 올리면 팝업으로 PR 내용도 보여주고, 해당 PR로 바로 갈 수 있는 링크까지 있어서 정말 편하다. (PR을 따라가며 히스토리를 파악할 때 유용한 익스텐션이라 추천한다.)

위의 그림에 나타난 'docs: es-hangul의 신뢰성을 나타낼 수 있는 문서를 만듭니다 #302'라는 제목의 PR을 타고 들어가봤다. 이 당시에는 아래처럼 표가 잘 나오고 있었다.

이전 PR

그렇다면 왜 이런 문제가 발생한걸까? 표를 만드는 코드를 살펴보자.

const filterValidFileEntries = (coverageFileEntries: typeof fileEntries) => {
  return Object.entries(coverageFileEntries)
    .filter(([filePath]) => isValidFilePath(filePath))
    .flatMap(([filePath, info]) => {
    const filename = extractFileName(filePath);

    return filename != null ? [[filename, info] as const] : [];
  });
};

coverageFileEntries라는 게 뭘까?

코드를 타고 올라가니, 이건 coverage summary 파일이라는 걸 알게 되었다.

import coverageJSON from '../../../../../coverage/coverage-summary.json';

오. 이 coverage-summary.json은 어떻게 생기는걸까?

이 궁금증은 PR을 다시 읽어보면서 알 수 있었다. yarn test를 실행할 때 자동으로 만들어지는 파일이었다!

coverage-summary json

파일은 아래와 같은 구조로 이루어져 있었다. 말 그대로 커버리지 내용이 요약된 json 파일이었다.

{
  "/Users/jiho/Workspace/es-hangul/src/core/canBeJongseong/canBeJongseong.ts": {
    "lines": {
      "total": 1,
      "covered": 1,
      "skipped": 0,
      "pct": 100
    },
    "functions": {
      "total": 1,
      "covered": 1,
      "skipped": 0,
      "pct": 100
    },
    "statements": {
      "total": 1,
      "covered": 1,
      "skipped": 0,
      "pct": 100
    },
    "branches": {
      "total": 0,
      "covered": 0,
      "skipped": 0,
      "pct": 100
    }
  },
  "/Users/jiho/Workspace/es-hangul/src/core/canBeJungseong/canBeJungseong.ts": {
    "lines": {
      "total": 5,
      "covered": 5,
      "skipped": 0,
      "pct": 100
    },
    "functions": {
      "total": 1,
      "covered": 1,
      "skipped": 0,
      "pct": 100
    },
    "statements": {
      "total": 5,
      "covered": 5,
      "skipped": 0,
      "pct": 100
    },
    "branches": {
      "total": 4,
      "covered": 4,
      "skipped": 0,
      "pct": 100
    }
  },
    ...
}

그렇군. 그럼 이제 다시 코드로 돌아가 isValidFilePath를 살펴보자.

const isValidFilePath = (filePath: string): boolean => {
  // src 뒤 2-depth까지의 경로를 필터링하며, anything.something.ts 등의 명칭을 가진 파일들은 반환되지 않도록 `.ts`로 끝나되 추가 점(`.`)이 없는 경우만 허용
  const regex = /\/src\/[^/]+\/[^/]+(?<!\..+)\.ts$/;

  return regex.test(filePath) && !filePath.endsWith('constants.ts') && !filePath.includes('_internal');
};

해당 코드에서 눈에 띄는 부분이 있는데, '2-depth'까지 허용한다는 점이었다. 정규식을 분석해보자.

  • /src/ - src 디렉토리
  • [^/]+/ - 첫 번째 depth + 슬래시 (슬래시가 아닌 문자들 + /)
  • [^/]+ - 두 번째 depth (슬래시가 아닌 문자들)
  • (?<!\..+) - negative lookbehind: 점(.)이 여러 개 있는 경우를 제외
  • \.ts$ - .ts로 끝나는 파일

다시 말해 아래와 같은 디렉토리는 허용된다.

src/core/canBeJongseong.ts     ✅ (2-depth: core -> canBeJongseong.ts)

그리고, 아래와 같은 디렉토리는 정규식에 위반된다.

src/core/canBeJongseong/canBeJongseong.ts  ❌ (3-depth: core -> canBeJongseong -> canBeJongseong.ts)

근데 내 json 파일을 다시 보니, src/core/canBeJongseong/canBeJongseong.ts으로, src 아래에 3-depth가 구성되어 있었다! 그래서 정규식에 걸렸고, 표가 비어버리고 만 것이었다.

그럼 그 때엔 왜 2-depth라고 코드를 짰을까? 해당 PR이 올라왔던 시점의 src 아래의 코드를 살펴보니 아래와 같았다. 즉, 카테고리 경로(core 등)가 없고, 바로 canBeJongseong같은 디렉토리로 구성되어 있었다. 즉, 이 당시에는 2-depth였던 것이다!

과거

이 정규식만 3depth로 고쳐주고 연관 코드를 조금만 정리하면 문제를 해결할 수 있으리라.

fork부터 시작

지금까지 팀 프로젝트에서는 로컬에서 브랜치를 만들고 작업한 다음 바로 레포에 푸시했는데, 오픈소스는 다르다. 해당 레포에 대한 권한이 없기 때문에 다른 방식으로 접근해야 한다.

오픈소스 기여의 기본 워크플로우는 다음과 같다.

  1. 내 계정으로 fork를 받는다
  2. 포크한 내 레포에서 작업을 완료한다
  3. 원본 레포로 PR(Pull Request)을 날린다

이때 원본 레포(toss/es-hangul)를 upstream이라고 부르고, 포크한 레포(wlgh1553/es-hangul)를 origin이라 부른다.

git graph
(이미지 출처)

PR 작성하기

버그 발생 히스토리를 확인하면서 잘 작성된 PR 덕분에 문제 추적에 어려움이 없었다. 나의 PR 또한 나중에 누군가에게 도움이 되길 바라며 신경 써서 작성했다.

PR 내용에는 문제 상황, 원인, 해결 방법, 결과를 최대한 간결하고 명료하게 담으려고 했다. 특히 UI 버그 수정이었기 때문에 이미지와 동영상을 첨부해서 이해하기 쉽게 만들었다. (PR 링크)

여담으로, UI 설명할 때 매번 캡처하고 이미지 편집하는 게 번거로웠는데 shottr이라는 도구 덕에 빠르게 해낼 수 있었다. shottr은 cmd+shift+2로 부분 캡처한 다음 바로 편집까지 할 수 있어서 아주 간편하다. (광고 아닙니다..) (이 링크에서 알게 된 도구이다.)

그리고 최종적으로 잘 머지됐다!

merge

그리고 컨트리뷰터 딱지도 달았다. (뿌듯하다)

contributor

앞으로

오픈소스, 막상 해보니 별 거 아니네! 들인 시간 대비 뿌듯함이 컸던 경험이라 이렇게 포스팅으로까지 남기게 되었다. 오픈소스 도전에 관심있으신 분들에게 용기가 되는 글이었길.

하면 되네요

이번 경험을 통해 더 유의미한 기여를 해보고 싶다는 욕심도 생겼다. 지금처럼 docs 중심의 기여도 좋지만, 결국엔 큰 코드베이스에 풍덩 뛰어들어보면서 성장하는 것이니까 말이다.

그런 점에서 OSSCA라는 프로그램이 눈에 띄어 도전하게 되었다. 멘토와 멘티를 매칭해서 특정 레포에 기여해보는 프로그램인데, 정말 재밌어 보인다. 매칭되어 활동할 수 있게 된다면, 그때 다시 오픈소스 관련 포스팅으로 돌아오겠다.

13개의 댓글

comment-user-thumbnail
2025년 6월 28일

글 잘 읽었습니다! 저도 최근 오픈소스 기여에 관심이 생겼는데 많은 도움이 되었습니다 🍀
제목보고 OSSCA가 생각났는데 지원하셨군요!! 무슨 프로젝트 지원하셨는지 궁금해지네용

1개의 답글
comment-user-thumbnail
2025년 6월 28일

오픈소스 기여 항상 도움이 많이 된다고 생각합니다! 재밌게 읽었어요!

1개의 답글
comment-user-thumbnail
2025년 6월 30일

대지호님 토스 오픈 소스 기여까지 이지하게 해내셨네요 ㅎㅎ 어떻게 하셨나 궁금했는데 글로 남겨주셔서 감사합니다

1개의 답글
comment-user-thumbnail
2025년 7월 1일

오픈소스에 기여가 막막했는데 접근과정을 쉽게 설명해주셔서 감이 잡힌거 같습니다. 글 잘읽었습니다!

1개의 답글
comment-user-thumbnail
2025년 7월 6일

역시 지호님 멋지십니다! ㅎㅎ 저는 여전히 오픈소스 기여 생각만 하고 있었는데,, 실천하시고 후기까지!! 또 한번 배워갑니다!

답글 달기
comment-user-thumbnail
2025년 7월 8일

응원합니당

답글 달기
comment-user-thumbnail
2025년 7월 8일

저도 이거보고 동기부여 얻어서 reactjs 한국 문서 기여하게 됐습니다!!!! 엄청 큰 동기부여가 된 벨로그였어요. 감사합니다.

답글 달기