Featured Post

AI 바이브 코딩에 Turborepo를 붙여보니 확실히 달라졌던 것들

Turborepo - 모노레포 관련 이미지

요즘 개발하다 보면 참 묘한 기분이 들어요. 예전엔 하루 종일 붙잡고 만들던 코드가 이제는 Cursor나 Copilot한테 슬쩍 말만 잘 걸어도 뚝딱 나오잖아요. 저도 처음엔 “이거 진짜 실무에 써도 되나?” 싶었는데, 어느 순간부터는 업무에서도, 사이드 프로젝트에서도 꽤 자연스럽게 쓰고 있더라고요. 그런데 말이죠. AI 바이브 코딩이 편하긴 한데, 프로젝트가 커지니까 다른 문제가 슬금슬금 올라왔습니다. 코드가 많아지는 속도를 레포 구조가 못 따라가는 거예요. 프론트엔드, 백엔드, 공통 UI, 유틸 함수, Prisma 스키마까지 한곳에 쌓이다 보니 빌드는 느려지고, 의존성은 자꾸 꼬이고, AI가 만든 코드를 넣을 때마다 “이번엔 또 어디가 터질까” 하는 마음이 들었어요. 그래서 제가 꽤 진지하게 도입해본 게 바로 Turborepo 모노레포였습니다.

오늘은 그냥 개념 설명만 하려는 글은 아니고요. 20년 가까이 개발하면서 이것저것 겪어본 입장에서, 실제로 AI 코딩 환경에서 Turborepo를 어떻게 써먹었는지, 어디서 삽질했는지, 어떤 설정이 체감상 도움이 됐는지 편하게 풀어보려고 합니다. 약간 친구한테 “야, 나 이거 써봤는데 이런 점은 좋고 이런 건 조심해라” 하고 말하는 느낌으로요.

AI로 코드를 빨리 만들수록 레포 구조가 더 중요해지더라

AI가 코드를 만들어주는 속도는 정말 빠릅니다. 버튼 컴포넌트 하나, 로그인 폼 하나, API 라우트 하나 정도는 이제 거의 대화 몇 번이면 나오죠. 문제는 그 다음이에요. 코드가 빨리 생기는 만큼, 프로젝트 안의 구조도 빨리 복잡해집니다.

제가 작년에 만들던 사이드 프로젝트가 딱 그랬어요. 구조는 대충 이랬습니다. Next.js 14로 만든 프론트엔드가 있고, Express 기반 백엔드가 있고, DB 쪽은 Prisma ORM을 썼어요. 여기에 공통 UI 컴포넌트랑 유틸 함수까지 따로 빼고 싶었죠. 처음엔 그냥 폴더만 나눴습니다. 뭐랄까, 처음엔 항상 그렇잖아요. “이 정도 규모면 굳이 모노레포까지?” 이런 생각이 들거든요.

그런데 AI로 컴포넌트를 몇 개 만들고, API를 몇 개 붙이고, 타입을 공유하기 시작하니까 금방 지저분해졌습니다. 작은 수정 하나 했는데 전체 빌드가 다시 돌고, 프론트만 건드렸는데 백엔드 쪽 타입 체크까지 엮이고, 어디선가 의존성이 살짝 어긋나면 한참을 헤매야 했어요. 솔직히 그때는 AI가 문제라기보다, 제가 AI가 만들어내는 속도를 받아줄 구조를 미리 못 만들어둔 게 문제였죠.

그때 Turborepo를 붙여봤습니다. 기대했던 건 단순했어요. “바뀐 것만 빌드해주면 좋겠다.” 그런데 실제로 써보니 단순히 빌드만 빨라지는 게 아니라, AI 코드를 실험하는 마음의 부담이 확 줄더라고요.

체감 항목 Turborepo 도입 전 Turborepo 도입 후
전체 빌드 시간 3분 12초 정도 대략 40초대
AI 코드 수정 후 재빌드 거의 매번 전체 빌드 변경된 패키지 중심으로 빌드
개발 서버 재시작 10초 안팎 2초 미만으로 느껴질 때가 많음
코드 실험 부담 수정 전에 한 번 더 망설임 일단 만들어보고 빠르게 검증

