혼자하는 사이드프로젝트에서 AI와 함께 리팩토링 진행하기

코드가 점점 복잡해지는 사이드프로젝트, AI 모델 4개에게 아키텍처 토론을 시켜봤습니다.

사이드프로젝트
Side Project, AI, Architecture, Refactoring

Blog Tone Lab 에서 오롯이 라는 앱으로 열심히 개편하고 있어요. 🤭

혼자 사이드프로젝트를 하다 보면, 처음에는 빠르게 만드는 데 집중하게 됩니다.
”일단 돌아가게 만들자”는 그 순간엔 합리적인 선택입니다.

문제는 그 “일단”이 쌓이면서 옵니다.
tRPC를 도입했다가, SSE 스트리밍이 필요해서 Route Handler를 추가했다가, 급한 기능은 raw fetch로 때웠다가.
어느 순간 같은 API를 호출하는 방식이 3가지가 되어있었습니다.

리팩토링이 필요하다는 건 알겠는데, 혼자 하니까 문제가 있습니다.
아키텍처 결정을 같이 논의할 동료가 없습니다.
그래서 AI 모델 4개에게 물어보고, 합의가 나온 뒤에는 Devil’s Advocate 역할의 에이전트를 추가로 투입해서 결론을 검증했습니다.


현재 상태: 3가지 패턴이 공존하는 HTTP 통신

먼저 현재 프로젝트의 HTTP 통신 구조를 정리하면 이렇습니다.

패턴 1: tRPC

[브라우저] → tRPC client → [/api/trpc] → procedure → [서버 로직]

tRPC는 프로젝트 초기에 타입 안전성을 위해 도입했습니다.
3개 라우터에 7개 프로시저를 정의해뒀는데, 시간이 지나면서 실제로 사용하는 건 딱 2곳 이 되었습니다.

패턴 2: raw fetch (14곳)

