새김 AI
프레임워크 없이 직접 구현한 개인 데이터 AI OS.
새김AI는?
프레임워크에 기대지 않고 함수콜링·스트리밍·RAG·에이전트 루프를 프레임워크 없이 직접 구현한 "개인 데이터 AI OS"다. FastAPI 백엔드 위에 데스크톱(Tauri)·웹·iOS(Capacitor) 프론트가 붙고, 대화 중 12개 함수콜링 툴과 MCP 툴을 스스로 골라 호출한다.
제품으로는 실패했다. "모든 걸 넣어서 아무것도 아니게 된" 종료작(2026-04-09)이다. 이 위키가 자랑이 아니라 부검 소견이자, 거기서 뽑은 교훈인 이유다. 값진 건 남은 제품이 아니라, 이 프로젝트를 통과하며 손에 남은 기술과 판단이다.

왜 만들었나
일정은 구글캘린더, 메모는 노션, 지출은 뱅크샐러드에 흩어져 있었다. "다음 주 뭐 하지?" 하나에 앱을 세 개씩 열어야 했다. 그래서 "내 모든 데이터를 아는 AI를 직접 만들자"에서 시작했다(2025.10). AI의 응답이 텍스트로 끝나지 않고 실제 앱 동작으로 이어지게 하려고 OS식 UI를 택했다.
전체 구조
SSE 응답 포맷이 같아서 프론트는 어떤 추론 엔진이 돌았는지 모른다. 엔진 교체는 Firestore 설정값 하나로 런타임에 일어난다.


직접 구현한 에이전트 엔진
이 프로젝트에서 가장 공을 들인 부분이다. 에이전트 루프를 프레임워크 없이 직접 만들고, 같은 걸 LangGraph로 다시 만들어 나란히 비교했다. 이 엔진은 제품이 끝난 뒤(2026-04)에도 한참을 더 팠다. 기능을 늘리려던 게 아니라, 프레임워크가 감춰둔 원리를 직접 확인하고 싶어서였다.
native 루프 직접 구현
프레임워크 없이 Anthropic/OpenAI SDK로 직접 만든 단일 턴 툴콜 루프다. 새김의 툴은 그 자체로 완성형 답변을 반환하기 때문에(날씨 툴이 "오늘 서울은…" 문장까지 생성) 결과를 LLM에 되먹일 필요가 없다. 그래서 ReAct 순환이 아니라 단일 턴이 기본이다.
동기 SDK를 async 스트리밍에 태우려고 생산자(daemon 스레드)·소비자(async 제너레이터) 사이에 큐 브릿지를 뒀다. 텍스트 토큰은 생기는 대로 즉시 흘려보내고, 툴 호출은 스트림이 끝난 뒤 한 번씩 실행해 그 답변을 최종 답으로 내보낸다.

