AI-Generated 코드를 모듈식으로 유지하는 방법

발행: (2026년 1월 16일 오후 06:34 GMT+9)
16 min read
원문: Dev.to

Source: Dev.to

안녕하세요, 저는 Paul이라고 하며 스타트업 전용으로 일하는 시니어 소프트웨어 엔지니어입니다.

저는 프로그래밍 언어를 무제한 뷔페처럼 여기며, 언어와 관계없이 제가 가장 중시하는 것은 모듈화입니다. 이를 위해서는 우리 OpenAI와 Claude 어시스턴트가 크게 도움이 되지 않습니다. 이들은 모든 것이 거대한 index.js 파일에 얽혀 있는 구식 프로젝트들에 대해 많이 학습돼 있기 때문이죠. 무분별한 “바이브 코딩”(vibe coding)은 파일당 +1000줄 이상의 코드를 만들고, 많은 중복을 초래합니다.

왜 문제가 되나요?
스타트업에서는 코드가 수명이 짧습니다. 바이브 코딩을 집중적으로 사용하면 AI가 언제든지 코드를 생성해 내고, 리팩토링 부담이 전혀 필요 없을 것처럼 보이지만, 현실은 그렇지 않습니다. 상황은 빠르게 변하고, 새로운 컴포넌트나 동작을 구현하는 과정은 신속하고 위험이 없어야 합니다. AI는 하나의 변경이 전체 시스템에 미치는 영향을 평가하는 데 매우 서툽니다. 모델에 필드를 추가하면 시스템 전체에 어떤 영향을 미칠까요? 보안 위험을 초래하고 있나요? 뭔가를 깨뜨리고 있나요?

물론 단위 테스트가 필요하지만, 제가 생각하기에 더 큰 필요는 모듈화입니다. 코드를 격리하고 외부와 무관하게 유지하며, 함수당 하나의 용도에만 사용하고, 테스트가 가능하도록 단일 파일에 보관하는 것이죠. 이런 방식은 저에게 많은 문제와 리팩토링을 방지해 주었습니다. 개발이 조금 더 장황하고 머리를 쓰게 만들지만, 그만큼 견고해지고 훨씬 더 재미있어집니다.

이 글이 다루는 내용도 바로 이것입니다: AI 도구가 모듈식 코드를 생성하도록 강제하는 방법. 이를 위해서는 모델을 철저히 제한하고, 모든 자유를 잘라내며, 창의성을 완전히 억제해야 합니다. 하지만 저는 Vybe.build에서 일하고 있으며, AI를 짧고 잘 설계된 리쉬(leash) 안에 두는 것이 우리 팀의 특성입니다.

내 스택

이 글을 위해, 나는 매우 잘 알고 거의 매번 작은 SaaS나 “SaaS‑replacement” 프로젝트를 만들 때 사용하는 스택을 사용할 것입니다. 이 스택은 이색적이지 않지만 모듈식 사고와 AI‑지원 개발에 매우 잘 맞습니다.

  • Next.js와 App Router
  • TypeScript, 어디서든
  • Vercel 배포용 (서버리스, 마찰 제로)
  • Cursor를 AI‑기반 편집기로 사용 (동일한 아이디어가 Claude Code, Copilot 등에도 적용됩니다.)

이 스택이 중요한 이유는 서버와 클라이언트, 라우트와 로직, 데이터와 UI와 같은 경계에 대해 생각하도록 강제하기 때문입니다. 경계는 AI가 무시하기 쉬운 부분이며, 이를 넘을 수 없게 만들지 않으면 AI는 이를 간과합니다.

시작 프로젝트는 여기에서 찾을 수 있습니다.

특정 파일 구조

제가 사용하는 구조는 다음과 같습니다:

./
├── app/                    # Next.js App Router pages and routes
├── src/
│   ├── __template__/      # Template used to generate new modules
│   │   ├── api/           # Server‑side actions (DB writes, heavy logic, etc.)
│   │   ├── components/    # React components specific to the module
│   │   ├── hooks/         # React hooks
│   │   └── types/         # TypeScript types (shared client/server)
│   ├── auth/              # Authentication module
│   ├── db/                # Database module
│   └── ui/                # Shared UI library
│       ├── components/
│       ├── hooks/
│       └── globals.css
├── prisma/                # Database schema and migrations
└── public/                # Static assets

src 아래의 각 폴더는 모듈입니다. 모듈은 자신의 로직, 훅, 컴포넌트, 타입을 모두 소유합니다. “이번 한 번만” 다른 모듈에 손을 대는 일은 허용되지 않습니다. 공유가 필요하다면 ui 혹은 전용 공유 모듈에 넣습니다.

