아주 오랜만에 글을 쓴다. 개강하고서 정신없이 학교를 다니니까 어느새 9월이 지나가 있었다.ㅎㅎ
오늘은 8월 동안 꾸준히 했던 Node.js 오픈소스 기여 경험을 기록하려고 한다.
PR(Pull Request)로도 내가 기여했던 경험이 기록되지만, PR 하나를 날리면서 고민했던 것들, close된 PR이더라도 배운 것들이 있었기에 글로 정리해두려고 한다.
지난번에 간단히 토스 오픈소스에 기여해본 경험 이후로 오픈소스에 더 관심이 생겼다.
그땐 문서 페이지의 버그를 고쳤지만, 이제는 정말로 핵심 코드에 기여해보고 싶었다.
때마침 OSSCA 프로그램을 발견하게 되었다. '오픈소스 컨트리뷰션 아카데미'라는 프로그램인데, 간단히 말하자면 멘토와 멘티를 매칭하여 특정 오픈소스 레포지토리에 기여해보는 프로그램이었다.
많은 선택지가 있었는데, 나는 그중에서도 Node.js 프로젝트를 선택했다. 평소 자주 사용해봤던 도구였고, 그 코어를 더 깊이 알고 싶다는 욕심도 있었기 때문이다.
7월 12일 발대식에 다녀왔다. 처음으로 OSSCA 인원 모두가 모여서 만난 시간이었다.
발대식에는 여러 세션이 진행되었는데, 그중에서도 가장 인상 깊었던 세션은 OpenStack Global Maintainer 초청 강연이었다. 오픈소스라는 매개로 전 세계 개발자들과 소통하시는 모습이 너무 멋있었다.

사실 나는 지금까지 계속해서 다양한 사람들을 새롭게 만나려고 노력해왔다. 처음엔 대학에서 우리 학교 사람들이랑만 교류했지만, 네이버 부스트캠프를 통해 학교 밖 개발자들을 만났고, 인턴을 하면서는 시니어 개발자들과 이야기 나눌 수 있었다. 새로운 사람들을 만날 때마다 시야가 넓어지는 걸 느꼈는데, 오픈소스는 그 범위를 전 세계로 확장할 수 있는 기회라는 생각이 들었다. 코드를 매개로 국경을 넘어 소통한다는 게 굉장히 설레는 포인트였다.
(발대식 날 드론으로 인형도 뽑았다 ㅎㅎ)
첫 빌드는 생각보다 어렵지 않았다.
처음엔 문서가 정말 많아서 어떤 것부터 읽고 어떤 것부터 해야 할지 막막했는데, 멘토님께서 빌드부터 해보라는 미션을 주셨다. 또 어떤 부분을 읽어야 할지도 알려주셔서 금세 해낼 수 있었다.
근데 좀 놀랐던 점은 빌드 한 번 하는 데에 25분 가량 수행되더라는 점이다..ㅋㅋ Node.js는 JavaScript뿐만 아니라 C++로 구성된 네이티브 모듈도 포함하고 있어서, 전체 프로젝트를 컴파일하는 데는 제법 시간이 필요했다.

빌드 이후 '자, 노드 기여를 시작해보자!' 하고 둘러보는데 도저히 어디에 기여를 해야 할지 감이 안 왔다.
good first issue부터 살펴보았으나, 크고 유명한 프로젝트인 만큼 good first issue들도 금세 해결하시는 분들이 많았다.(good first issue란) 또 어떤 이슈들은 너무 어려워서 뭘 해야 할지 감도 안 오는 것들이 많았다.
일단은 작게 시작해보자는 생각으로 문서(docs) 기여부터 해보기로 했다. 그러나 이 docs도 굉장히 많아서 쉽진 않았다. 일일이 읽어보았음에도, 기여할 만한 지점이 잘 안 보였다.
그러던 중 다른 멘티님께서 Discord에 VS Code의 spell checker 확장 프로그램을 사용해 기여하신 경험을 공유해주다. 여기서 아이디어를 얻어 나도 spell checker로 오타들을 열심히 찾아보았다.
그리고 사소하게나마 오타를 고쳐보았다.ㅎㅎ