12개 함수콜링 툴
대화 맥락에 맞춰 아래 툴을 스스로 골라 호출한다. @멘션·앱 컨텍스트로 후보를 좁히고, "참조(읽기)는 자유, 실행(쓰기)은 스코프 제한" 원칙으로 필터한다.
| 툴 | 하는 일 |
|---|---|
get_weather | 현재·오늘·내일·주간·주말 날씨 (기상청 KMA) |
manage_schedule | 일정 등록·수정·조회 |
manage_ledger | 가계부 등록·요약·소비 브리핑·고정거래 |
search_user_data | 내 기록 RAG 검색 (+출처 뱃지) |
search_place | 주변 장소 (카카오 + 구글) |
search_my_places | 저장한 장소 검색 |
get_place_reviews | 구글 리뷰 AI 요약 |
web_search | 웹 검색 + 요약 (인라인 [n] 출처) |
create_document | 창작 문서 생성 (Canvas) |
generate_image | AI 이미지 생성·편집 |
play_music | 음악 검색·재생 |
translate | 번역 |
find_meeting_point (MCP) | 그어봄이 찾는 공정한 중간지점 |
native ↔ LangGraph ↔ ReAct
관리자 토글 하나로 추론 엔진 3종을 런타임 교체한다. 목적은 "프레임워크는 마법이 아니라 도구"를 동등 구현으로 증명하는 것. LangGraph 그래프는 직접 구현한 루프와 1:1로 매핑되고, ReAct 엔진은 그 그래프에서 단 한 줄(tools→agent) 차이로 다단계 순환이 된다.
둘 다 만들어보고 native를 기본으로 채택했다. 근거:
| 축 | native (직접 구현) | LangGraph |
|---|---|---|
| 프로바이더 | claude + gpt 모두 | claude 전용 (gpt는 native 폴백) |
| 첨부(이미지) | 지원 | 미지원 → native 폴백 |
| 프롬프트 캐싱 | Anthropic ephemeral 적용 | 미적용 (토큰 비용↑) |
| 스트리밍 제어 | 저수준 직접 (Railway 우회 가능) | astream 추상화 의존 |
| 상태/메모리 | Firestore가 담당 | MemorySaver 제공하나 미주입 (Firestore가 이미 함) |
| 확장 | 루프 직접 수정 | ReAct로 한 줄 확장 |
native가 기능이 더 넓고(gpt·첨부까지 커버), 프롬프트 캐싱으로 더 싸고, 스트리밍 저수준 제어가 필요했고, 프레임워크가 주는 상태관리는 이미 Firestore가 하고 있어 더 얹을 게 없었다. 그럼에도 LangGraph를 옵션으로 남긴 건 선언적 구조와 ReAct 한 줄 확장의 학습·소통 가치 때문이다.
ReAct 엔진은 최대 12번까지 순환하고, 최종 답은 라운드 수로 가른다. 툴을 안 부르면 바로 답하고, 한 번만 부르면 그 툴 답을 원문 그대로 살려 출처를 보존하고, 여러 번 체이닝할 때만 종합한다.

멀티프로바이더 추상화
model="claude" | "gpt" 문자열 하나로 두 프로바이더를 통합했다. Claude Tool Use를 기준으로 양방향 스키마 변환을 두고, tool_choice 방식 차이와 tool_calls 파싱 차이(Claude는 인자가 이미 객체, OpenAI는 JSON 문자열)를 경계에서 흡수해 결과를 하나의 타입으로 통일한다.
SSE 스트리밍 (Railway 우회)
모든 SSE 이벤트에 ~1KB 공백 패딩을 붙인다. Railway 프록시가 작은 청크를 버퍼링해 스트리밍이 끊기는 걸 우회하기 위한 것이다. 이벤트를 token·thinking·step·rich_card·sources로 구분해 프론트가 각각 다르게 렌더한다.
임베딩 RAG + 출처 그라운딩
text-embedding-3-small(1536차원)로 게시글·일정·일기를 임베딩한다. 처음엔 청킹 없이 문서 하나를 벡터 하나로 만들어 Firestore에 인라인 저장하고, 검색은 전량 브루트포스 코사인으로 했다. 개인 데이터 규모엔 전용 벡터DB나 ANN이 필요 없다는 판단이었다. 검색된 근거는 응답에 출처 뱃지로 붙는다.
이후 오버랩 청커를 얹어 2000자를 넘는 장문도 뒷부분까지 검색되게 했다. 짧은 개인 기록엔 청킹이 필요 없었지만, 문서 첨부 기능이 청커를 불렀다. 규모가 커지며 설계가 따라 진화한 셈이다.
이 검색 경로는 지금도 채점당하고 있다. saegim-eval이 매일 돌면서 모형 파이프라인이 아니라 이 백엔드의 청킹·임베딩·랭킹 함수를 그대로 불러 검색 품질을 잰다. 그래서 청크 크기나 임베딩 모델을 건드리면 다음 날 점수로 드러난다. 종료한 프로젝트의 엔진이 다음 프로젝트의 측정 대상으로 남은 셈이다.
GraphRAG (지식그래프)
납작하게 쌓인 메모리에서 사람·장소·프로젝트·관심사 같은 엔티티 8종과 그 관계를 뽑아 "AI가 아는 나" 그래프를 만든다. 질의가 들어오면 이름을 그래프 노드와 매칭(LLM 호출 없이)해 한 홉 이웃까지 훑어 근거 기억을 주입한다.
처음엔 그래프를 그리기만 하고 질의에 참조하지 않아서 "시각화지 GraphRAG가 아니었다." 엔티티 링킹→서브그래프 주입을 붙이고 나서야 이름값을 했다. 말만 GraphRAG였던 걸 스스로 바로잡은 셈이다.

