오픈소스 기여, 생각보다 어렵지 않다

아더에러·2026년 2월 25일

프론트엔드

목록 보기
3/15
post-thumbnail

우아한테크코스 7기로 활동하며, 오픈소스에 기여하는 크루들을 보며 나도 한 번 도전해보고 싶다는 생각을 하고 당시에 처음으로 오픈소스 기여를 시작하게 되었다.

최근에는 토스에서 운영하는 리액트 기반 라이브러리인 react-simplikit의 공식 문서를 읽어보게 되었다. 해당 라이브러리를 사용해야 하는 이유를 설명하는 예시 코드를 살펴보던 중, 잘못된 부분을 발견하게 되었다.


기존 코드를 처음 봤을 때 가장 먼저 눈에 들어온 건 searchTimeoutRef의 타입이었다.

브라우저에서 동작하는 리액트 컴포넌트인데 NodeJS.Timeout 타입을 사용하고 있었다.

이 타입은 말 그대로 Node.js 환경에서 사용하는 타입이다. 브라우저 환경에서의 setTimeout은 number를 반환하는데, 굳이 Node 전용 타입을 명시해두면 프로젝트 설정에 따라 타입 에러가 발생할 수 있다.

특히 DOM 환경만 사용하는 프로젝트라면 NodeJS 네임스페이스 자체를 찾지 못하는 경우도 있다.

function AutoCompleteInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<SearchResult[]>([]);
  const [isLoading, setLoading] = useState(false);
  const [isOpen, setOpen] = useState(false);
  const searchTimeoutRef = useRef<NodeJS.Timeout>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (searchTimeoutRef.current) {
      clearTimeout(searchTimeoutRef.current);
    }

    if (query.trim().length === 0) {
      setResults([]);
      return;
    }

    setLoading(true);
    searchTimeoutRef.current = setTimeout(async () => {
      try {
        const response = await fetch(`/api/search?q=${query}`);
        const data = await response.json();
        setResults(data);
      } catch (error) {
        console.error('Failed to fetch results:', error);
      } finally {
        setLoading(false);
      }
    }, 300);

    return () => {
      if (searchTimeoutRef.current) {
        clearTimeout(searchTimeoutRef.current);
      }
    };
  }, [query]);

공식 문서의 예시 코드라면, 최대한 많은 환경에서 문제 없이 동작해야 한다고 생각했다. 그런데 특정 타입 설정에서는 바로 에러가 날 수 있는 코드라는 점이 아쉬웠다.

또 하나 느낀 점은 구조적인 부분이었다. 이미 useEffect의 cleanup 함수에서 clearTimeout을 호출하고 있는데, 굳이 ref로 timeout을 관리할 필요가 있을까 하는 생각이 들었다.

이 경우에는 지역 변수로 timeout id를 선언하고 cleanup에서 정리해주는 것만으로도 충분하다.

function AutoCompleteInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<SearchResult[]>([]);
  const [isLoading, setLoading] = useState(false);
  const [isOpen, setOpen] = useState(false);
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (query.trim().length === 0) {
      setResults([]);
      return;
    }

    setLoading(true);
    const timeoutId = setTimeout(async () => {
      try {
        const response = await fetch(`/api/search?q=${query}`);
        const data = await response.json();
        setResults(data);
      } catch (error) {
        console.error('Failed to fetch results:', error);
      } finally {
        setLoading(false);
      }
    }, 300);

    return () => clearTimeout(timeoutId);
  }, [query]);

그래서 ref 자체를 제거하고, 단순하게 timeoutId를 지역 변수로 관리하는 방식이 더 적절하다고 판단했다. 이렇게 하면 불필요한 타입 문제도 사라지고, 코드도 더 간결해진다.

해당 코드에 기여해보기로 결심한 뒤, 먼저 문서를 관리하는 레포지토리를 찾아보았다. 레포지토리는 해당 문서 우측 상단의 깃허브 아이콘을 클릭해 이동 할 수 있었다.

레포지토리의 README 파일에서 기여 가이드 문서를 확인할 수 있었고, 해당 문서를 충분히 읽은 뒤 본격적으로 작업을 시작했다.


이후 레포지토리를 포크한 다음, 로컬 환경으로 클론하고 새로운 브랜치를 생성했다. 브랜치 이름은 docs/refactor-autocomplete로 지정했다.

수정할 파일의 위치를 찾기 위해 검색 기능을 활용했고, 고치고자 하는 코드를 비교적 쉽게 찾을 수 있었다. 코드를 수정한 뒤 커밋을 진행했다. 커밋 메시지는 기존 PR들을 참고해 작성했으며, 대부분 영어로 작성되어 있어 동일하게 영어로 작성했다.


로컬에서 변경 사항을 확인하기 위해 최상위 경로에서 yarn docs:dev 명령어를 실행했다.


이를 통해 공식 문서와 동일한 형태의 화면을 로컬에서 확인할 수 있었고, 내가 수정한 내용이 정상적으로 반영되었는지 검증할 수 있었다.


이후 변경 사항을 포크한 레포지토리에 푸시하고 PR을 생성했다. PR 작성 시에는 템플릿이 제공되므로, 해당 형식에 맞춰 내용을 정리하면 되었다. 코드 리뷰를 거친 뒤 “thanks!”라는 코멘트와 함께 PR이 승인되었고, main 브랜치에 머지되었다.


실제 공식 문서에 수정 사항이 반영된 것도 확인할 수 있었다. 오픈소스 기여는 이번이 다섯 번째 정도였지만, 그동안은 주로 오탈자 수정이나 코드 개선과 같은 비교적 작은 단위의 작업을 진행해왔다.

다음 오픈소스 기여에서는 이러한 문서 수정이나 리팩토링을 넘어, 실제 기능을 직접 설계하고 구현하는 개발 작업에도 도전해보려 한다.

4개의 댓글

comment-user-thumbnail
2026년 3월 2일

멋집니다!

1개의 답글
comment-user-thumbnail
2026년 3월 2일

멋집니다!

1개의 답글