Bioinformatics

BioAI Market 1인 개발 회고 — 커밋 기록으로 다시 본 석 달

2025년 12월 19일 첫 커밋부터 2026년 3월까지, 웹 기반 오믹스 분석 플랫폼을 혼자 만들며 남긴 커밋 312개를 다시 읽었다. 시간이 어디에 들어갔는지, 다시 한다면 무엇을 다르게 할지 정리했다.

·15 min read
#1인개발#BioAI#Next.js#Supabase#회고#Claude Code

BioAI Market은 웹 브라우저에서 오믹스 데이터를 올리고 QC, DE, pathway 분석을 돌리는 플랫폼이다. 분석 파이프라인 설계는 프로테오믹스 분석 플랫폼을 직접 만들어봤다에 썼다. 이 글은 그 플랫폼을 혼자 만든 과정의 회고다.

예전에 다른 블로그에 회고를 한 편 올렸는데, 다시 보니 기억에 기대 쓴 숫자가 많았다. 함수 실행 시간, 실수한 횟수, AI가 코드의 몇 퍼센트를 썼는지 같은 것들이다. 코딩 도구 이름도 기록과 달랐다. 이번에는 기억 대신 git 기록을 다시 읽고 썼다. 이 글에 나오는 날짜와 숫자는 모두 커밋 기록에서 셌다.

무엇으로 만들었나

  • 프론트와 API: Next.js(현재 16.1), React 19, TypeScript, Tailwind, shadcn/ui. Vercel에 배포.
  • 인증과 DB: Supabase(PostgreSQL, pgvector).
  • 분석 백엔드: Python FastAPI 서버. R 패키지(limma, clusterProfiler 등)는 rpy2로 불렀다.
  • LLM: RTX 3090 서버의 Ollama. 기본 모델은 2월 15일부터 qwen3:30b.
  • 코딩 도구: Claude Code. 커밋 47개에 "Generated with Claude Code" 줄이 남아 있고, 12월 22일에는 세션이 바뀌어도 작업 맥락을 이어 가려고 claude.md를 저장소에 넣었다.

커밋 312개의 분포

달커밋 수
2025년 12월25
2026년 1월23
2026년 2월215
2026년 3월39
2026년 4월0
2026년 5월10

첫 커밋은 2025년 12월 19일의 "Initial commit from Create Next App"이다. 다음 날 "BioAI Market 전체 기능 구현"이 올라간다. 커밋의 3분의 2가 2월 한 달에 몰려 있다. 2월 25일 하루에만 49개다. 앱 기능을 만들거나 고친 커밋은 3월 중순(3월 13일 에이전트, 3월 16일 메인 페이지 수정)이 마지막이다. 그 뒤로는 광고 스크립트 경고 수정, 페이지 정리, 검색엔진 설정 커밋만 있다.

3월 16일까지의 커밋 296개 가운데 147개가 "fix"로 시작한다. 2월만 보면 215개 중 120개다. 절반 가까이가 고치는 커밋이었다.

범위: 만든 것이 너무 많았다

12월과 1월 커밋 제목만 훑어도 이 정도다.

  • 12월 20일: 플랫폼 전체 기능, 태그 필터, Python·NGS·AI 교육 과정 Level 1~4, 과정 소개 페이지
  • 12월 22일: 블로그 관리 시스템, 바이오마커 DB, 분석 대시보드, 요금제, 서비스 소개, 웹툰(OmicsToon), 샘플 리포트
  • 12월 30일: 근거 집계 시스템, 자동 주석(Auto-Annotation) 엔진, 관리자 사용자 관리
  • 1월 1~2일: 상용 키트(assay) DB, RAG 시스템, AI 채팅, 패널 빌더
  • 1월 9~16일: SSE 스트리밍 AI 에이전트와 PDF 리포트, 코드 실행 샌드박스가 붙은 자율 에이전트, Ensembl API 연동 에이전트
  • 1월 28~29일: AI Co-Scientist 연구 플랫폼, OpenAI·Anthropic·Ollama 다중 LLM

2월과 3월에는 여기에 에이전트 V2(코드 인터프리터, 2월 27일)와 V3(도구 사용, 2월 28일), RNA-seq·대사체 파이프라인, 머신러닝 기반 탐색, 공개 DB 가져오기(3월 7~9일)가 더해진다. 결제 연동(Stripe) API도 있다.