대화 메모리 + dedup
지속적인 사실만 추출해 저장한다. 가계부 금액이나 일회성 일정 같은 건 일부러 뺀다. 중복은 두 단계로 거른다: 텍스트가 똑같으면 버리고, 임베딩이 아주 비슷하면 새 것으로 교체한다.
MCP 호스트
새김 백엔드를 MCP 호스트로 만들어 원격 MCP 툴을 내부 툴에 머지한다. v1은 그어봄 하나(카카오 PlayMCP, Streamable HTTP 전송). 중간지점 결과는 전용 카드로 승격해 렌더한다.

끝난 제품, 계속 쓰이는 배관
제품은 2026년 4월에 끝났다. 그런데 백엔드는 8월에 다시 손을 봤다. 기능을 늘리려던 게 아니라, 다른 제품이 이 검색 엔진을 갖다 쓰기 시작해서다. 새로 만든 사내 도구(새김 웍스)의 자료 방이 파일을 올리면, 색인과 검색은 이 백엔드가 한다. 프론트도 데스크톱 앱도 커밋 한 줄 늘지 않았다. 늘어난 건 API 넷뿐이다.
경계를 이렇게 그었다. 원본 파일과 권한과 화면은 부르는 제품이 갖고, 색인은 여기가 갖는다. 부르는 쪽 코드에는 "벡터"라는 말이 한 번도 나오지 않는다. 임베딩 모델이나 청크 크기를 바꾸면 색인을 전량 다시 만들어야 하는데, 제품마다 색인을 들고 있으면 그때 제품을 하나씩 찾아다녀야 하기 때문이다. 그렇다고 묶이는 것도 아니다. 원본이 제품에 남아 있으니 파이프라인을 갈아끼우고 다시 색인하면 그만이다.
2만 자에서 잘리던 천장
원래 청킹 함수는 청크 스무 개에서 스스로 멈췄다. 청크를 문서 하나에 전부 담는 구조라 저장소 문서 크기 한도가 곧 본문 길이 한도였고, 실측하면 2만 자쯤에서 뒷부분이 사라졌다. 공용 색인은 청크 하나를 문서 하나로 저장하니 그 전제가 없다. 기존 함수는 그대로 두고 위에 조립 계층만 얹었다. 본문을 창으로 잘라 창마다 돌리고 이어 붙인다.
첫 판은 잘못 만들었다. 소비한 글자 수를 마지막 청크의 위치로 되짚었는데, 슬라이드나 표처럼 같은 문단이 반복되는 문서에서 뒤쪽의 같은 문자열을 잡았다. 4만 2천 자 문서가 청크 스무 개로 "전량 색인 완료"라고 보고했다. 조용한 손실이라 실측하지 않으면 영영 몰랐을 종류다. 지금은 창을 미리 잘라 두어 소비량을 추측할 필요가 없고, 그래도 상한에 걸리면 응답에 잘렸다고 적어 보낸다.
유사도 임계값을 버렸다
11장짜리 사업 제안 장표를 색인하고 질문 다섯 개를 던져 봤다. 배관은 도는데 답이 이상했다.
| 질문 | 결과 |
|---|---|
| 이사는 언제 하기로 했나 | 정답 문서가 0.276. 임계값 0.3에 잘림 |
| 점심 메뉴 추천 | 관계없는 그림이 0.361로 통과 |
| 회의실 부족 문제 | 본문 청크가 같은 문서의 캡션 청크보다 낮음 |
코사인 값의 절대 크기는 질의 길이와 청크 길이에 좌우된다. 짧은 한국어 질의는 통째로 낮게 나오고, 짧고 빽빽한 이미지 캡션은 아무 질문에나 중간값이 나온다. 어느 숫자를 박아도 같은 사고가 난다는 뜻이다. 그래서 임계값을 0으로 내리고 점수를 그대로 실어 보낸다. 관계가 있느냐는 판정은 점수 하나가 아니라 근거를 보고 한다.
청크는 1200자에서 400자로 줄였다. 1200자면 슬라이드 대여섯 장이 한 덩어리라, 그 안의 한 문장으로 물으면 나머지가 신호를 희석한다. 그리고 뜻으로 찾는 것과 별개로 키워드 검색을 붙였다. 문서에 그 글자 그대로 있는 드문 낱말은 벡터가 아니라 키워드의 일이다. 한글은 형태소 분석기 없이 글자 2-gram으로 자르고, 두 결과는 점수를 더하지 않고 순위로 합친다(RRF). 절대값은 못 믿어도 순위는 믿을 수 있다는 게 그날 얻은 결론이다. 응답에는 벡터 점수와 키워드 점수를 같이 실어, 어느 갈래가 잡았는지 보이게 했다.
지어내면 못 쓰는 기능
색인에서 근거를 뽑아 답하는 엔드포인트를 붙이면서 가장 신경 쓴 건 답을 잘 하는 게 아니라 못 할 때 답하지 않는 것이었다. 한 번 지어낸 답을 보면 그다음부터 전부 의심하게 되고, 의심하며 쓸 바에는 파일을 직접 여는 게 빠르다.
부르기 전에 근거의 세기를 잰다. 그 말이 문서에 글자 그대로 걸렸거나, 없더라도 뜻이 충분히 가깝거나. 답을 못 냈을 때는 오류 대신 이유를 돌려준다. 근거가 없어서 못 답한 것과 근거는 찾았는데 답을 못 만든 것은 화면에서 달라야 해서, 뒤엣것은 읽을 대목을 같이 넘긴다.
출처는 모델 말을 믿지 않는다. 실측에서 모델이 답은 맞게 하고 출처는 틀리게 적었다. 금액을 정확히 답해 놓고 근거로는 도입부 슬라이드를 가리켰는데, 그 조각에 그 숫자가 없었다. 뒷받침하지 않는 출처는 출처가 없는 것보다 나쁘다. 검증된 것처럼 보이기 때문이다. 그래서 번호 대신 원문 인용구를 받는다. 모델이 근거마다 원문 그대로의 대목을 옮겨 적으면, 그 글자가 실제로 어느 조각에 있는지 코드가 찾는다. 번호는 검증할 수 없지만 글자는 검증할 수 있다. 인용구가 원문에 없으면 지어낸 것으로 보고 답을 내보내지 않는다.
겹쳐 쌓은 방어를 하나 걷어냈다
방어를 하나 더 두고 있었다. 그림 캡션만 걸린 질문에는 아예 답하지 않는 규칙이었다. 캡션은 그림 묘사지 사실 진술이 아니니, 거기서 예산 숫자를 뽑는 걸 막자는 것이었다.
그런데 "테라리아컨셉 이미지 어디 있나"에 못 답했다. 그림은 캡션밖에 없다. 그림에 대한 질문을 구조적으로 막고 있었던 셈이다. 게다가 그 방어는 중복이었다. 나중에 붙인 인용구 검증이 같은 일을 더 정확하게 한다. 캡션에 예산 숫자가 없으면 모델은 그 숫자를 인용할 수 없고, 인용을 못 하면 답이 안 나간다. 방어를 둘 두느라 되는 것까지 막고 있었다.
원인이 하나 더 있었다. 캡션 청크에 캡션과 태그만 넣어서 "테라리아컨셉"이라는 글자가 색인 어디에도 없었다. 사람은 파일을 이름으로 기억하는데, 캡션은 AI가 쓴 글이라 사용자가 아는 말과 다르다. 이름과 분류를 색인에 같이 넣었다. 둘 다 내가 만든 제약이었다는 게 이 건의 요지다.
문서 검색기에서 어시스턴트로
"안녕?"에도 "자원 방 문서 1건을 뒤졌지만 근거가 될 대목이 없었습니다"라고 답했다. 어시스턴트인데 문서 검색기로 굴러가고 있었다.
"근거 없으면 답하지 않는다"는 회사 안 사실을 물었을 때만 맞는 규칙이다. 인사와 일반 지식에까지 걸면 아무것도 못 하는 물건이 된다. 그래서 검색 도구를 하나 쥐여주고 부를지 말지를 모델이 정하게 했다. 부르면 근거와 출처가 따라오고, 안 부르면 그냥 답한다. 그때는 출처가 없는 게 맞다. 대신 회사 안 사실은 자기 지식으로 답하지 말고 반드시 찾아보라고 못 박았다.
방도 하나로 묶지 않았다. "디자인팀 김철수 전화번호"는 조직 색인에 있고 "판교 이전 비용"은 자료 색인에 있는데, 사용자는 그 구분을 모르고 묻는다. 볼 수 있는 색인을 한꺼번에 뒤진다. 키워드 색인은 합친 코퍼스 하나로 만든다. 색인마다 따로 만들면 희소도 기준이 달라져 점수를 나란히 놓을 수 없다.
되묻는 질문도 처리해야 했다. "그럼 그 사람 자리는?"을 그대로 임베딩하면 아무것도 안 걸린다. 그 문장에 찾을 낱말이 하나도 없기 때문이다. 앞 대화를 보고 혼자 뜻이 통하는 검색어로 다시 쓴 뒤에 찾는다. 재작성이 실패하거나 장황해지면 원문을 쓴다.
부르는 쪽에는 끝까지 청크 이야기를 안 한다. 그림 두 장에 「본문」 배지가 붙는 사고가 났을 때도, 부르는 쪽이 청크 번호로 판단하게 두는 대신 서버가 판정해서 제품이 쓰는 말로 실어 보냈다. 본문에 걸렸는지 AI 설명에 걸렸는지. 경계를 그어놓고 편할 때만 넘어가면 그건 경계가 아니다.
기술 스택
- 백엔드: Python 3.11 · FastAPI 0.115 · APScheduler(실질 스케줄러) · Meilisearch · numpy
- AI: anthropic 0.113 · openai 1.58 · langgraph 1.2 · langchain-core · mcp 1.12
- 모델:
claude-sonnet-4-6/gpt-4o· 경량claude-haiku-4-5· 이미지gpt-image-1 - 데이터: Firebase Firestore(25 컬렉션) · Storage · Meilisearch(키워드)
- 프론트: React 19 · Tauri 2 · Capacitor(iOS) · zustand · TypeScript
- 배포: Railway (uvicorn 단일 프로세스)
Celery+Redis는 스택에서 뺐다. 원래 알림 시스템을 Redis 브로커 + Celery Beat로 돌렸지만 APScheduler in-process로 이관했다. "죽은 코드"가 아니라 "쓰다가 걷어낸 코드"다.
왜 이 스택
기술은 "좋아서"가 아니라 "이 규모·맥락에 맞아서" 골랐다. 방어되는 이유만 적는다.
- FastAPI: 토큰 스트리밍(SSE)과 async가 native로 되고, AI SDK(anthropic·openai)가 전부 Python이라 백엔드도 Python으로 붙였다. Django보다 가볍고 async에 강하다.
- Firestore: 1인 프로젝트라 저장소 운영을 지고 싶지 않았다(관리형·서버리스). 인증을 이미 Firebase로 해서 per-user 보안 규칙으로 "내 데이터만 내가" 격리가 공짜였고, 형태가 제각각인 개인 데이터엔 문서형 스키마가 유연하다. 단 벡터 검색이 없어 브루트포스 코사인으로 우회했다. 개인 규모(몇 만 건)엔 순식간이지만 프로덕션 규모면 이 선택이 뒤집힌다(그땐 OpenSearch, 실제로 RagOps에서 그렇게 했다).
- Meilisearch: 벡터 검색과 별개로 정확한 키워드 검색이 필요했다. Elasticsearch는 개인 규모에 무겁고, Meilisearch는 가볍고 셋업이 빠르다.
- Tauri: 같은 웹 프론트를 데스크톱으로 재사용하되 Electron의 번들·메모리 부담을 피하려 Rust 기반 Tauri를 택했다. 하나의 프론트를 웹·데스크톱·iOS(Capacitor) 셋으로 재활용한다.
Trouble Shooting
- LangGraph
<thinking>누수. 사고 과정이 답변에 새거나 화면이 비었다.<thinking>블록을 분리 처리해 해결했다. - 추론 엔진에서 출처 뱃지 소실. 뱃지는 "LLM 단일턴 마커 생성" 파싱 구조인데 다단계 추론이 마커 보존을 깼다(버그가 아닌 구조 충돌). 라운드 스코프 분리(1툴 verbatim / 체이닝 종합)로 해결했다.
- GraphRAG 항상 빈 화면 → 진짜 원인은 상류. 화면 코드를 의심했지만, 로그를 보니 기억 추출기가 모델 응답을 그대로 파싱하는데 모델이
```json코드펜스로 감싸 모든 기억 추출이 실패하고 있었다(기억 0 → 그래프 항상 빔). 펜스를 제거해 막았다. 교훈은 이렇다. "화면이 비었다"의 원인이 화면 코드란 보장은 없다, 로그가 가리키는 상류를 봐라. - 새로 만든 검색이 403으로 죽었는데 원인이 코드가 아니었다. 본체 Firebase 프로젝트가 요금 문제로 삭제돼 있었고, 색인을 거기 두는 구조라 통째로 못 쓰게 된 것이다. 초기화 코드가 설정이 없어도 경고만 찍고 개발 모드로 넘어가는 바람에, 프로젝트가 사라진 사실을 남의 말로 된 403을 받고서야 알았다. 색인은 전용 프로젝트로 분리하고, 설정이 없으면 무엇이 없는지 이름을 대고 죽게 바꿨다.
- Claude 529 과부하가 엔진마다 다르게 새어 나왔다. 직접 구현한 native 경로는 3회 지수 백오프(1·2·4초)와 안내 문구를 갖췄는데, LangGraph 경로는 SDK 기본 재시도만 걸려 있어 과부하 응답을 그대로 사용자에게 덤프했다. SDK 재시도를 4회로 늘리고 529(서버 혼잡)와 429(요청 몰림)를 갈라 문구를 붙인 뒤, native 쪽 문구도 같은 케이스로 맞췄다. 엔진을 셋으로 늘리면 에러 처리도 세 벌이 된다는 유지비용을 여기서 치렀다.
Decision Log
프레임워크가 감추는 추론 루프를 직접 이해하려고. 함수콜링·스트리밍·RAG를 직접 만들면 원리가 남고, 새 프레임워크 내부가 먼저 보인다는 가설.
학습 목적으로 native로도, LangGraph로도 만들어 차이를 몸으로 겪었다. 결론은 native였다. 기능이 더 넓고(gpt·첨부 커버), 프롬프트 캐싱으로 더 싸고, 프레임워크가 주는 상태관리는 이미 Firestore가 하고 있어 더 얹을 게 없었다. "프레임워크는 마법이 아니라 도구."
전부 다단계 종합으로 통일하지 않았다. 1툴 라운드는 원문 그대로 인용 마커를 보존하고, 2라운드 이상만 종합한다. 기능별 요구가 다르면 경로를 가른다.
원래 카톡 알림을 Redis 브로커 + Celery Beat(60초 발행) + 워커로 돌렸다. 별도 워커·브로커가 떠 있어야만 알림이 나가는 의존성이 배포 환경에 과했다. 커밋 하나로 APScheduler in-process로 완전히 이관했다. API 프로세스 하나가 폴링·발송하도록 단순해졌다. 돌려보고 안 맞아서 걷어낸 판단이다.
18앱 모놀리식은 진입장벽이 높다. "캘린더 하나 써봐"가 허들이 낮고, 캘린더는 데이터 생성 빈도가 높아 AI 시연 효과가 즉각적이다.
AI 사이드바를 붙였다가 하루 만에 4,650줄을 들어냈다. 전략을 "앱=데이터 저장 / 런처=AI 인터페이스"로 정리했다. 앱마다 AI를 박는 대신 AI는 한 곳(런처)에 뒀다.
끝난 제품의 백엔드를 다른 제품이 갖다 쓰게 되면서 그은 경계. 임베딩 모델이나 청크 크기를 바꾸면 색인을 전량 다시 만들어야 하는데, 제품마다 색인을 들고 있으면 그때 제품을 하나씩 찾아다녀야 한다. 원본은 제품에 남기니 파이프라인을 갈아끼워도 잠기지 않는다.
코사인 절대값은 질의와 청크 길이에 좌우돼서 어떤 숫자를 박아도 정답을 자르고 오답을 통과시킨다. 실제로 0.276짜리 정답이 잘리고 0.361짜리 오답이 통과했다. 임계값 대신 벡터와 키워드의 순위를 융합하고, 판정은 점수가 아니라 근거를 보고 한다.
무엇이 과했나, 개인 AI OS의 함정
핵심 실패 명제는 README에 내가 직접 박아둔 그대로다: "모든 걸 넣어서 아무것도 아니게 됨."
- 24개 앱을 한 OS UI에 뭉쳐 진입장벽을 스스로 높였다.
- 프론트 코드의 약 42%가 진입로 없는 죽은 게임 코드였다. RE:CODE라는 38,910줄짜리 자체 2D 게임엔진이 여기 포함됐다. 그 콘셉트가 "AI에 대체된 개발자의 이세계 로그라이크"였는데, 공교롭게 이 프로젝트 자체의 은유가 됐다.
- 번들 하나라 메모 하나 고쳐도 전체 재배포. 웹 배포라 정작 사용자는 안 썼다.
집중을 시도했다(캘린더만 분리, AI 제거). 하지만 근본 과욕은 이미 굳어 있었고 결국 2026-04-09에 종료했다.
회고: 실패한 제품, 성공한 학교
한마디로 "실패한 제품이지만, 성공한 학교."
제품으로는 자기 진단 그대로다. 24개 앱, 프론트 42%가 진입로 없는 죽은 코드. "모든 걸 넣어서 아무것도 아니게 됨"은 변명이 아니라 정확한 부검 소견이다. 그런데 코드를 전수조사하고 나서 든 생각은, 이 프로젝트가 아까운 게 아니라 이 프로젝트를 통과한 사람이 남았다는 것이다.
프레임워크 없이 함수콜링·스트리밍·RAG를 직접 짜고, 그걸 LangGraph로 다시 짜서 나란히 비교하고, 그러고도 "프레임워크는 마법이 아니라 도구"라는 결론에 도달했다. 튜토리얼 따라 한 사람은 못 하는 궤적이다. Celery+Redis를 실제로 굴려보고 배포 환경에 안 맞으니 APScheduler로 걷어낸 판단, 청커 없이 가도 되는 규모를 알아본 감각. 다 "돌려보고 몸으로 안 것"들이다.
가장 값진 건 회고 자체다. 대부분은 실패를 미화하거나 지운다. 나는 README에 "모든 걸 넣어서 아무것도 아니게 됨"이라고 박아뒀고, 그 교훈을 다음 프로젝트 noon으로 실제로 이월했다. 확장이 아니라 집중으로. 틀린 걸 틀렸다고 쓸 수 있는 사람이 만든 포트폴리오는 화려한 것보다 믿음이 간다.
그래서 새김AI는 자랑이 아니라 "나는 이렇게 크게 실패했고, 정확히 무엇을 배웠고, 그래서 다음엔 이렇게 한다"의 증거로 여기 있다. 제품은 끝났지만 그 안에서 만든 검색 배관은 아직 굴러가고, 다음 도구들이 그 위에 올라탄다. 남은 게 제품이 아니라 부품이라는 게 이 프로젝트의 결론이다.