본문으로 건너뛰기

오픈소스 체험기

· 약 9분
lifeisegg

개발자로써 '한번쯤은 오픈소스에 기여해보고 싶다'라는 생각을 가지고 있지만, 예전의(어쩌면 지금도..?) 저처럼 어디서 부터 시작해야 할 지 막막한 분들을 위해, 오픈소스에 기여하게 된 계기와 그 과정에 대한 경험을 적어보려고 합니다.

아직 주니어 개발자이고 부족한 부분이 많기 때문에 이 점 감안하고 봐주시면 감사하겠습니다.

오픈소스 해보고 싶다...

서두에 서술했듯 오픈소스에 기여해보고 싶다는 생각은 가지고 있었지만, 어떻게 기여하는 것인지도 잘 모르겠고 관심있는 라이브러리의 코드를 봐도 ‘검은건 글씨고 흰건 배경이다’ 수준으로 밖에 읽지 못하는 저의 실력에 좌절하며 금방 포기하곤 했었습니다.

FxTS?

함수형 프로그래밍에 관심을 갖고 유인동님의 강의를 보던 중, FxJS 라는 라이브러리에 관심을 갖게 되었고 조금 더 최근에 개발되고 있는 FxTS라는 라이브러리가 있다는 것을 알게 되었습니다.

typescript로 작성된 함수형 유틸 라이브러리이고 FxJS보다는 제공중인 함수가 적었습니다. 강의를 보며 typescript로 진행되지 않는게 내심 아쉬었는데 이 라이브러리를 참고해가면서 공부하면 좋을 것 같다고 생각하여, 코드를 보게 되었습니다.

계기와 결심

코드를 구경하던 중 dropdropRight는 있는데 take는 있고 takeRight는 없다는 사실을 알게 되었고, 다른 라이브러리에는 이러한 함수가 있는지 확인해보았습니다.

lodash나 ramda와 같은 라이브러리에서는 takeRight라는 함수를 제공하고 있는 것을 확인했고, FxTS에도 있으면 좋을 것 같다고 생각하여, 직접 개발해서 PR을 올려보자라는 결심을 하게 되었습니다.

오픈소스 기여 과정

아직 함수형 프로그래밍에 대해 공부하던 수준인 저에게는 함수 하나를 짜는 일도 쉽지는 않았는데, FxTS에 작성된 다른 함수들과 타 라이브러리에 구현되어 있는 takeRight의 코드를 참고해가며 코드를 작성하기 시작했습니다.

PR 작성

코드의 기능을 구현하고, 테스트코드와 간단한 문서를 작성하여 ‘코드가 반영되지는 못하겠지만 이런 시도를 해봤다는 것에 의의를 두자' 라는 심정으로 아래와 같은 PR을 올렸습니다. (PR 링크) takeRightPR

리뷰

PR을 올리고 며칠뒤 아래와 같은 메인테이너의 리뷰가 달렸고, 몇가지 수정사항에 대한 피드백을 받았습니다.

review1 review2

더 자세한 내용은 PR 링크에서 확인해보세요!

최종 코드

피드백 받은 사항들을 수정하였고, 다시 리뷰과정을 거쳐 최종적으로 머지가 되었습니다. 제가 구현한 takeRight 함수는 다음과 같습니다.

import { isAsyncIterable, isIterable } from "../_internal/utils";
import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
import IterableInfer from "../types/IterableInfer";
import toArray from "../toArray";
import isArray from "../isArray";
import isString from "../isString";
import concurrent from "./concurrent";
import { isConcurrent } from "./concurrent";

function* stringTakeRight(length: number, str: string) {
const arr = Array.from(str);
const index = arr.length - length;
for (let i = index; i < arr.length; i++) {
if (arr[i]) yield arr[i];
}
}

function* arrayTakeRight<A>(length: number, arr: Array<A>) {
const index = arr.length - length;
for (let i = index; i < arr.length; i++) {
if (arr[i]) yield arr[i];
}
}

function* sync<A>(length: number, iterable: Iterable<A>): IterableIterator<A> {
const arr = toArray(iterable);
const index = arr.length - length;
for (let i = index; i < arr.length; i++) {
if (arr[i]) yield arr[i];
}
}