에이전트는 두 달 사이에 세 번 새로 만들었다. 3월 13일 마지막 에이전트 커밋의 제목은 "Agent V3 API - simple and reliable implementation"이다. 결국 단순한 쪽으로 돌아왔다.

커밋 기록을 다시 보면 기능을 붙이는 속도가 그 기능을 검증하는 속도보다 빨랐다. 아래 문제들은 대부분 그 차이에서 나왔다.

시간이 들어간 곳 1: 서버리스 시간 제한

분석 API는 vercel.json에서 함수 실행 시간이 60초로 묶여 있었다. 프로테오믹스 분석을 웹 요청 하나 안에서 끝내려던 설계가 여기에 계속 걸렸다.

  • 2월 18일: "split analysis into steps to avoid Vercel 60s timeout", 같은 날 비동기 작업 큐 도입
  • 2월 24일: "step-by-step save to avoid 504 timeout", 진행 표시와 재시도를 붙인 작업 큐
  • 2월 25일: 주석 처리를 GPU 서버 백엔드로 넘김("to avoid Vercel serverless timeout"), 큰 결과를 Vercel을 거쳐 전달하지 않게 바꿈
  • 3월 13일: 작업 큐를 끔("disable job queue")

무거운 계산은 처음부터 별도 백엔드에서 돌리고, 웹은 작업 상태만 묻게 설계했어야 했다. 결국 그렇게 됐지만, 거기까지 가는 데 커밋이 많이 들었다.

시간이 들어간 곳 2: 데이터 저장소 두 곳

2월 24일부터 분석 결과와 바이오마커 데이터를 Supabase에서 GPU 서버의 로컬 PostgreSQL로 옮기기 시작했다("hybrid DB - local PostgreSQL for results, Supabase Cloud fallback"). 커밋이 가장 많았던 2월 25~26일이 이 작업이다. 한쪽에서 읽고 다른 쪽에 쓰는 경로가 섞이면서 이런 커밋이 줄줄이 나왔다.

  • "handle PostgreSQL array strings in RTX biomarker response"
  • "convert Decimal strings to numbers in annotation results API"
  • handle non-array diseases field from RTX DB (string '{}' → [])
  • "fetch project biomarker details from RTX local DB instead of Supabase Cloud JOIN"

같은 데이터가 두 곳에 있고, 두 곳의 드라이버가 같은 값을 다른 타입으로 돌려준다. 그러면 화면마다 방어 코드가 붙는다. 3월 9일에는 "comprehensive defensive fallbacks on all potentially undefined arrays"라는 커밋까지 나온다. 저장소는 하나로 정하고 시작하는 게 맞았다.

시간이 들어간 곳 3: 프론트엔드 상태

React 상태 관리에서도 시간이 많이 들었다. 전처리 옵션 컴포넌트(PreprocessingOptions)의 무한 렌더링을 2월 21일부터 23일까지 사흘 동안 커밋 아홉 개로 고쳤다.

  • 2월 21일: "prevent useEffect infinite loop in PreprocessingOptions (useRef for callback)"
  • 2월 21일: "remove useEffect render loop, revert sed variable renames, use direct config.* access"
  • 2월 22일: "restore all config.* references broken by sed"
  • 2월 22일: "use useRef+useCallback for preprocessing config to eliminate render loop entirely"
  • 2월 23일: "move onOptionsChange out of setState updater to prevent infinite loop (React #185)"
  • 2월 23일: "rewrite: PreprocessingOptions - simple Auto/Manual toggle, no expand/recommend, no useEffect"

(나머지 세 개도 같은 루프를 다른 방법으로 막으려던 커밋이다.) 중간에는 변수 이름을 sed로 일괄 변경했다가 참조가 깨져서 되돌린 일도 있다. 22일 커밋 제목에 "entirely"라고 썼지만 다음 날 커밋이 네 개 더 있다. 마지막 해법은 기능을 줄인 것이다. 추천과 펼치기 옵션을 없애고 자동/수동 토글 하나만 남겼다.

2월 14일에는 인증 컨텍스트가 사용자 정보를 네 번씩 중복 조회하던 것을 고쳤다("prevent 4x duplicate API calls"). 같은 날 페이지 미리 불러오기(prefetch)를 끄려고 Next.js Link를 일반 a 태그로 바꾸는 커밋도 여럿 있다. 2월 16일에는 대시보드가 계속 새로고침되는 루프를 고쳤다.

