
이 글을 쓰게 된 이유: AI가 코드는 잘 짜는데, API에서 자꾸 삐끗하더라고요
요즘 주변 개발자들이랑 이야기하다 보면 “바이브 코딩 해봤어?”라는 말을 꽤 자주 듣습니다. 저도 뭐, 궁금한 건 못 참는 편이라 GitHub Copilot이랑 Cursor를 업무 중간중간 꽤 적극적으로 써보고 있어요. 처음엔 신기했습니다. 진짜로요. 예전 같으면 손으로 한참 쳤을 보일러플레이트 코드를 AI가 순식간에 만들어주니까, 약간 옆자리 주니어가 엄청 빠르게 초안을 던져주는 느낌이랄까요.
그런데 며칠 써보니 묘한 불안감이 생기더라고요. 화면 컴포넌트나 간단한 유틸 함수는 꽤 괜찮은데, API 연결 부분에서 종종 허술한 코드가 나왔습니다. 타입이 느슨하게 잡히거나, 서버 응답 구조를 AI가 자기 마음대로 추측하거나, 런타임에서야 “아, 이 필드 없네?” 하고 터지는 식이었죠. 개발 오래 하신 분들은 아실 거예요. 이런 에러는 작을 때는 귀엽지만, 서비스 커지면 진짜 사람 피곤하게 만듭니다.
그래서 이번에 한 프로젝트에서 AI 바이브 코딩과 tRPC를 같이 써봤습니다. tRPC가 가진 타입 안전 API라는 장점이 AI가 만든 코드의 빈틈을 얼마나 막아줄 수 있을지 궁금했거든요. 20년 가까이 개발하면서 제가 점점 더 강하게 믿게 된 게 하나 있는데요. 빠른 것도 좋지만, 결국 오래 가는 코드는 “믿을 수 있어야” 하더라고요. 이 글은 그런 관점에서 제가 직접 부딪혀본 이야기입니다.
tRPC를 붙였더니 AI 코드가 조금 더 믿음직해졌습니다
tRPC를 아주 간단히 말하면, 서버에 있는 TypeScript 함수의 타입을 클라이언트에서도 그대로 가져다 쓸 수 있게 해주는 도구입니다. REST API처럼 요청 URL을 따로 맞추고, 응답 타입을 프론트에서 다시 선언하고, 문서랑 실제 코드가 달라져서 서로 눈치 보는 그런 일이 확 줄어듭니다. GraphQL처럼 스키마를 따로 운영하는 방식과도 조금 다르고요.
쉽게 말하면 이런 느낌이에요. 서버에서 만든 함수를 클라이언트에서 마치 직접 호출하듯 쓰는데, 그 사이에 TypeScript 타입 추론이 쫙 이어집니다. 저는 이 부분이 AI 코딩 도우미랑 만나면 꽤 재미있는 조합이 된다고 봤어요. AI가 코드를 만들더라도 타입 시스템이 옆에서 계속 “야, 그건 아니야” 하고 잡아주는 구조가 되니까요.
처음에는 솔직히 반신반의했습니다. “AI가 tRPC 패턴을 제대로 이해할까?” 싶었거든요. 그런데 생각보다 잘하더라고요. Cursor에 “사용자 목록을 가져오는 tRPC API를 만들어줘. Zod로 input 검증도 넣어줘”라고 요청했더니, Router, Procedure, Zod 스키마까지 꽤 그럴듯하게 잡아줬습니다. 물론 완벽하진 않았습니다. 가끔 이상한 import를 넣거나, 프로젝트 버전에 안 맞는 예전 문법을 가져오기도 했어요. 그래도 기본 뼈대는 생각보다 괜찮았습니다.
뭐랄까, AI에게 “대충 API 만들어줘”라고 던졌을 때보다, tRPC라는 틀을 미리 깔아놓고 “이 규칙 안에서 만들어줘”라고 했을 때 결과물이 훨씬 안정적으로 나왔습니다. 이게 제가 느낀 가장 큰 차이였어요.
간단한 Todo 앱에서 먼저 테스트해봤습니다
처음부터 큰 프로젝트에 들이붓기는 좀 겁나잖아요. 저도 이제 막 무모하게 새 기술 넣고 야근하는 나이는 지나서요. 그래서 먼저 아주 작은 Todo 리스트 앱으로 테스트했습니다. 백엔드는 Next.js와 tRPC, 프론트엔드는 React와 tRPC Client를 붙였습니다. DB 쪽은 Prisma를 사용했고요.
아래 코드는 Cursor가 만들어준 코드에 제가 살짝 손본 버전입니다. 실제로 이런 정도는 AI가 꽤 빠르게 만들어줍니다.
// server/routers/todo.ts
import { z } from 'zod';
import { publicProcedure, router } from '../trpc';
export const todoRouter = router({
getAll: publicProcedure.query(async () => {
const todos = await prisma.todo.findMany();
return todos;
}),
add: publicProcedure
.input(z.object({ title: z.string().min(1) }))
.mutation(async ({ input }) => {
const newTodo = await prisma.todo.create({
data: { title: input.title },
});
return newTodo;
}),
});
// client/components/TodoList.tsx
import { trpc } from '../utils/trpc';
export function TodoList() {
const { data: todos, isLoading } = trpc.todo.getAll.useQuery();
const addMutation = trpc.todo.add.useMutation();
if (isLoading) return <div>로딩 중...</div>;
return (
<ul>
{todos?.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
여기서 제가 제일 마음에 들었던 건 타입이 자연스럽게 이어진다는 점이었습니다. todos가 그냥 막연한 배열이 아니라, Prisma에서 가져온 Todo 타입으로 추론됩니다. addMutation에 넘기는 input도 Zod 스키마에 맞춰 검증되고요.
보통 AI가 만들어주는 fetch 기반 코드를 보면 이런 식으로 흘러갈 때가 있습니다. 서버 응답을 대충 any로 받고, 프론트에서 느낌으로 필드를 꺼내 쓰고, 나중에 데이터 구조 바뀌면 화면에서 조용히 깨지는 거죠. 저는 이런 코드가 제일 무섭습니다. 당장은 잘 돌아가는데, 두 달 뒤에 꼭 뒷덜미를 잡거든요.
tRPC를 붙이면 이런 불안감이 꽤 줄어듭니다. AI가 초안을 만들고, TypeScript와 tRPC가 그 초안을 검문하는 느낌이에요. 경찰까지는 아니고, 꼼꼼한 동료 개발자 한 명이 옆에서 빨간펜 들고 봐주는 정도랄까요.
제가 실제로 겪은 실수들, 그리고 나름의 사용 요령
처음엔 저도 신나서 AI에게 이것저것 시켰습니다. “이 API 만들어줘”, “이 mutation 추가해줘”, “프론트 연결까지 해줘” 이런 식으로요. 그런데 역시 세상에 공짜 점심은 없습니다. AI가 tRPC 코드를 꽤 잘 만들긴 하지만, 그대로 믿고 붙이면 자잘한 삽질이 생깁니다.
제가 몇 번 당해보고 나서 정리한 기준을 표로 남겨볼게요. 이런 건 사실 공식 문서보다 실무에서 한 번 데여봐야 몸에 들어오긴 합니다.
| 제가 신경 쓰는 부분 | 왜 중요한지 | 실제로 겪었던 일 |
|---|---|---|
| Zod 스키마는 꼭 직접 확인하기 | AI가 nullable, optional 같은 조건을 은근히 잘 빼먹습니다. 입력값 검증은 대충 넘어가면 나중에 꼭 터져요. |
한 번은 AI가 z.string()만 넣어두고 실제로는 비어 있을 수 있는 값을 처리하지 않았습니다. 프론트에서 undefined 관련 에러가 나서 한참 찾았어요. |
| Context 타입을 미리 알려주기 | 로그인 사용자, session, db client 같은 값은 context에 많이 들어가는데, AI가 이 구조를 모르면 any로 뭉개버릴 때가 있습니다. |
처음엔 AI가 session을 그냥 any로 처리해서 인증 체크가 엉성했습니다. CreateContext 타입을 명시해줬더니 그 뒤부터 훨씬 정확해졌습니다. |
| Mutation 후 invalidate 요청하기 | AI는 데이터를 추가하거나 수정하는 mutation은 잘 만들지만, 화면 갱신 로직은 자주 빼먹습니다. | Todo를 추가했는데 리스트가 그대로라서 잠깐 멍해졌습니다. 알고 보니 utils.todo.getAll.invalidate()를 안 넣었더라고요. |
| 폴더 구조를 먼저 잡아두기 | 아무 기준 없이 시키면 AI가 router를 한 파일에 계속 쌓아버립니다. 나중에 파일 하나가 괴물이 됩니다. | 초반에 모든 API가 한 파일에 들어가서 500줄이 넘었습니다. 리팩터링하면서 “아, 처음에 나눴어야 했는데” 하고 후회했죠. |
| tRPC 버전을 프롬프트에 적어주기 | tRPC v10과 v11은 코드 스타일이 조금 다릅니다. AI가 예전 예제를 참고해서 엉뚱한 코드를 만들 때가 있어요. | Copilot이 v10 스타일의 createRouter를 생성해서 v11 프로젝트에서 컴파일 에러가 났습니다. package.json 버전을 같이 알려주니 바로 좋아졌습니다. |
AI에게 그냥 맡기지 말고, 작업 지시서를 짧게 써주는 게 좋았습니다
제가 써보니 AI 바이브 코딩은 “알아서 해줘”보다 “이 규칙 안에서 해줘”가 훨씬 잘 먹힙니다. 개발자 입장에서는 조금 귀찮아 보여도, 처음에 조건을 잘 적어두면 나중에 수정 시간이 줄어듭니다.
예를 들면 저는 Cursor에 이런 식으로 자주 말합니다.
현재 프로젝트는 Next.js App Router, tRPC v11, Prisma, Zod를 사용합니다.
server/routers 폴더 아래에 도메인별 router를 분리해서 작성해주세요.
모든 input은 Zod로 검증하고, nullable과 optional을 실제 Prisma schema 기준으로 맞춰주세요.
mutation 이후에는 관련 query를 invalidate하는 프론트 코드까지 같이 작성해주세요.
Context에는 session과 prisma가 들어있고, session이 없으면 protectedProcedure에서 막아주세요.
이렇게 써두면 AI가 훨씬 덜 헤맵니다. 사람도 마찬가지잖아요. “그냥 알아서 해봐”보다 “우리 팀 규칙은 이렇고, 이 폴더에 이렇게 넣어줘”라고 말하면 결과물이 좋아지는 것처럼요. AI도 결국 맥락을 먹고 사는 도구라서, 맥락을 잘 주는 사람이 이깁니다.
타입 에러가 오히려 고마웠던 순간
tRPC와 AI를 같이 쓰면서 제일 재밌었던 순간은 타입 에러가 난 순간이었습니다. 이상하죠. 에러가 났는데 좋았다니. 그런데 개발 오래 하다 보면 런타임 에러보다 컴파일 타임 에러가 훨씬 고맙습니다. 빨리 알려주는 친구니까요.
예전에 이런 코드가 있었습니다. 서버에서 내려오는 User 객체에 age?: number 필드가 있었어요. 나이가 없을 수도 있는 구조였죠. 그런데 프론트 코드에서 AI가 이렇게 만들어놨습니다.
<span>{user.age.toFixed(2)}</span>
fetch로 대충 any 처리된 코드였다면 아마 그냥 지나갔을 겁니다. 그리고 실제 데이터에 age가 없는 사용자가 들어오는 순간 화면이 터졌겠죠. 그런데 tRPC를 붙여둔 상태에서는 TypeScript가 바로 빨간 줄을 그었습니다.
Object is possibly 'undefined'.
이때 AI에게 “이 에러를 optional chaining으로 안전하게 처리해줘”라고 말했더니 바로 이렇게 고쳐줬습니다.
<span>{user.age?.toFixed(2) ?? '나이 정보 없음'}</span>
별거 아닌 코드처럼 보이지만, 저는 이런 순간이 꽤 중요하다고 봅니다. AI가 똑똑해지는 것도 좋지만, AI가 실수했을 때 바로 잡아줄 수 있는 안전장치가 있어야 합니다. tRPC의 타입 안전성은 그 안전장치 역할을 꽤 잘해줬습니다.
제가 쓰고 있는 폴더 구조도 살짝 공유해볼게요
tRPC를 AI와 같이 쓸 때는 폴더 구조가 생각보다 중요합니다. 구조가 흐트러져 있으면 AI도 같이 흐트러집니다. 저는 보통 아래처럼 단순하게 시작합니다. 처음부터 너무 멋진 아키텍처를 만들려고 하면 손이 안 가더라고요. 일단 명확한 게 좋습니다.
src/
server/
trpc.ts
context.ts
routers/
index.ts
todo.ts
user.ts
project.ts
app/
api/
trpc/
[trpc]/
route.ts
utils/
trpc.ts
components/
todo/
TodoList.tsx
TodoForm.tsx
이렇게 도메인별로 router를 나눠두면 AI에게 지시하기도 편합니다. “todo router에 update mutation 추가해줘”라고 말하면 어디를 봐야 하는지 비교적 잘 찾습니다. 반대로 모든 procedure가 rootRouter.ts 하나에 몰려 있으면, 어느 순간부터 AI도 문맥을 놓치기 시작합니다. 사람도 긴 파일 싫어하는데 AI라고 좋겠습니까.
- router는 도메인별로 나누기: todo, user, project처럼 기능 단위로 분리하는 게 관리하기 편했습니다.
- protectedProcedure와 publicProcedure를 초반에 만들어두기: 인증이 필요한 API와 아닌 API를 AI가 구분하기 쉬워집니다.
- Prisma schema를 AI가 볼 수 있는 상태로 열어두기: 필드 타입을 추측하지 않고 실제 schema를 참고하게 하는 게 중요합니다.
- 에러 처리 패턴을 하나 정해두기:
TRPCError를 어떤 식으로 던질지 정해두면 코드 톤이 일정해집니다.
AI 바이브 코딩과 tRPC를 같이 쓸 때 제가 느낀 장단점
좋은 이야기만 하면 좀 광고 같잖아요. 저는 tRPC를 꽤 좋아하지만, 모든 프로젝트에 무조건 넣어야 한다고 생각하진 않습니다. 도구는 늘 상황을 보고 골라야 합니다. 괜히 멋있어 보여서 넣었다가 팀 전체가 고생하는 경우도 많이 봤습니다.
| 구분 | 좋았던 점 | 조심할 점 |
|---|---|---|
| 생산성 | AI가 router, procedure, client hook까지 빠르게 만들어줍니다. CRUD 작업은 확실히 빨라졌습니다. | 처음 설정이 낯설면 오히려 시간이 더 걸립니다. 팀원이 tRPC를 모르면 온보딩 비용도 있습니다. |
| 타입 안전성 | 서버와 클라이언트 타입이 이어져서 AI가 만든 코드도 컴파일 단계에서 많이 걸러집니다. | 타입이 맞는다고 비즈니스 로직까지 맞는 건 아닙니다. 이건 사람이 반드시 봐야 합니다. |
| 유지보수 | API 응답 타입을 따로 문서화하지 않아도 코드 변경이 클라이언트에 바로 반영됩니다. | 백엔드와 프론트엔드가 완전히 분리된 조직에서는 tRPC 방식이 어색할 수 있습니다. |
| AI 활용 | 정해진 패턴 안에서 AI가 움직이기 때문에 결과물이 안정적입니다. | 프롬프트에 버전, 폴더 구조, 인증 방식 등을 안 알려주면 AI가 엉뚱한 예제를 가져올 수 있습니다. |
제 기준으로는 혼자 만드는 작은 토이 프로젝트라면 굳이 tRPC를 안 써도 됩니다. 그냥 Next.js Route Handler에 fetch 붙여도 충분할 때가 많아요. 그런데 API가 많아지고, 프론트와 백엔드 타입이 자주 어긋나고, AI가 만든 코드를 팀 코드베이스에 넣어야 한다면 이야기가 달라집니다. 그때는 tRPC가 꽤 든든합니다.
이런 분들에게는 꽤 잘 맞을 것 같습니다
제가 직접 써본 느낌으로는 AI 바이브 코딩과 tRPC 조합은 아무에게나 다 맞는 조합은 아닙니다. 하지만 잘 맞는 사람에게는 꽤 강력합니다. 특히 TypeScript를 이미 실무에서 쓰고 있고, API 타입 때문에 피곤했던 경험이 있는 분이라면 만족도가 높을 가능성이 큽니다.
- Next.js와 TypeScript 기반으로 풀스택 개발을 하고 있는 분
- GitHub Copilot, Cursor 같은 AI 코딩 도우미를 실무에 적극적으로 써보고 싶은 분
- API 응답 타입이 자주 바뀌어서 프론트에서 깨지는 경험을 해본 분
- 작은 팀에서 프론트와 백엔드를 같이 보며 빠르게 기능을 만들어야 하는 분
- AI가 만든 코드를 그냥 믿기보다, 타입 시스템으로 한 번 더 검증하고 싶은 분
반대로 백엔드가 여러 클라이언트에 공개 API를 제공해야 하거나, 프론트와 백엔드 조직이 완전히 분리되어 있거나, TypeScript 자체가 아직 팀에 익숙하지 않다면 조금 신중하게 접근하는 게 좋습니다. tRPC는 편한 도구지만, 팀의 개발 문화와 잘 맞아야 힘을 냅니다.
제가 내린 판단은 이렇습니다
솔직히 말하면, 저는 앞으로도 AI 코딩 도우미를 계속 쓸 것 같습니다. 이제 안 쓰던 시절로 돌아가기는 어렵더라고요. 다만 AI가 만들어주는 코드를 그대로 믿지는 않습니다. 믿고 싶어도 아직은 그 정도까지는 아니에요. 대신 AI에게 초안을 맡기고, 타입 시스템과 테스트로 검증하는 방식은 꽤 현실적인 선택이라고 봅니다.
그런 면에서 tRPC는 AI 바이브 코딩과 궁합이 좋았습니다. AI가 빠르게 만들어주고, tRPC가 타입으로 잡아주고, 개발자는 비즈니스 로직과 예외 상황을 더 깊게 보는 구조가 되니까요. 약간 이런 느낌입니다. AI가 운전대를 잡는 건 아직 불안하지만, 옆에서 내비게이션과 차선 이탈 경고를 켜두고 같이 달리는 건 꽤 쓸 만하다, 뭐 그런 느낌이요.
AI 바이브 코딩을 하면서 “왜 내 API는 자꾸 런타임에서 터지지?”, “AI가 만든 코드를 어디까지 믿어야 하지?”, “프론트와 백엔드 타입을 좀 깔끔하게 맞추고 싶은데 방법 없나?” 이런 고민을 해보셨다면 tRPC를 한 번쯤 붙여보셔도 좋겠습니다. 처음엔 조금 낯설어도, 한 번 감이 오면 API 작업이 꽤 편해집니다.
긴 글 읽느라 고생 많으셨습니다. 저도 아직 삽질 중입니다. 다만 예전보다 삽이 조금 더 좋아졌고, 어디를 파야 하는지도 조금은 알게 된 느낌이에요. 다음에 또 실제로 써보고 괜찮았던 개발 이야기 들고 와볼게요.
댓글
댓글 쓰기