[브라우저] → fetch → [app/api/*/route.ts] → [외부 서비스]

SSE 스트리밍이 필요한 AI 채팅 기능은 tRPC로 처리할 수 없어서 Route Handler를 만들었고, 그 외 기능들도 하나씩 raw fetch로 추가되었습니다.
매번 headers, JSON.stringify, res.ok 체크, res.json() 파싱이 반복됩니다.

패턴 3: SSE 스트리밍

[Claude API] → SSE → [Next.js Route Handler] → SSE → [React Client]

제가 진행중인 사이드프로젝트의 핵심 UX인 AI 토큰 스트리밍입니다.
58줄짜리 SSE 파싱 상태 머신이 여러 곳에서 중복되고 있었습니다.

숫자로 보는 현실

항목수치
tRPC 인프라 코드537줄
tRPC 실제 사용처2곳 (14.3%)
raw fetch 반복14곳
tRPC 번들 사이즈89KB (gzipped)
SSE 파싱 중복58줄 × 여러 곳

537줄의 인프라가 2개의 mutation 호출을 지원하고 있습니다.


AI 4명에게 아키텍처 토론을 시키다

4가지 선택지를 정리하고, 4개 AI 모델에게 각자의 의견을 물었습니다.

옵션전략
A. tRPC 올인모든 API를 tRPC로 통일
B. Route Handler 올인tRPC 제거, raw fetch로 통일
C. Route Handler + 공유 HTTP 클라이언트tRPC 제거 + apiGet/apiPost/apiSSE 헬퍼 도입
D. Route Handler + Server Actions스트리밍은 Route Handler, 나머지는 Server Actions

각 모델에게 프로젝트의 전체 코드를 컨텍스트로 제공하고, 자유롭게 의견을 내고 서로 반박하게 했습니다.

투표 결과

AI 모델선택한줄 요약
GPT-5.3옵션 C”제품의 심장박동이 SSE다. Route Handler 축 고정”
Gemini 3 Pro옵션 D”Next.js App Router의 Native 패턴이 유일한 정답”
Claude Opus 4.6옵션 C”단일 통신 채널 원칙. Server Actions는 admin 인증과 충돌”
Claude Sonnet 4.6옵션 C”Server Actions는 영원히 불필요. 옵션 C 올인”

3:1로 옵션 C 압승. 그리고 하나의 만장일치 합의가 있었습니다.


만장일치: “tRPC는 즉시 제거해야 한다”

4개 모델 모두, 예외 없이 tRPC 제거에 합의했습니다.

근거는 명확했습니다.

  • 537줄 인프라가 2개 호출만 지원 (사용률 14.3%)
  • SSE 스트리밍과 호환되지 않아서 결국 Route Handler를 병행해야 함
  • 89KB 번들 사이즈가 2개의 mutation을 위해 존재
  • tRPC 미들웨어와 Route Handler에서 admin 인증 방식이 서로 다름 (보안 이슈)

마지막 항목은 제가 미처 인식하지 못하고 있던 문제였습니다.

// tRPC: 이메일 기반 인증
const adminProcedure = protectedProcedure.use(async ({ ctx, next }) => {
  const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
  if (!adminEmails.includes(ctx.user.email)) {
    throw new TRPCError({ code: 'FORBIDDEN' });
  }
  return next();
});
 
// Route Handler: 비밀번호 기반 인증
const response = await fetch('/api/admin/whitelist', {
  headers: { 'X-Admin-Password': password },
});

같은 “관리자 인증”인데 메커니즘이 완전히 다릅니다.
하나가 뚫려도 다른 하나는 안전할 수 있지만, 이건 의도된 설계가 아니라 각각 따로 만들다 보니 생긴 불일치 였습니다.

혼자 코딩하면 이런 것들을 놓치기 쉽습니다. 코드를 작성할 땐 해당 파일에만 집중하고 있으니까요.


3:1로 갈린 핵심 쟁점: Server Actions

tRPC 제거는 합의했지만, “그 다음에 뭘 쓸 것인가” 에서 의견이 갈렸습니다.

Gemini 3 Pro (단독): “Server Actions가 미래다”

Gemini는 Next.js App Router의 네이티브 패턴을 따라야 한다고 주장했습니다.

  • 데이터 조회 → Server Components에서 직접 DB 접근 (fetch 자체가 불필요)
  • 데이터 변경 → Server Actions (타입 안전성 자동 제공)
  • AI 스트리밍만 → Route Handler 유지

프레임워크의 설계 의도에 가장 충실한 방향이었습니다.

Team C (GPT-5.3 + Opus + Sonnet): “이 프로젝트에서는 안 맞는다”

나머지 3개 모델이 공통적으로 지적한 반론이 있었습니다.

admin 인증 방식이 Server Actions와 호환되지 않습니다.

// 현재 admin 인증: HTTP 헤더로 비밀번호 전송
const response = await fetch('/api/admin/whitelist', {
  headers: {
    'X-Admin-Password': password,  // ← Server Actions에서는 보낼 수 없음
  },
});

Server Actions는 함수 호출이지, HTTP 요청이 아닙니다. 헤더를 직접 제어할 수 없습니다.
Server Actions로 전환하려면 admin 인증 아키텍처 자체를 세션 기반으로 재설계 해야 합니다.

이건 “마이그레이션”이 아니라 “재구현”이었습니다.

추가로 지적된 현실적 제약들

  • 이 프로젝트의 80%가 Client Component 중심 (채팅 UI + admin 페이지)
  • Server Actions를 도입해도 SSE 스트리밍에는 여전히 Route Handler가 필요
  • 결국 3가지 패턴이 공존하게 됨 → 지금의 문제를 해결하는 게 아니라 분산시킴

Gemini의 논리는 이론적으로 깔끔했지만, 이 프로젝트의 현실 조건과 맞지 않았습니다.


합의된 방향: 공유 HTTP 클라이언트

최종적으로 3개 모델이 합의한 구체적 실행 계획입니다.

공유 HTTP 클라이언트 도입

// shared/api/http-client.ts
export async function apiGet<T>(endpoint: string, config?: ApiConfig): Promise<T> {
  /* 공통 에러 처리 + JSON 파싱 */
}
 
export async function apiPost<T>(endpoint: string, body?: unknown, config?: ApiConfig): Promise<T> {
  /* 공통 헤더 + 에러 처리 + JSON 파싱 */
}
 
// Admin 전용 (X-Admin-Password 헤더 자동 부착)
export function createAdminClient(password: string) {
  const headers = { 'X-Admin-Password': password };
  return {
    get: <T>(endpoint: string) => apiGet<T>(endpoint, { headers }),
    post: <T>(endpoint: string, body?: unknown) => apiPost<T>(endpoint, body, { headers }),
  };
}
 
// SSE 스트리밍 전용
export async function apiSSE(
  endpoint: string,
  body: unknown,
  callbacks: { onToken: (text: string) => void; onDone?: () => void; onError?: (msg: string) => void }
): Promise<string> {
  /* 58줄 SSE 상태 머신 캡슐화 */
}