이게 숫자로 보면 그냥 “빌드가 빨라졌네” 정도일 수 있는데, 실제 개발할 때는 꽤 큰 차이입니다. 특히 AI 바이브 코딩은 짧게 만들고, 바로 돌려보고, 마음에 안 들면 다시 고치는 흐름이잖아요. 그 리듬이 끊기면 재미도 줄고 생산성도 확 떨어집니다. Turborepo의 캐싱병렬 실행은 그 리듬을 살려주는 쪽에 가까웠어요.

제가 실제로 썼던 Turborepo 모노레포 구조

처음부터 대단히 멋진 구조를 만든 건 아니었습니다. 사실 처음엔 꽤 엉성했어요. 그래도 몇 번 갈아엎고 나니까 지금은 아래 정도 구조가 제일 무난하더라고요. 너무 복잡하지 않으면서, AI가 만든 코드도 받아내기 괜찮은 구조였습니다.

my-monorepo/
├── apps/
│   ├── web/          # Next.js 14 프론트엔드
│   └── api/          # Express 백엔드
├── packages/
│   ├── shared-ui/    # 공통 UI 컴포넌트
│   ├── shared-utils/ # 공통 유틸 함수
│   └── database/     # Prisma 스키마 및 클라이언트
├── turbo.json
├── package.json
└── pnpm-workspace.yaml

여기서 제 기준으로 중요한 건 appspackages를 확실히 나누는 거였습니다. apps에는 실제 실행되는 애플리케이션을 두고, packages에는 여러 앱에서 공유할 수 있는 것들을 넣었어요. 이 기준을 잡아두니까 AI에게 지시할 때도 훨씬 편했습니다.

예를 들면 이런 식으로 말할 수 있거든요.

이 프로젝트는 Turborepo 모노레포야.
Next.js 앱은 apps/web에 있고,
공통 UI 컴포넌트는 packages/shared-ui에 있어.
새로운 로그인 폼은 shared-ui 패키지에 만들고,
import 경로는 @myrepo/shared-ui 형태로 맞춰줘.

이렇게 맥락을 미리 던져주면 AI가 엉뚱한 위치에 파일을 만들거나, 상대 경로를 잔뜩 물고 오는 일이 확실히 줄었습니다. 물론 완벽하진 않아요. AI도 가끔 정신줄을 놓습니다. 그래도 그냥 “로그인 폼 만들어줘”라고 던지는 것보다는 훨씬 낫습니다.

turbo.json 설정에서 제가 진짜로 삽질했던 부분

