AI와 함께 개발하기 — 문서가 곧 실력이다

Hyunjun By Hyunjun 2026년 02월 07일

AI를 활용한 개발에서 가장 중요한 건 코드를 잘 치는 것이 아니다. AI에게 맥락을 얼마나 잘 전달하느냐가 결과물의 퀄리티를 결정한다. 반년간 AI와 함께 앱을 만들면서 느낀 건, 결국 문서를 잘 정리하는 사람이 AI를 잘 쓰는 사람이라는 것이다.

이 글에서는 실제로 내가 모노레포(Flutter + Next.js + NestJS) 구조에서 활용하고 있는 문서 체계와 베스트 프랙티스를 정리한다.


프로젝트 구조

먼저 전체 모노레포 구조는 다음과 같다.

project-root/
├── AGENTS.md                  # 루트 레벨 — 프로젝트 전체 맥락 (업계 표준)
├── CLAUDE.md                  # Claude Code 전용 (AGENTS.md 참조 + Claude 전용 설정)
├── ARCHITECTURE.md            # 시스템 아키텍처 문서
├── CONVENTIONS.md             # 공통 코딩 컨벤션
├── .claude/
│   └── settings.json          # Claude Code 프로젝트 설정
├── mobile/                    # Flutter 앱 (독립 — pnpm workspace 외부)
│   ├── AGENTS.md              # 모바일 앱 전용 맥락
│   ├── CLAUDE.md
│   ├── lib/
│   │   ├── core/              # 공통 유틸, 상수, 테마
│   │   ├── features/          # 피처 기반 구조
│   │   │   ├── auth/
│   │   │   │   ├── data/
│   │   │   │   ├── domain/
│   │   │   │   └── presentation/
│   │   │   └── home/
│   │   └── shared/            # 공유 위젯, 모델
│   └── pubspec.yaml
├── web/                       # Next.js 프론트엔드
│   ├── AGENTS.md              # 웹 앱 전용 맥락
│   ├── CLAUDE.md
│   ├── src/
│   │   ├── app/               # App Router 기반
│   │   ├── components/        # UI 컴포넌트
│   │   ├── hooks/             # 커스텀 훅
│   │   ├── lib/               # 유틸리티
│   │   └── types/             # 타입 정의
│   ├── next.config.ts
│   └── package.json
├── api/                       # NestJS 백엔드
│   ├── AGENTS.md              # API 전용 맥락
│   ├── CLAUDE.md
│   ├── src/
│   │   ├── common/            # 공통 가드, 필터, 인터셉터
│   │   ├── modules/           # 모듈 기반 구조
│   │   │   ├── auth/
│   │   │   │   ├── auth.controller.ts
│   │   │   │   ├── auth.service.ts
│   │   │   │   ├── auth.module.ts
│   │   │   │   └── dto/
│   │   │   └── user/
│   │   └── config/            # 환경 설정
│   ├── prisma/
│   │   └── schema.prisma
│   └── package.json
├── packages/
│   └── shared/                # 공유 패키지 (타입, 상수 — web, api 간 공유)
│       ├── src/
│       │   ├── types/
│       │   └── constants/
│       └── package.json
└── package.json               # pnpm workspace 루트 (web, api, packages)
Plaintext

참고로, 패키지 매니저로 pnpm을 쓰는 이유가 있다. pnpm은 npm, yarn과 같은 Node.js 패키지 매니저인데, 모노레포에서 특히 강점이 있다. npm이나 yarn은 각 패키지마다 node_modules에 의존성을 중복 설치하지만, pnpm은 전역 스토어에 한 번만 설치하고 심볼릭 링크로 연결한다. 디스크 사용량이 크게 줄고 설치 속도도 빠르다. 그리고 pnpm workspace를 쓰면 web, api, packages 같은 여러 패키지를 하나의 레포에서 관리하면서, 공유 패키지(packages/shared)의 타입이나 상수를 별도 배포 없이 바로 참조할 수 있다. Flutter는 Dart 생태계라 pnpm workspace에 포함할 수 없으므로 독립적으로 둔다.