https://github.com/nodejs/node/pull/59330
아주 단순하게 고친 거였지만, (1) 레포를 fork 받고 (2) 로컬에서 브랜치 파고 (3) 오타 고친 다음 test 쭉 돌려보고 (4) PR 만들어보고 (5) 리뷰 받아보는 전체 사이클을 경험해볼 수 있었다.
이렇게 기여 사이클을 알고 난 뒤부터는 더 자신감이 붙어서 본격적으로 기여해볼 수 있게 되었다.
8월 2일부터 9월 6일까지 일주일에 하나씩은 PR을 날려보려고 했다.
그중에서도 특히 인상 깊었던 PR들을 소개한다.
http, benchmark: remove CRLF variables (#59466)
코드 일관성 개선을 위해 내부 모듈(_http_common)에서 더 이상 사용하지 않는 CRLF 변수를 제거하고, 이를 문자열 리터럴 '\r\n'으로 대체하는 PR을 올렸다.
그 작업에는 아래와 같이 module export 구문에 있던 CRLF 변수 제거도 포함되었다.

단순한 변수 제거라고 생각했는데, 생각지 못했던 리뷰를 받았다. 아직 _http_common.js 모듈이 End-of-Life deprecation된 상태가 아니기 때문에 섣불리 export 변수를 제거해서는 안 된다는 것이었다.

리뷰어의 피드백을 받고 나서야 대규모 프로젝트가 안정성을 어떻게 유지하는지 배울 수 있었다.
이 경험을 통해 Node.js의 deprecation 정책을 알게 되었다. Deprecation은 총 4단계로 진행된다. 먼저 Documentation-only 단계에서는 공식 문서에서만 경고를 표시하고, Application 단계에서는 내가 작성한 코드에서만 경고를 보여준다(외부 라이브러리인 node_modules에서는 경고하지 않는다). 이후 Runtime 단계에서는 모든 코드에서 경고를 출력하고, 마지막 End-of-Life 단계에 이르러서야 해당 기능이 실제로 제거된다. (https://nodejs.org/api/deprecations.html#deprecated-apis)
나는 내부적으로만 사용되는 변수니까 그냥 제거해도 되겠지 싶었는데, Node.js는 변경 사항이 사용자에게 미칠 영향을 매우 신중하게 고려한다는 걸 실감했다.
typings: add missing properties in ConfigBinding, OSBinding (#59585)
Node.js의 TypeScript 타입 정의를 개선하는 작업을 했다. ConfigBinding, OSBinding에 누락된 속성 타입을 추가했는데, 이 과정에서 Node.js 코어의 C++ 코드를 직접 들여다보게 되었다.
Node.js는 C++로 작성된 네이티브 모듈과 JavaScript 간의 연결고리(바인딩)가 존재하는데, 이 타입 정의가 일부 누락되어 있었다. C++ 코드에서 실제로 제공하는 속성들을 찾아내어 TypeScript 타입 정의에 추가하는 작업이었다. 처음으로 두 언어의 경계를 넘나드는 코드를 보았고, JavaScript 너머의 저수준 구현을 이해할 수 있었던 흥미로운 경험이었다.
lib: use slice to compact Readable buffer in streams (#59676)
Stream 모듈의 Readable 버퍼 처리 성능을 개선하고자 splice 대신 slice를 사용하는 변경을 제안했다. splice는 배열 내부에서 요소를 하나씩 이동시키는 O(N) 비용이 발생하는 반면, slice는 새 배열을 할당하긴 하지만 V8의 최적화된 bulk-copy를 사용해 한 번에 복사하니 더 효율적일 거라고 생각했다. 실제로 이와 같은 아이디어로 PR을 올리고 merge된 사례까지 있어 내 생각에 자신이 있었다.
하지만 리뷰어는 내 가설에 의문을 제기했다. slice도 결국 모든 요소를 복사해야 하는데 왜 더 빠르냐는 것이었다. 나는 큰 배열로 간단한 벤치마크 시나리오를 만들어 slice가 더 빠르다는 결과를 보여주며 설득하려고 노력했다. 하지만 리뷰어는 내 벤치마크가 잘못되었다고 알려주었다. V8이 사용되지 않는 변수를 최적화로 제거해버려서 실제로는 slice 연산이 실행조차 안 되었을 수 있다는 것이었다. 리뷰어는 올바른 벤치마크를 보여주었고, 결과는 오히려 splice가 더 빠르다는 걸 증명했다.
비록 PR은 Close되었지만, 이 토론을 통해 정말 많은 걸 배웠다. splice와 slice의 차이부터 하여, 올바른 벤치마크 작성 방법, V8의 최적화 방식까지 말이다. 특히 Node.js TSC와 이렇게 깊이 있는 기술 토론을 나눌 수 있었다는 것 자체가 큰 배움이었다. 내 주장을 단순히 거부하는 게 아니라, 왜 그런지 코드로 설명하고 올바른 접근법을 알려주는 모습에서 많이 배울 수 있었다.
path: change win32.join to use array (#59781)
path 모듈의 win32.join 함수 성능을 개선하는 PR을 제출했다. 이 함수는 여러 경로 조각들을 하나의 Windows 경로로 합쳐주는 기능을 한다(예: 'C:\\foo', 'bar', 'baz' → 'C:\\foo\\bar\\baz').
기존 코드는 경로 조각을 처리할 때마다 문자열을 직접 연결하는 방식(result += segment)을 사용했다. JavaScript에서 문자열은 불변(immutable)이라서, 연결할 때마다 새로운 문자열 객체를 생성하게 된다. 경로 조각이 많을수록 이 비용이 누적되는 구조였다.
나는 이를 배열 기반 방식으로 변경했다. 경로 조각들을 먼저 배열에 담아두고(arr.push(segment)), 마지막에 한 번만 join()으로 합치는 것이다. 이렇게 하면 중간에 불필요한 문자열 객체 생성을 피할 수 있다.
로컬 환경에서 벤치마크를 돌려본 결과 약 6%의 성능 향상을 확인했고, 이를 근거로 PR을 제출했다. Node.js의 벤치마크 시스템을 직접 사용하며 대형 오픈소스 프로젝트가 성능 변화를 어떻게 측정하는지 배울 수 있었다. 작은 최적화지만, 수많은 개발자들이 사용하는 코어 모듈이라는 점에서 의미 있는 기여였다.
이전에 이런 그림을 밈처럼 본 적이 있다.
거대한 코끼리처럼 보이는 전 세계 IT 인프라가, 사실은 작은 개미들처럼 보이는 무급 오픈소스 개발자들의 헌신으로 지탱되고 있다는 걸 보여주는 그림이다.
학생 신분으로 꾸준히 기여하는 것도 이토록 어려운데, 금전적 대가 없이 꾸준히 기여해 나가는 사람들이 정말 대단했다.
지금까지 내가 사용해오던 Node.js가 이렇게 관리되고 발전되어 왔던 거구나를 이번 기회에 알 수 있었으며, 감사함이 느껴졌다.
한 번 이렇게 대형 프로젝트에 기여를 하고 나니 이제는 어떤 오픈소스 프로젝트든 기여할 수 있다는 자신감을 갖게 되었다. 예전에는 '내가 감히 이런 큰 프로젝트에 기여할 수 있을까?' 하는 두려움이 있었다면, 이제는 '문제를 발견하면 직접 고쳐볼 수 있지 않을까?'라는 생각이 먼저 든다.
특히 동료 멘티분들과 함께 기여하고 공유하는 과정에서 큰 영감과 동기부여를 받았다. 혼자였다면 여전히 문서 수정 정도에 머물렀을 텐데, 함께 학습하고 성장하는 환경 덕분에 더 다양하고 깊이 있는 기여를 시도할 수 있었다. 또한 멘토님께 멘토링을 받으면서 Node.js의 동작 원리에 대한 깊이 있는 학습을 할 수 있었다.
조금 더 Node.js 자체에 딥다이브하여 어려운 문제를 풀어봤으면 좋았을걸 하는 아쉬움이 남는다. 어떤 멘티분은 웹 표준 문서를 보면서 Node.js의 구현이 스펙과 일치하는지 확인하며 기여하시기도 하고, 특정 모듈 하나를 깊이 파고들어 집중적으로 기여하시기도 하셨다. 나는 그런 딥다이브보단 일단은 눈에 보이는 것들 중 빠르게 해낼 수 있는 것부터 했던지라 깊이감이 아쉬움으로 남는다.
또 9월 개강 이후로 꾸준히 기여하지 못하고 있는 점도 아쉽다. 앞으로는 조금이라도 시간을 내어 작은 기여라도 꾸준히 이어가고 싶다.
지호님 멋지게 성장 중이시네요 동기부여 하고 갑니다 ㅎㅎ