turbo.json은 처음 보면 그렇게 어려워 보이진 않는데, 막상 프로젝트에 붙여보면 은근히 손이 많이 갑니다. 특히 캐시 쪽은 잘못 잡으면 “왜 이게 다시 빌드되지?” 혹은 “왜 이게 빌드가 안 되지?” 같은 애매한 상황이 생겨요. 제가 한동안 쓰면서 정리한 설정은 대략 이렇습니다.

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local", "tsconfig.json"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"],
      "cache": true
    },
    "test": {
      "dependsOn": ["^build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

여기서 제가 제일 크게 데인 건 globalDependencies였습니다. 처음엔 대충 넘어갔어요. 환경변수 파일이 뭐 그렇게 중요하겠어 싶었죠. 그런데 .env.local이나 설정 파일이 바뀌었는데 캐시가 이상하게 남아 있거나, 반대로 별것도 아닌 변경에 캐시가 깨지는 일이 생기더라고요. 그때부터 환경변수와 공통 설정 파일은 명시적으로 관리해야 한다는 생각이 들었습니다.

특히 AI로 코드를 만들다 보면 환경변수를 은근히 자주 추가합니다. DATABASE_URL, JWT_SECRET, NEXT_PUBLIC_API_URL 같은 것들이요. AI가 예제 코드를 만들면서 “이 환경변수 필요해요” 하고 툭 던지는 경우도 많고요. 그래서 globalDependencies에 환경변수 파일 패턴을 넣어두면 나중에 덜 찝찝합니다.

AI가 만든 LoginForm을 shared-ui에 넣어봤던 예시

조금 더 실제적인 얘기를 해볼게요. 제가 어느 날 AI에게 “로그인 폼 컴포넌트 하나 만들어줘”라고 했더니 이런 코드를 줬습니다. 아주 흔한 코드죠.

import { useState } from 'react';
import { useRouter } from 'next/router';
import { apiClient } from '@myrepo/shared-utils';

export default function LoginForm() {
  const [email, setEmail] = useState('');
  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();
    await apiClient.post('/auth/login', { email });
    router.push('/dashboard');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  );
}

이 코드를 packages/shared-ui/src에 넣고 바로 빌드를 돌려봤습니다.

pnpm turbo build --filter=shared-ui

재밌는 건 여기서부터였어요. Turborepo가 의존성 그래프를 보고 shared-utils가 바뀌지 않았다는 걸 확인하더니, 필요한 범위만 처리했습니다. 체감상 거의 순식간이었어요. 실제로는 0.3초 정도였고요. 이 정도면 “빌드했다”기보다 “확인했다”에 가깝습니다.

예전 방식이었다면 어땠을까요. 프론트 앱 전체를 다시 빌드하거나, 타입 체크가 여러 군데에서 같이 돌면서 괜히 기다렸을 겁니다. 그 몇 초, 몇십 초가 별거 아닌 것 같아도 하루에 열 번, 스무 번 반복되면 생각보다 큽니다. 개발자는 기다리는 시간에 딴짓을 하게 되고, 딴짓을 하면 다시 집중하는 데 시간이 걸리거든요. 나이 들수록 이 집중 전환 비용이 더 크게 느껴집니다. 이건 진짜입니다.

여기서 바로 보이는 AI 코드의 함정

그런데 방금 코드에도 살짝 함정이 있습니다. useRouternext/router에서 가져오고 있죠. Next.js 13 이후 App Router를 쓰는 프로젝트라면 next/navigation을 써야 할 수도 있습니다. 그리고 더 근본적으로는, shared-ui 같은 공통 UI 패키지가 Next.js에 직접 의존하는 게 맞는지도 고민해야 합니다.

이런 지점이 AI 바이브 코딩의 묘한 부분이에요. AI는 그럴듯한 코드를 정말 잘 만듭니다. 그런데 프로젝트 구조의 철학까지 항상 맞춰주진 않아요. 그래서 저는 공통 UI 컴포넌트에는 가능하면 라우터 의존성을 빼려고 합니다. 이동 처리는 앱 쪽에서 주입하거나, 콜백으로 넘기는 식이 더 오래 버티더라고요.

예를 들면 이렇게 바꾸는 쪽을 더 좋아합니다.

import { useState } from 'react';
import { apiClient } from '@myrepo/shared-utils';

type LoginFormProps = {
  onSuccess?: () => void;
};

export default function LoginForm({ onSuccess }: LoginFormProps) {
  const [email, setEmail] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await apiClient.post('/auth/login', { email });
    onSuccess?.();
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  );
}

이렇게 하면 shared-ui는 UI 역할에 좀 더 충실해지고, 실제 라우팅은 apps/web에서 처리할 수 있습니다. AI에게도 이런 기준을 알려주면 결과물이 훨씬 좋아집니다. “공통 패키지에는 Next.js router 의존성을 넣지 말아줘” 같은 식으로요.

Turborepo를 AI 코딩과 같이 쓸 때 체감 좋았던 습관들

몇 달 정도 써보니까, Turborepo 자체보다도 어떻게 쓰는 습관을 들이느냐가 더 중요하다는 생각이 들었습니다. 좋은 도구도 대충 쓰면 그냥 복잡한 설정 파일 하나 늘어난 것뿐이거든요.

작업 범위를 filter로 좁혀두면 마음이 편하다

AI에게 API 라우트를 만들게 했으면 일단 API만 빌드하고 테스트합니다. UI 컴포넌트를 만들었으면 shared-ui만 확인하고요. 이때 --filter 옵션이 아주 쓸 만합니다.

pnpm turbo build --filter=api
pnpm turbo test --filter=shared-ui
pnpm turbo build --filter=web

저는 이걸 거의 습관처럼 씁니다. AI가 만들어준 코드를 바로 전체 프로젝트에 태우지 않고, 작은 범위에서 먼저 확인하는 거죠. 약간 여행 갈 때도 숙소 예약 전에 지도부터 보는 느낌이랄까요. 전체 일정을 다 짜기 전에, 일단 위치가 괜찮은지 보는 것처럼요. 개발도 비슷합니다. 작은 단위에서 괜찮은지 보고, 그다음 전체로 넓히는 게 훨씬 덜 피곤합니다.

inputs 설정을 대충 두면 캐시가 자꾸 애매해진다