보안 측면에서도 pnpm이 유리하다. npm이나 yarn은 의존성을 flat하게 호이스팅하기 때문에, package.json에 선언하지 않은 패키지도 우연히 접근할 수 있다. 이를 “유령 의존성(Phantom Dependencies)”이라고 하는데, 내가 직접 설치하지 않은 패키지를 코드에서 import해도 동작하는 것이다. 문제는 그 패키지가 어느 날 상위 의존성 업데이트로 사라지거나 버전이 바뀌면 갑자기 빌드가 깨진다는 것이고, 더 위험한 건 악의적인 패키지가 호이스팅을 통해 프로젝트 전체에서 접근 가능해질 수 있다는 점이다. pnpm은 node_modules를 non-flat 구조로 만들어서, package.json에 명시적으로 선언한 패키지만 접근할 수 있도록 강제한다. 선언하지 않은 패키지를 import하면 즉시 에러가 나기 때문에, 의존성 그래프가 정직하게 유지된다.

핵심은, AGENTS.md가 루트와 각 앱에 모두 존재한다는 점이다. 대부분의 AI 코딩 도구(Cursor, Codex, Copilot 등)는 작업 디렉토리 기준으로 가장 가까운 AGENTS.md부터 읽고, 상위 디렉토리까지 참조한다. 이 계층 구조를 활용하면 전체 맥락과 앱별 맥락을 자연스럽게 분리할 수 있다.


AGENTS.md vs CLAUDE.md — 업계 표준 이야기

잠깐 짚고 넘어갈 것이 있다. 왜 CLAUDE.md와 AGENTS.md가 둘 다 존재하는가.

원래 각 AI 코딩 도구마다 자체 설정 파일을 사용했다. Claude Code는 CLAUDE.md, Gemini CLI는 GEMINI.md 같은 식이다. 같은 프로젝트에서 여러 도구를 쓰면 동일한 내용을 파일마다 복사해야 하는 문제가 생겼다.

이를 해결하기 위해 Sourcegraph 주도로 AGENTS.md라는 오픈 스탠다드가 만들어졌고, 현재 OpenAI(Codex), Google(Gemini CLI), GitHub(Copilot), Cursor, Amp, Warp 등 대부분의 AI 코딩 도구가 이를 지원한다. 다만 Claude Code는 아직 AGENTS.md를 공식 지원하지 않고, CLAUDE.md만 읽는다.

실용적인 베스트 프랙티스는 둘 다 두는 것이다. AGENTS.md에 실제 내용을 작성하고, CLAUDE.md에서 이를 참조하면 된다.

# CLAUDE.md
See AGENTS.md for project guidelines.
Plaintext

이렇게 하면 Claude Code는 CLAUDE.md → AGENTS.md 순서로 읽고, 다른 도구들은 AGENTS.md를 직접 읽는다. 어떤 AI 도구를 쓰든 동일한 맥락을 공유할 수 있고, 내용을 한 곳에서만 관리하면 된다. Claude Code 전용 설정이 필요한 경우에만 CLAUDE.md에 추가로 적으면 된다.

Vercel의 자체 평가에서도 흥미로운 결과가 나왔다. AI에게 필요할 때 문서를 찾아 읽게 하는 방식(Skills)보다, AGENTS.md처럼 항상 컨텍스트에 포함시키는 방식이 더 좋은 성능을 보였다. 구체적으로, AGENTS.md에 압축된 문서 인덱스를 넣었을 때 pass rate가 100%였던 반면, Skills 기반 검색은 명시적 지시를 포함해도 79%, 기본 상태에서는 53%(문서가 아예 없는 것과 동일)에 그쳤다. Skills가 존재하는 상황에서도 56%의 경우 AI가 아예 호출하지 않았다고 한다. 결국 “필요하면 찾아봐”보다 “여기 다 적어뒀으니 읽어”가 현시점에서는 더 효과적이라는 뜻이다.


루트 AGENTS.md — 프로젝트의 뇌

루트 AGENTS.md는 프로젝트 전체를 관통하는 맥락을 담는다. AI가 어떤 앱에서 작업하든 이 문서를 먼저 읽기 때문에, 여기에 적힌 내용이 모든 작업의 기반이 된다.

# Project Name

## 개요
- 서비스 설명 (한 줄 요약)
- 모노레포 구조: Flutter(모바일) + Next.js(웹) + NestJS(API)
- 패키지 매니저: pnpm workspace
- 데이터베이스: PostgreSQL + Prisma ORM