async function* asyncSequential<T>(length: number, iterable: AsyncIterable<T>) {
const arr = await toArray(iterable);
const index = arr.length - length;
for (let i = index; i < arr.length; i++) {
if (arr[i]) {
yield arr[i];
}
}
}

function async<A>(
length: number,
iterable: AsyncIterable<A>,
): AsyncIterableIterator<A> {
let iterator: AsyncIterator<A>;
return {
[Symbol.asyncIterator]() {
return this;
},
next(_concurrent: any) {
if (iterator === undefined) {
iterator = isConcurrent(_concurrent)
? asyncSequential(length, concurrent(_concurrent.length, iterable))
: asyncSequential(length, iterable);
}

return iterator.next(_concurrent);
},
};
}

function takeRight<A>(
l: number,
iterable: Iterable<A>
): IterableIterator<A>;

function takeRight<A>(
l: number,
iterable: AsyncIterable<A>,
): AsyncIterableIterator<A>;

function takeRight<A extends Iterable<unknown> | AsyncIterable<unknown>>(
l: number,
): (iterable: A) => ReturnIterableIteratorType<A>;

function takeRight<A extends Iterable<unknown> | AsyncIterable<unknown>>(
l: number,
iterable?: A,
):
| IterableIterator<IterableInfer<A>>
| AsyncIterableIterator<IterableInfer<A>>
| ((iterable: A) => ReturnIterableIteratorType<A>) {
if (l < 0) {
throw new TypeError("'length' must be greater than 0");
}

if (iterable === undefined) {
return (iterable: A) => {
return takeRight(l, iterable as any) as ReturnIterableIteratorType<A>;
};
}

if (isArray(iterable)) {
return arrayTakeRight<A>(l, iterable) as any;
}

if (isString(iterable)) {
return stringTakeRight(l, iterable) as any;
}

if (isIterable<IterableInfer<A>>(iterable)) {
return sync(l, iterable);
}

if (isAsyncIterable<IterableInfer<A>>(iterable)) {
return async(l, iterable);
}

throw new TypeError("'iterable' must be type of Iterable or AsyncIterable");
}

export default takeRight;

PR 머지 이후

pr이 머지된 이후로는 언제쯤 버전이 올라가서 제가 짠 코드가 배포될 지 기다리는 나날의 연속이었는데요, 며칠전 아래와 같은 릴리즈노트와 함께 제가 짠 코드가 배포되었습니다.

release note

해당 함수에 대한 문서는 여기에서 확인해 보실 수 있습니다.

소감

처음으로 오픈소스에 PR을 올린 것이라 당연히 반영이 되지 않을 것이라고 생각했었지만, 친절하게 코드리뷰를 해주신 덕분에 반영이 될 수 있었던 것 같습니다.

아직까지 배울게 많다는 사실을 다시한번 깨닫게 되었습니다. 이전엔 사용해 본 적이 없었던 function overloading을 사용해보았고, js의 이터레이션 프로토콜에 대해서도 좀 더 깊게 공부하게 되었습니다.

개인적으로 생각해 본 오픈소스를 하게 되며 얻는 장점은 아래와 같습니다.

  1. 해당 언어에 대한 이해도 상승
  2. 메인테이너에게 코드에 대해 리뷰받을 수 있는 기회
  3. 다른사람의 코드를 보며 모르는 것들을 학습
  4. 오픈소스에 기여했다는 뿌듯함(?)

기여 이후 현재는?

FxTS에 기여하며 얻은 자신감을 바탕으로 부족한 영어실력이지만 pnpm의 문서 번역에도 참여해보고 있고, Recoil에 문서 오타를 수정하는 PR을 올렸었습니다.

최근에는 react-query를 조금 더 편하게 사용하기 위해 react-query-toolkit이라는 라이브러리도 개발하고 있습니다.

오픈소스에 기여하기 위한 첫발을 내딛기가 두렵고 어려웠지만, 그 한번의 시도를 통해 얻은 자신감을 바탕으로 더 다양한 활동들을 할 수 있게 된 것 같습니다. 앞으로 더 많은 분들이 오픈소스에 기여하며 더욱 활발하고 재밌는 개발문화가 만들어져 가길 기대합니다.