inputs 설정은 처음엔 귀찮습니다. 그런데 나중에 캐시 히트율을 생각하면 꽤 중요해요. AI로 코딩하면 작은 파일이 자주 생기고, 테스트 파일도 자주 바뀝니다. 이때 어떤 파일 변경이 어떤 작업에 영향을 주는지 범위를 잘 잡아두면 불필요한 작업을 많이 줄일 수 있습니다.

예를 들어 테스트 작업에는 정말 테스트에 필요한 파일만 포함시키는 식이죠.

"test": {
  "dependsOn": ["^build"],
  "inputs": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "test/**/*.ts",
    "test/**/*.tsx"
  ]
}

이 설정 하나로 모든 게 마법처럼 빨라진다고 말하면 거짓말이고요. 다만 불필요하게 캐시가 깨지는 일이 줄어듭니다. 회사 프로젝트에서도 비슷한 방식으로 정리했더니 빌드와 테스트 대기 시간이 꽤 줄었어요. 정확히 모든 상황에서 몇 퍼센트라고 말하긴 어렵지만, 체감은 분명했습니다. 개발팀 분위기가 조금 부드러워진다고 해야 하나요. “또 기다려야 해?”라는 말이 줄어드는 것만으로도 꽤 좋습니다.

force는 비상약처럼 쓰는 게 좋다

--force 옵션은 캐시를 무시하고 다시 돌릴 때 씁니다.

pnpm turbo build --force

이게 가끔은 정말 시원합니다. 뭔가 캐시가 꼬인 것 같고, AI가 의존성을 이것저것 바꿔놓은 뒤라 찝찝할 때 한 번 돌리면 마음이 놓이거든요. 그런데 평소에 자주 쓰면 Turborepo를 쓰는 의미가 좀 줄어듭니다. 캐시라는 좋은 장점을 스스로 꺼버리는 셈이니까요.

저는 --force를 약간 비상약처럼 씁니다. 평소엔 서랍에 넣어두고, 진짜 이상할 때만 꺼내는 느낌이죠. AI가 대량으로 파일을 생성했거나, 패키지 버전을 꽤 많이 건드렸거나, 원인을 알 수 없는 빌드 실패가 이어질 때 정도면 충분합니다.

AI에게 Turborepo 맥락을 알려주는 프롬프트가 생각보다 중요하다

이 부분은 진짜 여러 번 겪고 나서 생긴 습관입니다. AI에게 그냥 “코드 만들어줘”라고 하면, 꽤 높은 확률로 일반적인 단일 프로젝트 기준의 코드를 줍니다. 모노레포 구조, 패키지 경계, import 규칙 같은 건 알아서 맞춰주지 않는 경우가 많아요.

그래서 저는 작업 전에 이런 식으로 맥락을 꼭 줍니다.

이 프로젝트는 pnpm workspace 기반 Turborepo 모노레포야.

구조는 아래와 같아.
  • apps/web: Next.js 14 프론트엔드
  • apps/api: Express 백엔드
  • packages/shared-ui: 공통 React UI 컴포넌트
  • packages/shared-utils: 공통 유틸 함수
  • packages/database: Prisma 관련 코드