## 기술 스택
- **모바일**: Flutter 3.x, Riverpod, GoRouter, Dio
- ****: Next.js 16 (App Router), TypeScript, Tailwind CSS, Zustand
- **API**: NestJS 11, Prisma, PostgreSQL, Passport
- **공통**: TypeScript (웹/API), Dart (모바일)

## 핵심 규칙
- 모든 코드는 한국어 주석 금지, 영문으로 작성
- 커밋 메시지는 Conventional Commits 형식 (feat:, fix:, chore:)
- API 응답은 반드시 공통 응답 포맷을 따름
- 환경변수는 .env.example에 키만 명시, 값은 절대 커밋하지 않음

## 워크스페이스 구조
- pnpm workspace: web, api, packages (TypeScript 공유)
- mobile은 Flutter 독립 프로젝트 (workspace 외부)

## 공통 명령어
- `pnpm install` — 전체 의존성 설치 (web, api, packages)
- `pnpm dev:api` — NestJS 개발 서버
- `pnpm dev:web` — Next.js 개발 서버
- `cd mobile && flutter run` — Flutter 실행

## 참고 문서
- 아키텍처: ./ARCHITECTURE.md
- 코딩 컨벤션: ./CONVENTIONS.md
- API 스펙: ./api/docs/api-spec.md
Markdown

여기서 중요한 건 핵심 규칙 섹션이다. AI는 명시적으로 금지하지 않으면 자기 방식대로 한다. “한국어 주석 금지”, “공통 응답 포맷 사용” 같은 규칙을 여기에 적어두면 매번 프롬프트에서 반복할 필요가 없다.


ARCHITECTURE.md — 시스템의 지도

아키텍처 문서는 AI가 “이 프로젝트가 어떻게 생겼는지”를 이해하는 지도다. 이 문서가 없으면 AI는 매번 추측하고, 추측은 높은 확률로 틀린다.

# Architecture

## 시스템 구성

```
[Flutter App] ──→ [NestJS API] ──→ [PostgreSQL]
[Next.js Web] ──→ [NestJS API] ──→ [Redis Cache]
```

## 인증 흐름
1. 클라이언트에서 이메일/비밀번호로 로그인 요청
2. API에서 JWT Access Token (15분) + Refresh Token (7일) 발급
3. Access Token은 Authorization 헤더, Refresh Token은 httpOnly 쿠키
4. 토큰 만료 시 /auth/refresh 엔드포인트로 갱신

## 데이터베이스 설계 원칙
- 모든 테이블에 id (UUID), createdAt, updatedAt 필수
- Soft delete 사용 (deletedAt 컬럼)
- 관계는 Prisma 스키마에서 명시적으로 정의

## API 응답 포맷
```json
{
  "success": true,
  "data": {},
  "error": null,
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 100
  }
}
```

## 에러 처리
- API: NestJS ExceptionFilter로 통합 처리
- 웹: Error Boundary + React Query의 onError
- 모바일: Dio Interceptor에서 공통 에러 핸들링

## 디렉토리 컨벤션
- NestJS: 모듈 기반 (modules/[feature]/)
- Next.js: App Router 기반 (app/[route]/)
- Flutter: Feature-first 구조 (features/[feature]/{data,domain,presentation}/)
Markdown

특히 인증 흐름이나 API 응답 포맷처럼 여러 앱에 걸치는 규약은 반드시 여기에 명시해야 한다. 그래야 AI가 Flutter에서 API를 호출하는 코드를 짤 때도, Next.js에서 응답을 파싱할 때도 동일한 구조를 따른다.


CONVENTIONS.md — 코드의 문법

컨벤션 문서는 코드 스타일, 네이밍, 패턴을 정의한다. AI는 일관성을 유지하는 데 약하기 때문에, 이 문서가 없으면 파일마다 스타일이 달라진다.

# Coding Conventions

## 공통
- 변수/함수명: camelCase
- 클래스/타입명: PascalCase
- 상수: UPPER_SNAKE_CASE
- 파일명: kebab-case (NestJS, Next.js), snake_case (Flutter)

