오픈소스 Fork에서 Upstream 기여까지 — NanoClaw Contribute 기록

· 5분 읽기
목차

프로젝트: qwibitai/NanoClaw 기간: 2026-02-28 ~ 2026-03-02 결과: PR 4건 제출, 1건 머지, 3건 자진 close

오픈소스 프로젝트를 fork해서 쓰다 보면 자연스럽게 수정 사항이 쌓인다. 버그를 고치고, 기능을 붙이고, 운영 환경에 맞춰 코드를 조정한다. 문제는 이 수정 사항들을 upstream에 올릴지, fork에만 남길지 판단하는 것이다.

NanoClaw를 fork하여 개인 AI 어시스턴트로 운영하면서 총 4건의 PR을 upstream에 제출했다. 1건이 머지되고 3건은 직접 close했다. 이 글은 각 PR의 내용과, 올릴지 말지를 결정한 판단 과정을 기록한다.

머지된 PR — CJK 폰트 지원

NanoClaw의 컨테이너 안에서 Chromium으로 웹페이지를 캡처하면, 한중일(CJK) 문자가 빈 사각형으로 렌더링되는 문제가 있었다. 이른바 “tofu” 현상이다. 컨테이너 이미지에 CJK 폰트가 포함되어 있지 않아서 발생하는 문제였다.

수정은 단순했다. Dockerfile의 apt-get install 목록에 fonts-noto-cjk 한 줄을 추가하는 것이 전부다. 변경 범위가 작고, 모든 CJK 사용자에게 보편적으로 유용하며, 외부 의존성도 없다. upstream에 올리기에 적합한 변경이었다.

PR은 당일에 생성되어 당일에 머지되었다. 이것이 유일한 upstream 기여이며, contributor 목록에 등록되었다.

Close한 PR 3건

나머지 3건은 제출 후 검토 과정에서 “이건 upstream보다 fork에 남기는 게 맞다”고 판단하여 직접 close했다.

tool-use 로깅

agent-runner에서 AI가 호출하는 도구(Bash, Read, WebSearch 등)의 이름과 입력값을 로그에 기록하는 기능이다. 컨테이너 안에서 에이전트가 무슨 작업을 하고 있는지 실시간으로 파악할 수 있어서 디버깅에 유용하다.

하지만 이 기능은 운영 환경의 디버깅 편의를 위한 것이지, NanoClaw의 핵심 기능은 아니다. upstream에 필수적이지 않은 로깅을 추가하면 노이즈가 될 수 있다고 판단하여 close했다.

scheduler 중복 실행 방지

NanoClaw의 태스크 스케줄러는 60초 간격으로 실행 대상을 확인한다. 문제는 태스크 실행이 60초를 초과하면 다음 poll cycle에서 같은 태스크가 다시 실행되는 것이었다. AI 에이전트가 컨테이너에서 복잡한 작업을 수행하면 수 분이 걸리는 경우가 흔한데, 그때마다 동일한 작업이 중복으로 실행되었다.

수정 자체는 간단했다. 태스크 실행을 시작하는 시점에 next_run을 즉시 갱신하여 다음 poll에서 잡히지 않게 하는 것이다. 하지만 이 구현이 fork에서 추가한 다른 커스텀 로직과 결합되어 있어서, upstream에 올리려면 깔끔하게 분리하는 작업이 필요했다. 분리 비용 대비 가치가 낮다고 판단하여 fork에 남겼다.

update_project IPC 호스트 커맨드

컨테이너 안의 에이전트가 채팅 명령 하나로 호스트의 upstream 업데이트를 트리거하는 기능이다. 호스트에서 fetch, merge, 의존성 설치, 빌드, 서비스 재시작을 자동으로 수행한다. 충돌이나 빌드 실패 시 자동 롤백까지 포함되어 있다.

이 PR은 기능 자체가 NanoClaw의 철학과 맞지 않았다. NanoClaw는 “개인 노트북에서 돌리는 가벼운 어시스턴트”를 지향한다. 호스트 시스템의 서비스를 재시작하고, 크로스 플랫폼(launchd/systemd) 감지를 하고, IPC로 컨테이너-호스트 간 통신을 하는 것은 프로덕션 서비스의 영역이다. 기능은 유용하지만 upstream의 성격과 맞지 않으므로 fork custom으로 유지했다.

Upstream에 올릴지 판단하는 기준

4건의 PR을 거치며 나름의 판단 기준이 생겼다.

프로젝트 철학과의 부합 — 가장 중요한 기준이다. NanoClaw는 개인용 노트북 어시스턴트를 지향한다. 이 맥락에서 보편적으로 유용한 변경인지, 특정 운영 환경에 특화된 변경인지를 구분한다. CJK 폰트는 전자, IPC 호스트 커맨드는 후자다.

변경의 독립성 — 다른 fork custom 코드와 얽혀 있지 않고 깔끔하게 분리할 수 있어야 한다. scheduler 수정처럼 fork 특화 로직과 결합되어 있으면 분리 비용이 발생한다.

복잡도와 범위 — Dockerfile에 한 줄 추가하는 것과, 새 파일 5개를 생성하고 기존 파일 3개를 수정하는 것은 리뷰어의 부담이 다르다. 변경이 단순하고 범위가 좁을수록 머지 가능성이 높다.

Private Fork에서 PR 보내기

운영 repo를 private으로 전환하면서 GitHub의 fork 관계가 영구적으로 끊어졌다. GitHub 정책상, private으로 바꾼 뒤 다시 public으로 되돌려도 fork 라벨은 복원되지 않는다. private으로 유지하는 이유는 커밋 히스토리에 환경 설정, 도메인, 인프라 구성 정보가 포함되어 있기 때문이다.

fork 관계가 없는 상태에서 upstream PR을 보내려면 별도의 public fork를 만들어야 한다. 절차는 다음과 같다.

  1. PR 전용 public fork를 생성한다
  2. private repo에서 해당 변경만 cherry-pick하여 clean 브랜치를 만든다
  3. public fork에 push하고 upstream으로 PR을 생성한다

핵심은 3단계에서 개인 인프라 코드가 섞이지 않도록 주의하는 것이다. cherry-pick할 때 관련 커밋만 정확히 골라야 한다.

돌아보며

4건 중 1건만 머지되었지만, 나머지 3건을 close한 것도 의미 있는 판단이었다고 생각한다. “올릴 수 있는 건 전부 올리자”가 아니라, 프로젝트 성격에 맞는 것만 선별해서 올리는 것이 건강한 기여 방식이다.

upstream 기여의 가치는 코드 변경량이 아니라 프로젝트에 얼마나 자연스럽게 녹아드느냐에 있다. Dockerfile 한 줄이 파일 5개짜리 기능보다 더 적합한 기여가 될 수 있다.

관련 글