규칙은 이래.
  • 패키지 간 import는 상대 경로를 쓰지 말고 @myrepo/* 형태를 써줘.
  • shared-ui에서는 Next.js router에 직접 의존하지 말아줘.
  • 새 의존성이 필요하면 어느 package.json에 추가해야 하는지도 같이 알려줘.

이렇게 말해두면 결과물이 확실히 달라집니다. 완벽하진 않지만, 최소한 엉뚱한 상대 경로가 줄고, 공통 패키지에 앱 전용 코드가 섞이는 일이 줄어요. 저는 이걸 AI 바이브 코딩의 안전벨트라고 생각합니다. 속도를 내되, 최소한의 방향은 잡아주는 거죠.

제가 쓰는 AI 코딩용 Turborepo 체크리스트

아래 체크리스트는 제가 실제로 새 모노레포를 만들거나, 기존 프로젝트에 AI 코딩 흐름을 붙일 때 확인하는 것들입니다. 거창한 건 아닌데, 이 정도만 챙겨도 삽질이 꽤 줄어듭니다.

  • turbo.json의 globalDependencies에 환경변수와 공통 설정 파일이 들어가 있는지 확인합니다.
  • ✅ 각 패키지의 package.json에 build, test, dev scripts가 일관되게 정의되어 있는지 봅니다.
  • ✅ AI가 생성한 코드는 바로 전체 빌드하지 말고 --filter로 해당 패키지부터 확인합니다.
  • ✅ 공통 패키지에는 앱 전용 의존성이 섞이지 않도록 import를 한 번 더 봅니다.
  • ✅ 캐시가 정말 의심될 때만 --force를 씁니다. 습관처럼 쓰면 손해입니다.
  • ✅ pnpm workspace를 쓸 때 peer dependency 충돌이 잦다면 .npmrc 설정도 같이 점검합니다.
    • ✅ AI에게 작업을 시키기 전에 모노레포 구조와 import 규칙을 먼저 알려줍니다.

조심해야 할 부분도 분명히 있다

Turborepo가 만능은 아닙니다. AI 코딩도 마찬가지고요. 둘을 같이 쓰면 생산성이 좋아지는 건 맞는데, 구조를 잘못 잡으면 오히려 더 복잡해질 수도 있습니다.

제가 가장 자주 본 문제는 패키지 경계를 흐리는 코드였습니다. 예를 들어 packages/shared-ui 안에서 apps/web의 파일을 상대 경로로 가져온다거나, shared-utils가 갑자기 프론트엔드 전용 라이브러리에 의존한다거나 하는 식입니다.

// 이런 식의 import는 나중에 발목 잡히기 쉽습니다.
import { formatUser } from '../../apps/web/utils/formatUser';

당장은 돌아갈 수 있습니다. 그래서 더 위험해요. 당장 에러가 안 나니까 그냥 넘어가게 되거든요. 그런데 시간이 지나면 빌드 순서가 꼬이고, 패키지를 분리하기 어려워지고, 테스트도 애매해집니다. AI가 이런 코드를 만들면 바로 잡아줘야 합니다.

제가 선호하는 방식은 공통으로 쓸 코드는 공통 패키지로 올리는 겁니다.

import { formatUser } from '@myrepo/shared-utils';

사소해 보이지만 이런 규칙이 쌓이면 프로젝트가 오래 버팁니다. 20년 가까이 개발하면서 느낀 건데, 좋은 구조는 처음엔 약간 귀찮고 느려 보입니다. 그런데 프로젝트가 커질수록 그 귀찮음이 보험처럼 돌아옵니다.

이런 분들이라면 Turborepo를 한번 붙여볼 만합니다

AI 바이브 코딩을 하면서 코드 생성 속도는 빨라졌는데, 빌드나 테스트가 자꾸 발목을 잡는다면 Turborepo를 꽤 진지하게 봐도 좋습니다. 특히 프론트엔드, 백엔드, 공통 패키지를 한 레포에서 같이 관리하고 있다면 체감이 더 큽니다.

반대로 아주 작은 개인 프로젝트라면 처음부터 너무 무겁게 시작할 필요는 없다고 봐요. 간단한 랜딩 페이지 하나 만들면서 Turborepo까지 붙이면 배보다 배꼽이 커질 수 있습니다. 저는 보통 앱이 두 개 이상이거나, 공통 UI나 유틸 패키지가 생기기 시작할 때부터 모노레포를 고민합니다. 그쯤 되면 슬슬 구조의 힘이 필요해지거든요.

제 경험상 Turborepo는 AI를 더 똑똑하게 만들어주는 도구라기보다는, AI가 빠르게 만든 코드를 안전하게 받아주는 작업장에 가깝습니다. AI가 재료를 빠르게 가져오면, Turborepo가 작업대를 정리해주는 느낌이랄까요. 덕분에 코드를 만들고, 확인하고, 고치고, 다시 돌려보는 흐름이 훨씬 편해졌습니다.

이 글은 Cursor나 Copilot 같은 도구로 AI 코딩을 이미 하고 있는 분들, 모노레포를 고민 중인 프론트엔드·풀스택 개발자분들, 그리고 빌드 시간이 길어질 때마다 커피를 가지러 가는 자신을 발견한 분들이 읽으면 특히 도움이 될 것 같습니다. 저도 아직 계속 배우는 중입니다. 그래도 이 조합은 꽤 오래 가져갈 만하다는 생각이 들어요. 속도도 중요하지만, 결국 오래 가는 개발은 구조가 받쳐줘야 하니까요.

댓글