## NestJS
- 컨트롤러: HTTP 로직만, 비즈니스 로직은 서비스로
- 서비스: 단일 책임 원칙, 하나의 서비스가 너무 커지면 분리
- DTO: class-validator 데코레이터 필수, Swagger 데코레이터 병행
- 가드/인터셉터: common/ 디렉토리에 배치
- 예외: HttpException 직접 사용 금지, 커스텀 예외 클래스 사용

### DTO 예시
```typescript
export class CreateUserDto {
  @ApiProperty({ description: 'User email' })
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @ApiProperty({ description: 'User password', minLength: 8 })
  @IsString()
  @MinLength(8)
  password: string;
}
```

## Next.js
- 컴포넌트: 함수형 컴포넌트 + named export
- 서버 컴포넌트 기본, 클라이언트 필요 시에만 'use client'
- 상태 관리: 서버 상태는 React Query, 클라이언트 상태는 Zustand
- 스타일: Tailwind CSS, 인라인 스타일 금지
- API 호출: lib/api/ 디렉토리에 함수로 분리

### 컴포넌트 예시
```tsx
interface UserCardProps {
  user: User;
  onSelect?: (id: string) => void;
}

export function UserCard({ user, onSelect }: UserCardProps) {
  return (
    <div className="rounded-lg border p-4">
      <h3 className="text-lg font-semibold">{user.name}</h3>
    </div>
  );
}
```

## Flutter
- 상태 관리: Riverpod (Notifier / AsyncNotifier + riverpod_generator)
- 라우팅: GoRouter, 라우트 정의는 router.dart에 집중
- API 통신: Dio + Repository 패턴
- 위젯: 재사용 위젯은 shared/widgets/, 피처 전용은 해당 피처 내 presentation/widgets/
- 네이밍: 위젯 파일명과 클래스명 일치

### Provider 예시
```dart
@riverpod
class AuthNotifier extends _$AuthNotifier {
  @override
  FutureOr<AuthState> build() async {
    return await _checkAuthStatus();
  }

  Future<void> signIn(String email, String password) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final result = await ref.read(authRepositoryProvider).signIn(email, password);
      return AuthState.authenticated(result);
    });
  }
}
```
Markdown

이 문서에서 가장 중요한 건 예시 코드다. AI는 규칙을 텍스트로 설명하는 것보다 예시를 하나 보여주는 게 훨씬 정확하게 따른다. “DTO는 이렇게 만들어”라고 하나 보여주면, 이후 모든 DTO가 같은 패턴으로 나온다.


앱별 AGENTS.md — 로컬 맥락

각 앱 디렉토리의 AGENTS.md는 해당 앱에서만 필요한 맥락을 담는다.

api/AGENTS.md

# API (NestJS)

## 실행
- `pnpm dev:api` — 개발 서버 (포트 3000)
- `pnpm prisma:migrate` — 마이그레이션 실행
- `pnpm prisma:generate` — Prisma Client 생성
- `pnpm prisma:studio` — Prisma Studio

## 모듈 생성 규칙
새 모듈을 만들 때는 반드시 다음 파일을 함께 생성:
- [name].module.ts
- [name].controller.ts
- [name].service.ts
- dto/create-[name].dto.ts
- dto/update-[name].dto.ts

## 현재 모듈 목록
- auth: 인증 (로그인, 회원가입, 토큰 갱신)
- user: 사용자 프로필 관리
- post: 게시글 CRUD

## 주의사항
- Prisma 스키마 변경 후 반드시 `prisma generate` 실행
- 새 모듈은 AppModule에 등록 필수
- Guard 적용: @UseGuards(JwtAuthGuard) 기본, 공개 API는 @Public() 데코레이터
Markdown

mobile/AGENTS.md

# Mobile (Flutter)

## 실행
- `flutter run` — 개발 실행
- `flutter build apk` — Android 빌드
- `flutter build ios` — iOS 빌드
- `dart run build_runner build` — 코드 생성

## 새 피처 생성 구조
features/[name]/
├── data/
│   ├── repositories/[name]_repository_impl.dart
│   └── datasources/[name]_remote_datasource.dart
├── domain/
│   ├── entities/[name].dart
│   └── repositories/[name]_repository.dart
└── presentation/
    ├── pages/[name]_page.dart
    ├── widgets/
    └── providers/[name]_provider.dart

## 환경 설정
- API Base URL은 --dart-define으로 주입
- 플레이버: dev, staging, prod