__template__ 폴더는 매우 중요합니다. 이는 모든 새로운 모듈을 위한 청사진 역할을 합니다. 새로운 기능이 생기면 바로 코드를 작성하지 않고, 이 템플릿으로부터 모듈을 생성하는 것부터 시작합니다. 이렇게 하면 나와 AI 모두가 해야 할 의사결정이 크게 줄어듭니다.

api / components / hooks / types 로 나누는 방식을 좋아하는 이유는 Next.js의 양쪽 환경(서버와 클라이언트)과 잘 맞기 때문입니다. 서버 코드와 클라이언트 코드가 명확히 구분되어 AI가 모든 것을 뒤섞어 버리는 변명을 줄일 수 있습니다. 궁극적으로 모듈이 어떻게 구성될지는 여러분에게 달려 있습니다.

Next.js 구조

제가 매우 엄격하게 지키는 규칙 하나: app/ 디렉터리에는 오직 아주 특정한 로직만 포함합니다:

  • 페이지를 위한 prop 주입, 서버‑사이드 데이터 패칭
  • 접근 보호, 인증, 라우트에 대한 반환값

Next.js는 라우팅 및 실행 컨텍스트에 대해 강력한 의견을 가지고 있으며, 그것은 괜찮습니다. 앱 라우터는 페이지와 API 라우트를 정의합니다—그 외는 없습니다.

비즈니스 로직, 데이터 접근, 변환, 혹은 약간이라도 재사용 가능한 동작과 관련된 모든 것은 src/ 안의 모듈에 위치합니다.

예시 페이지

import { getUsers } from '@/src/user/api';
import { User } from '@/src/user/types';

export default async function UserPage() {
  let users: User[] = [];
  let error: string | null = null;

  try {
    users = await getUsers();
  } catch (err) {
    error = err instanceof Error ? err.message : 'An error occurred';
  }

  if (error) {
    return Error: {error};
  }

  return <div>{/* render users */}</div>;
}

데이터를 가져오고 라우트‑레벨 오류를 처리하는 것은 Next.js의 책임입니다. 그 외 모든 것은 user 모듈에 속합니다.

예시 API 라우트

import { NextResponse } from 'next/server';
import { getUsers } from '@/src/user/api/getUsers';
import { auth } from '@/src/auth';

export const GET = withAuth(async (request: Request) => {
  const session = await auth.api.getSession();
  if (!session) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  const users = await getUsers();
  return NextResponse.json(users);
});

인증 검사와 HTTP 관련 내용은 라우트에 남겨두고, 실제 로직은 모듈에 위치합니다.

이러한 분리는 지루하고 반복적이지만 매우 효과적입니다. 또한 AI에게 명확하고 강제 가능한 경계를 제공해 주어, 코드베이스를 모듈화하고 유지보수하기 쉽게 만드는 데 정확히 필요한 접근 방식입니다.

라우트는 모든 것을 연결하고, 모듈은 작업을 수행한다

이제 AI와 함께

이 구조가 마련되면 AI는 엄청나게 강력하면서도 위험해진다. 방치하면 AI는 가장 게으른 방식으로 도움을 주려고 할 것이다: 페이지의 모든 것을 코딩하고, 결국 불필요하게 여러 API 호출을 하게 되며, 로직을 여기저기서 다시 작성하게 된다 등.

목표는 간단하다: 선택지를 없애는 것이다. 할 수 있는 방법이 하나뿐이라면 AI는 결국 그 방법을 따르게 된다.

EJS 템플릿을 사용하여 모듈 생성하기

첫 번째 단계는 새로운 모듈을 만드는 작업을 사소하고 기계적으로 만드는 것입니다.

저는 작은 스크립트(Node + EJS)를 사용해 템플릿 폴더에서 모듈을 생성합니다. 외부에서 보이는 형태는 다음과 같습니다:

npm run create-module user

다음과 같은 파일 트리를 생성합니다:

src/user/
├── api/
│   ├── getUser.ts
│   ├── getUsers.ts
│   ├── createUser.ts
│   └── updateUser.ts
├── components/
│   └── UserCard.tsx
├── hooks/
│   ├── useUser.ts
│   └── useUsers.ts
└── types/
    └── User.ts

특별한 마법은 없습니다. API 파일은 얇고 명시적인 함수들로 이루어져 있습니다. 훅은 보통 React Query를 감싸는 작은 래퍼이며, 컴포넌트는 기본적으로 단순합니다. 타입은 서버와 클라이언트 사이에서 공유됩니다.

