두 폴더를 GitHub에 공개로 올리기로 했다. saegim-eval과 새김AI 백엔드. 둘 다 내 모노레포 안에 하위 폴더로 들어 있다.
보통은 이럴 때 폴더를 밖으로 꺼낸다. 새 레포를 파고, 그 폴더만 복사해 넣고, 따로 관리한다. 그런데 나는 그게 싫었다.
이유는 사소하다. 나는 모노레포 전부를 VSCode 한 창에 띄워놓고 작업한다. noon 워치앱, 새김AI, 이 블로그, eval 엔진이 한 사이드바에 다 보인다. 폴더 하나를 밖으로 빼는 순간 그 창이 깨진다. 공개하겠다고 창이 둘 셋으로 갈라지는 게 싫었다.
그래서 폴더는 집에 그대로 두고, 밖으로는 복사본만 내보내는 방법을 찾았다.
git subtree로 폴더만 내보냈다
git subtree push가 그걸 해준다.
하위 폴더 하나를 지정하면 그 폴더의 커밋만 뽑아 별도 레포의 루트로 밀어 넣는다. 편집도 커밋도 전부 모노레포에서 한다. 공개 레포는 읽기용 미러다.
git subtree push --prefix=projects/saegim-eval saegim-eval main
git subtree push --prefix=projects/saegim-ai/backend saegim-ai-backend main
--prefix가 집 안에서 내보낼 폴더고, 그 뒤가 리모트 이름과 브랜치다. saegim-eval은 gnldnd11/saegim-eval로, 백엔드는 gnldnd11/saegim-ai-backend로 나간다. 새김AI 프론트엔드와 게임 코드는 안 딸려간다. --prefix가 백엔드 폴더만 가리키니까.

공개는 되돌릴 수 없다
여기서 손이 한 번 멈췄다.
push는 취소가 없다. 공개 레포는 올라가는 순간 크롤러가 긁고 캐시가 뜬다. 나중에 지워도 어딘가엔 남는다. 그러니 올리기 전에 확실히 해야 했다. 이 폴더 어딘가에 API 키나 서비스계정 키가 섞여 있지는 않은지.
문제는 지금 파일만 깨끗해선 부족하다는 거다. git은 과거를 다 기억한다. 두 달 전에 실수로 .env를 한 번 커밋했다가 다음 커밋에서 지웠다면, 파일은 지금 없어도 히스토리엔 남아 있다. 그리고 subtree는 폴더의 히스토리를 통째로 들고 나간다. 과거의 실수까지 같이.
그래서 지금 파일이 아니라 히스토리 전체를 뒤졌다.
git log --all -- '*-firebase-adminsdk-*.json'
git log --all -- '*/.env'
Firebase 서비스계정 키와 .env가 이 레포에 한 번이라도 커밋된 적 있는지 본 것이다. 둘 다 빈 결과였다. 커밋된 적이 없다.
이유는 확인하고 나서 알았다. .gitignore가 처음부터 막고 있었다. .env, *.env, *-firebase-adminsdk-*.json, .venv/, results/가 전부 무시 목록에 있어서 애초에 git에 들어간 적이 없었다. 들어간 적이 없으니 히스토리에도 없고, subtree에도 안 딸려갔다.
과거의 내가 처음에 막아둔 게 이제 와서 도움이 됐다. 그래도 눈으로 확인은 했다. 막아뒀을 것과 안 샜다는 다르니까.
히스토리 전체를 뒤졌는데 아무것도 안 나왔다. 빈 결과가 곧 안전 신호다.
첫 push가 거부됐다
스캔이 끝나고 마음 놓고 git subtree push를 쳤다. 바로 거부당했다.
Updates were rejected because the remote contains work that you do not have locally.
리모트에 내가 로컬에 없는 커밋이 있다는 거였다. 빈 레포를 만든 줄 알았는데, GitHub에서 레포를 만들 때 초기 커밋 하나가 딸려 있었던 모양이다. subtree가 밀어 넣으려는 히스토리와 그 커밋이 안 맞아 튕겼다.
git pull로 합칠 수도 있었지만, 미러에 남의 초기 커밋을 섞고 싶지 않았다. 미러는 내 폴더 히스토리 그대로여야 했다. 그래서 폴더를 브랜치로 한 번 잘라내서 강제로 덮었다.
git subtree split --prefix=projects/saegim-eval -b _pub
git push saegim-eval _pub:main --force
git branch -D _pub
split이 그 폴더의 커밋만 뽑아 _pub이라는 임시 브랜치로 만든다. 그걸 미러의 main에 강제로 덮어씌우고, 임시 브랜치는 지운다. 미러가 내 폴더 히스토리로 깨끗하게 초기화됐다.
그리고 subtree push는 공짜가 아니다.
이 명령은 밀 때마다 폴더의 히스토리를 처음부터 다시 쪼갠다. 커밋이 1600개가 넘는 모노레포에서 이 폴더를 건드린 커밋만 골라내려면 전체를 훑어야 하니, 한 번 밀 때마다 그 계산을 다시 한다. 즉시 끝나는 명령은 아니다.
공개용으로 README를 다시 썼다
백엔드 README는 이 서버를 어떻게 띄우고 인증을 어떻게 붙이는지만 적힌 낡은 문서였다. 정작 이 코드가 뭘 하는 물건인지는 한 줄도 없었다. 남이 레포를 처음 열었을 때 그래서 이게 뭔데가 안 보였다.
그래서 엔진을 보여주는 문서로 다시 썼다. 이 백엔드의 핵심은 프레임워크에 기대지 않고 손으로 짠 에이전트 엔진이라는 것. 같은 에이전트를 native, LangGraph, ReAct 세 가지로 구현해서 관리자 토글 하나로 런타임에 갈아끼운다는 것. 함수콜링 툴 12개, 임베딩 RAG, 지식그래프, MCP 호스트까지 직접 만들었다는 것. README를 열면 그 지도가 먼저 보이게 했다.
인증 매뉴얼이던 문서를 엔진 쇼케이스로 바꿨다.
남은 것
이렇게 두 레포가 공개로 열렸다. 그런데 이 방식이 완전히 매끄럽진 않다.
코드를 고칠 때마다 subtree push를 한 번씩 더 해야 미러가 따라온다. 그리고 그 push는 매번 히스토리를 처음부터 다시 훑는다. 히스토리가 길어질수록 더 무거워질 거다. 언젠가 이게 못 참을 만큼 느려져서 다른 방법을 찾게 될지도 모른다.
그래도 지금은 이걸로 간다. 폴더를 밖으로 빼서 창이 갈라지는 것보단, 공개하고 싶을 때 명령 한 줄 더 치는 편이 낫다. 자동으로 나가는 건 없다.