14곳에서 반복되던 fetch 보일러플레이트가 한 곳으로 모이고, SSE 파싱 로직의 중복도 사라집니다.

예상 결과

항목BeforeAfter
코드 줄 수~1,200줄~600줄 (-50%)
의존성tRPC 4개 패키지0개
번들 사이즈+89KB0KB
HTTP 통신 패턴3가지1가지

Round 2: Devil’s Advocate — “정말 이게 맞아?”

4개 모델이 합의했으니 끝인가 싶었지만, 한 가지가 찜찜했습니다.
AI 모델들이 비슷한 학습 데이터에서 비슷한 편향을 공유하고 있다면, 4개가 동의한다고 해서 정말 맞는 건 아닐 수 있습니다.

그래서 5번째 에이전트를 투입했습니다.
앞선 4개 모델의 토론 결과 전체를 컨텍스트로 넘기고, “이 합의에서 허점을 찾아라. 반드시 반대 입장에서 논증하라” 라는 역할을 부여했습니다.

AI 모델역할목표
Claude Opus 4.6Devil’s Advocate합의된 결론의 허점과 리스크를 찾아내기

Devil’s Advocate가 제기한 반론은 3가지였습니다.

반론 1: “tRPC 제거가 정말 답인가?”

537줄이 많아 보이지만, 그 537줄이 런타임 버그를 막아주고 있었을 수도 있다. tRPC를 제거하고 apiPost<ReviewResponse>('/api/reviews/edit', params) 같은 제네릭 호출로 바꾸면, 엔드포인트 오타 하나로 런타임에 터지는 버그가 생길 수 있다. tRPC가 제공하던 E2E 타입 안전성의 가치를 과소평가하고 있지 않은가?

유효한 지적이었습니다. 다만 현실적으로 따져보면,

  • tRPC의 타입 안전성은 7개 프로시저 중 2개에서만 실제로 작동하고 있었음
  • 나머지 14곳의 raw fetch는 이미 타입 안전성 없이 돌아가는 중
  • Zod 스키마 + 제네릭 HTTP 클라이언트로 “완벽하진 않지만 충분한” 수준은 확보 가능

판정: 이론적으로는 맞지만, 이미 14곳이 타입 안전성 없이 동작하는 현실에서는 설득력이 약했습니다.

반론 2: “공유 HTTP 클라이언트가 또 다른 기술 부채가 되지 않을까?”

직접 만든 래퍼는 시간이 지나면 또 다른 관리 대상이 된다. apiGet, apiPost, apiSSE 헬퍼를 만들었다가, 나중에 “이 헬퍼가 너무 많은 걸 하고 있어서 리팩토링해야 해” 라는 상황이 오지 않을까? tRPC를 “과한 추상화”라고 비판하면서, 또 다른 추상화를 만드는 것 아닌가?

솔직히 찔리는 반론이었습니다. 하지만 핵심적인 차이가 있었습니다.

  • tRPC: 537줄 인프라 + 4개 외부 의존성 + 별도 학습 비용
  • 공유 HTTP 클라이언트: ~220줄 + 외부 의존성 0개 + fetch를 아는 사람이면 즉시 이해

판정: 추상화의 두께가 다릅니다. 얇은 래퍼가 두꺼운 프레임워크보다 기술 부채가 될 가능성이 낮습니다.

반론 3: “4개 AI가 동의하면 맞는건가?”

AI 모델들은 공통적으로 “의존성 줄이기”, “단순화”, “최신 패턴” 을 선호하는 편향이 있다. 4개 모델이 tRPC 제거에 합의한 건, 실제로 맞아서가 아니라 모델들이 공유하는 편향 이 같은 방향을 가리킨 것일 수 있다.

이 반론이 가장 메타적이면서 가장 중요했습니다.
실제로 AI 모델들은 “복잡한 것을 단순화하라”는 방향으로 기울어지는 경향이 있습니다. 그래서 이 부분은 편향이 아닌 숫자 로 검증했습니다.

  • 537줄 인프라 → 2곳 사용: 편향이 아니라 사실
  • 89KB 번들 → 2개 mutation: 측정 가능한 사실
  • 인증 이중 구현: 코드를 직접 확인한 사실

판정: 편향일 수 있다는 의심은 건강하지만, 근거가 사실에 기반하고 있다면 결론은 유효합니다.

Devil’s Advocate를 거치고 나서