시간이 들어간 곳 4: 필드 이름

DE 결과는 log2FC, pValue로 저장되는데, TypeScript 쪽 여기저기서는 log2fc, pvalue, p_value를 읽었다. pathway 결과도 enrichedPathways와 enrichment_results가 섞였다. 2월 16일, 2월 27일에 같은 종류의 커밋이 반복된다. 지금 코드에도 흔적이 남아 있다.

// 저장소 src/lib/agents/tools/executors.ts 에서 발췌
log2fc: (r.log2FC as number) ?? (r.log2fc as number) ?? 0,
pvalue: (r.pValue as number) ?? (r.pvalue as number) ?? (r.p_value as number) ?? 1,

이 실수는 표시 오류로 끝나지 않았다. LLM에게 넘기는 프롬프트에 실제 값 대신 0과 N/A가 들어갔고, LLM은 그걸 보고 "분석 실패"라는 설명을 썼다. 이 이야기는 로컬 LLM에게 분석 결과 설명을 맡겼다가 템플릿으로 바꾼 이유에 따로 정리했다. 백엔드의 응답 형식을 한 곳에서 정의하고(예를 들어 FastAPI의 Pydantic 모델에서 OpenAPI 스키마를 뽑고, 거기서 TypeScript 타입을 생성하는 식), 프론트는 그 타입만 쓰게 했어야 했다.

시간이 들어간 곳 5: 데이터

바이오마커 DB는 1월 6일에 크게 늘렸다. 그날 커밋에는 같은 대량 삽입 마이그레이션이 네 벌(원본, FIXED, v2, v3) 들어 있다. 타입 변환 오류와 중복 삽입을 차례로 고친 흔적이다. 다시 읽어 보니 같은 날 들어간 연관 관계 마이그레이션 일부는 disgenet_score 같은 점수 칸을 random()으로 채우고 있었다. 이 점수가 나중에 결과 화면에 그대로 찍힐 수 있는 구조였다. 자세한 내용은 바이오마커 DB에 시맨틱 검색을 붙이며 배운 것에 적었다.

순서가 거꾸로였던 것

2월 15일 커밋에서 앱이 GPU 서버의 Ollama를 외부 주소로 부르도록 바꿨다. 그 앞단 프록시에서 API 키를 확인하게 한 커밋은 2월 20일이다("Add Ollama API key auth (X-API-Key header via Caddy proxy)"). 모델 서버를 바깥에서 부를 수 있게 하는 일과 인증을 붙이는 일은 같은 커밋에 들어갔어야 했다.

다시 한다면

  1. 기능 수를 정하고 시작한다. 교육 과정, 웹툰, 블로그, 요금제, 에이전트 세 벌을 분석 플랫폼과 같은 석 달에 만들 이유가 없었다. 분석 하나를 끝까지 검증하는 쪽을 먼저 했어야 했다.
  2. 무거운 계산은 처음부터 백엔드와 작업 큐로. 웹 요청 안에서 분석을 끝내려는 설계는 시간 제한에 계속 걸린다.
  3. 저장소는 하나. 같은 데이터를 두 곳에 두면 타입 변환과 폴백 코드가 화면마다 붙는다.
  4. 백엔드와 프론트가 같은 스키마를 쓴다. 필드 이름이 하나라도 다르면 그 오류가 LLM 출력까지 번진다.
  5. 시드 데이터를 코드처럼 검사한다. 점수 칸에 무엇이 들어 있는지, 넣으려던 건수와 들어간 건수가 같은지 확인한다.
  6. LLM이 내는 숫자는 쓰지 않는다. 숫자는 코드가 찍고, LLM 문단은 표시를 붙인다.
  7. AI 코딩 도구가 만든 변경도 내가 검증한다. 빨리 만들 수 있다는 것과 맞게 만들었다는 것은 다른 문제다.

커밋 기록으로 보면 이 프로젝트의 기능 개발은 3월 중순에 멈췄다. 그래도 석 달 동안 남긴 기록 덕분에, 기억으로 쓴 회고보다 정확한 회고를 쓸 수 있었다.

관련 글