## 주의사항
- 새 Provider 생성 후 반드시 `dart run build_runner build` 실행
- 이미지 에셋은 assets/images/에 배치, pubspec.yaml에 등록
- 플랫폼별 코드는 최소화, 필요 시 platform_channel/ 디렉토리에 분리
Markdown

web/AGENTS.md

# Web (Next.js)

## 실행
- `pnpm dev:web` — 개발 서버 (포트 3001)
- `pnpm build:web` — 프로덕션 빌드
- `pnpm lint:web` — ESLint 실행

## 라우트 구조
app/
├── (auth)/
│   ├── login/page.tsx
│   └── register/page.tsx
├── (main)/
│   ├── layout.tsx          # 인증 필요한 레이아웃
│   ├── dashboard/page.tsx
│   └── settings/page.tsx
└── api/                    # Route Handlers (필요 시)

## 주의사항
- 서버 컴포넌트에서 직접 API 호출 시 내부 URL 사용
- 클라이언트 컴포넌트에서는 React Query + api 함수 사용
- 미들웨어에서 인증 체크 (middleware.ts)
- 환경변수: NEXT_PUBLIC_ 접두사만 클라이언트에서 접근 가능
Markdown

.claude/settings.json — 프로젝트 레벨 설정

Claude Code의 프로젝트 설정 파일로, 허용할 명령어나 MCP 서버 등을 정의할 수 있다.

{
  "permissions": {
    "allow": [
      "Bash(pnpm:*)",
      "Bash(flutter:*)",
      "Bash(dart:*)",
      "Bash(npx prisma:*)",
      "Bash(git:*)"
    ]
  }
}
JSON

자주 쓰는 명령어를 미리 허용해두면 작업 중 매번 승인할 필요가 없어 흐름이 끊기지 않는다.


실전에서 느낀 것들

이 문서 체계를 갖추기 전과 후는 확연히 다르다.

문서 없이 작업할 때는 매번 프롬프트에 “이 프로젝트는 NestJS를 쓰고, Prisma를 쓰고, 응답 포맷은 이렇고…”를 반복해야 했다. 그래도 AI는 중간에 컨벤션을 잊어버리고, 파일마다 스타일이 달라졌다. 결국 사람이 일일이 고치는 시간이 늘어났다.

문서를 갖춘 후에는 “auth 모듈에 소셜 로그인 추가해줘”라고만 해도, 기존 패턴에 맞는 컨트롤러, 서비스, DTO가 나온다. AI가 읽을 문서가 곧 프롬프트의 연장이기 때문이다.

결국 AI 시대의 개발 실력은 코드를 타이핑하는 속도가 아니라, AI가 이해할 수 있는 형태로 의도를 구조화하는 능력이다. 문서를 잘 쓰는 사람이 AI를 잘 쓰는 사람이고, AI를 잘 쓰는 사람이 빠르게 좋은 제품을 만든다.


그래서, 개발자는 이제 할 일이 없는 건가?

AI가 코드를 다 짜주니까 개발자는 이제 필요 없다는 말을 가끔 듣는다. 내 생각은 좀 다르다.

이 글에서 다룬 내용을 다시 보자. 모노레포에서 Flutter, Next.js, NestJS를 어떻게 나누고, pnpm workspace로 무엇을 공유하며, 인증 흐름은 어떤 구조로 설계하고, API 응답 포맷은 왜 통일해야 하는지. 이걸 처음부터 설계하려면 앱 아키텍처, 서버 아키텍처, 웹 아키텍처를 이해하고 있어야 한다. 아키텍처를 모르는 사람이 이 문서 체계를 구성할 수 있을까? AI에게 “모노레포 설계해줘”라고 하면 그럴듯한 답이 나오겠지만, 그게 내 프로젝트에 맞는 구조인지 판단하는 건 결국 사람의 몫이다.

AI가 바꾼 건 코드를 치는 속도지, 설계를 판단하는 능력이 아니다. 오히려 AI가 빠르게 코드를 만들어주기 때문에, 잘못된 설계 위에 코드가 쌓이는 속도도 빨라졌다. 기반이 잘못되면 되돌리기가 더 어려워진다는 뜻이다. AI 시대에 개발자의 가치는 코드를 짜는 데서 설계를 짜는 쪽으로 이동했을 뿐, 사라진 게 아니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

목차