3가지 반론 중 합의를 뒤집을 만한 것은 없었습니다.
하지만 이 과정이 무의미하진 않았습니다. 반론 2 덕분에 “공유 HTTP 클라이언트를 최대한 얇게 유지해야 한다” 는 설계 원칙이 하나 더 생겼고, 반론 3 덕분에 “AI의 합의를 사실 기반으로 검증하는 습관” 이 중요하다는 걸 체감했습니다.

찬성만 모아서는 좋은 결정을 내리기 어렵습니다. 의도적으로 반대 의견을 만들어내는 과정이 있어야 결론에 대한 확신이 생깁니다.


AI와 리팩토링하면서 느낀 것들

혼자 코딩할 때 놓치는 것들을 잡아준다.

가장 큰 수확은 인증 이중 구현 문제의 발견이었습니다.
tRPC는 이메일 기반, Route Handler는 비밀번호 기반으로 admin 인증이 각각 구현되어 있었는데, 이건 코드를 한 파일씩 작성하면서는 인식하기 어려운 문제였습니다.

AI는 프로젝트 전체를 한 번에 보기 때문에, 파일 간 불일치전체 아키텍처 수준의 문제 를 찾아내는 데 강합니다.

여러 모델을 비교하면 더 입체적인 시각을 얻는다.

4개 모델 중 3개가 같은 결론을 내리면, 단일 모델의 답변보다 신뢰도가 높아집니다. 동시에 Gemini처럼 다른 의견을 내는 모델이 있으면 “이 부분은 한 번 더 생각해봐야 하는구나” 라는 신호가 됩니다.

실제로 Gemini의 “Server Actions가 미래” 라는 주장은 틀린 게 아니었습니다. 다만 지금 이 프로젝트의 조건에서는 맞지 않을 뿐입니다. 이런 구분은 단일 모델에게만 물어봤으면 놓칠 수 있었습니다.

모델마다 편향이 있다.

흥미로웠던 건 각 모델의 성격 차이입니다.

  • Gemini: 프레임워크의 공식 패턴에 충실한 경향. “Next.js가 제공하는 걸 왜 안 써?” 라는 방향
  • GPT-5.3: “제품 중심 의사결정” 이라는 키워드를 반복. 실용주의적
  • Claude Opus: 보안 아키텍처와 타입 시스템에 집중. admin 인증 충돌을 최초로 지적
  • Claude Sonnet: 코드량과 DX를 수치로 비교. “Server Actions는 코드를 줄이지 않는다”를 숫자로 증명

어떤 모델이 “맞다”가 아니라, 각각의 관점이 겹치는 부분 이 가장 신뢰할 수 있는 결론이었습니다.

최종 결정은 여전히 사람의 몫이다.

AI가 아무리 좋은 분석을 해줘도, “지금 이 시점에 뭘 먼저 할 것인가” 는 프로젝트의 맥락을 아는 사람만이 판단할 수 있습니다.

4개 모델 모두 tRPC 제거에 합의했지만, 실제로 실행할 때는 제가 프로젝트의 우선순위, 일정, 리스크를 고려해서 순서를 정해야 합니다. AI는 좋은 재료를 주지만, 요리는 결국 제가 해야 합니다.


정리

혼자 하는 사이드프로젝트에서 아키텍처 결정이 어려울 때, AI 여러 모델에게 의견을 구하는 방식은 꽤 효과적이었습니다.

핵심은 한 모델에게 답을 구하는 게 아니라, 여러 모델의 의견을 교차 검증하는 것 입니다. 만장일치로 합의한 부분은 높은 확신을 갖고 진행할 수 있고, 의견이 갈린 부분은 더 신중하게 검토할 수 있습니다.

이번에 얻은 구체적인 액션

  1. tRPC 즉시 제거 — 537줄 인프라, 89KB 번들, 2곳만 사용
  2. 공유 HTTP 클라이언트 도입 — 14곳 raw fetch 보일러플레이트 제거
  3. SSE 헬퍼 캡슐화 — 58줄 상태 머신 중복 제거
  4. 인증 방식 통일 — tRPC/Route Handler 이중 구현 해소

애정이 가득한 사이드프로젝트라서 그런지 AI에게 맡기고 코드 한 줄도 안읽기엔 너무 아쉽더라구요.
요즘 저에대한 부족함을 많이 느끼는 날들을 보내서 그런지, 이런 코드가 저에게 하나 하나의 인사이트가 되어주고 있어요.
코드 리팩토링을 마친 후에는 기획을 좀 더 탄탄하게 잡아서 개발을 빠르게 한뒤에 얼른 소개를 시켜드리고 싶습니다.

OratI 홈페이지

관련 글