이 방식은 두 가지 일을 합니다:

  • 사람에게 발생하는 마찰을 없앱니다.
  • AI에게 매우 강력한 사전 지식을 제공합니다.

추가적인 장점:

  • 거대한 서비스 파일이 없습니다.
  • “utils.ts” 같은 덤프 장소가 없습니다.

제가 AI에게 “사용자 기능을 추가해라”라고 요청하면, AI는 구조를 새로 만들지 않고 이미 존재하는 구조를 따릅니다. 만약 그렇지 않다면, 엔트로피가 쌓이는 것을 방지하기 위해 모듈을 다시 생성하거나 수정합니다.

커스텀 ESLint 규칙을 가드 레일로

템플릿은 정상 흐름을 처리하고, 린터는 부정 행위를 처리합니다.

구체적인 예시: API 라우트

Next.js에서는 다음과 같이 작성하는 것이 매우 쉽습니다:

export const POST = async (req: Request) => {
  // do stuff
};

하지만 내 애플리케이션에서는 모든 라우트가 컨텍스트(세션, 사용자, 권한 등)를 주입하는 래퍼를 거쳐야 합니다:

export const POST = withAuth(async ({ user, req }) => {
  // do stuff
});

따라서 이를 강제하는 커스텀 ESLint 규칙을 추가합니다. 라우트가 POST, GET, PUT 등을 내보내면서 올바르게 래핑되지 않으면 린팅이 실패합니다.

흥미로운 점은 AI가 이에 어떻게 반응하느냐입니다.
AI가 라우트를 생성하면서 래퍼를 빼먹으면 린터 오류가 즉시 표시됩니다. AI는 오류를 보고 패턴을 이해한 뒤, 누락된 래퍼를 추가하여 코드를 수정합니다. 보안을 다시 설명하거나 논쟁할 필요 없이 툴체인이 교육을 담당하게 됩니다.

이는 다양한 상황에 적용됩니다:

  • 파일 경계 강제
  • 모듈 간 임포트 방지
  • 명명 규칙 강제
  • “똑똑한” 단축키 차단

명확한 지침

이것은 명백해 보이지만 반복해서 강조할 가치가 있습니다. Cursor 규칙을 사용하든, Claude Code 지시사항을 사용하든, 다른 곳에서 시스템 프롬프트를 사용하든, 아이디어는 동일합니다: 제약 조건을 초기에 적어두고 시간이 지남에 따라 발전시켜 나가세요.

저는 모든 것을 포괄하려고 하지 않습니다. 짧은 목록으로 시작합니다:

  • 코드가 위치할 수 있는 곳
  • 모듈이 구성되는 방식
  • 금지되는 사항(단일 파일, 교차 import, 라우트에 비즈니스 로직 포함 등)

AI가 나쁜 방식으로 놀라울 때마다 한 번만 고치지 않고, 규칙을 추가합니다.

보통 모듈 구조, app/에 관한 규칙, 새로운 패턴을 만들기보다 기존 모듈을 확장하는 방법 등을 설명하는 큰 지시 블록을 유지합니다. 여기에는 붙여넣지 않겠지만, 정확한 내용보다 습관이 더 중요합니다: AI 지시를 코드처럼 다루세요. 버전 관리하고, 개선하고, 관리하지 않으면 부패할 것이라고 가정하세요.

The Result

그 도구들은 오래전부터 존재했지만, 나는 최근까지 대부분 무시해 왔습니다. 이제 AI가 타이핑의 큰 부분을 담당하면서, 그 도구들이 생산성에 미치는 영향이 비로소 실감되었습니다.

나는 모듈식 코드나 “vibe coding”을 옹호하는 것이 아닙니다. 여러분에게 맞는 방식을 선택하면 됩니다. 나는 내가 해온 방식을 공유할 뿐이며, 내 경우 꽤 잘 작동합니다.

희망컨대 이것이 여러분에게 약간의 아이디어가 되길 바랍니다.

읽어 주셔서 감사합니다!

Back to Blog

관련 글

더 보기 »

기술은 구원자가 아니라 촉진자다

왜 사고의 명확성이 사용하는 도구보다 더 중요한가? Technology는 종종 마법 스위치처럼 취급된다—켜기만 하면 모든 것이 개선된다. 새로운 software, ...

에이전틱 코딩에 입문하기

Copilot Agent와의 경험 나는 주로 GitHub Copilot을 사용해 인라인 편집과 PR 리뷰를 수행했으며, 대부분의 사고는 내 머리로 했습니다. 최근 나는 t...