3단계
폴더를 계약으로
25 분
폴더를 계약으로
"여기에 파일 두면 무엇이 자동으로 된다" 를 약속으로 고정. Next.js · FastAPI 라우트는 이미 이 철학이지만, 더 넓게 적용 가능.
1. "URL = 폴더" 정합 (Next.js App Router)
src/app/
├── posts/
│ ├── page.tsx → /posts
│ └── [id]/
│ └── page.tsx → /posts/:id
├── admin/
│ └── settings/
│ └── page.tsx → /admin/settings
파일 경로 읽으면 URL 유추 가능. 반대도 가능. grep 한 번으로 "어떤 파일이 이 URL 을 담당?".
2. 하이픈 금지 · 복합 단어 금지
src/app/
├── admin/pryzeet/ ✓
├── admin/pryzeet-users/ ✗ (URL 에 하이픈은 OK, 폴더 구조는 depth 로)
폴더 구조가 의미 계층을 표현 · URL 에 하이픈 합성을 하면 grep 이 어려움.
3. 파일명 규약
kebab-case.ts → 일반 모듈
camelCase.ts → 사용자 정의 훅 (useX)
PascalCase.tsx → React 컴포넌트
__tests__/*.test.ts → 테스트 (vitest 규약)
*.spec.ts → Playwright E2E
한 프로젝트 안에서 일관. 두 스타일 섞이면 혼란.
4. "진입점 안정화" — barrel export
src/shared/lib/
├── index.ts ← 공개 API 만 re-export
├── db.ts
├── auth/
│ ├── index.ts ← auth/ 공개 API
│ ├── session.ts
│ └── oauth.ts
외부에서는 import from "@/shared/lib" 만. 내부 구현 파일 경로는 refactor 자유.
5. 공용 코드 배치
src/shared/lib/ → 도메인 무관 (db · logger · fmt)
src/shared/ui/ → 시각 요소 (Button · Table)
src/shared/types/ → 타입 전용
src/domains/*/ → 도메인 코드
"shared = N 군데서 쓰이는 것" · "domains = 1 도메인 전용".
6. 테스트 폴더 위치
두 학파:
- 코드 옆 —
src/lib/foo.ts+src/lib/foo.test.ts - 분리 —
src/lib/foo.ts+tests/lib/foo.test.ts
코드 옆이 실용적 (grep 쉬움 · 이동 시 같이). 분리는 큰 프로젝트.
7. 실전 — warragon docs 폴더 계약
docs/
├── RULES.md — 전역 규칙 (변경 주기 분기 이하)
├── shared/*.md — 기술 규칙 (분기별)
├── agent/{svc}/ — Claude agent 전용 지시
│ ├── README.md — 진입점
│ ├── rules.md — 서비스 고유 규칙
│ └── features/ — 기능별 세부
└── service/{svc}/ — 제품 · 운영 문서
├── prd.md — 제품 요구사항
├── api.md — API 레퍼런스
└── improvements.md — 개선 · 기술부채
파일 이름 하나로 "여기서 뭘 볼 수 있는지" 즉답 가능. 새 팀원 · AI agent 가 빠르게 읽음.
8. _ prefix — private
src/
├── pages/
├── components/
├── _legacy/ ← 제거 예정 · import 금지
├── _scripts/ ← 일회성 마이그 스크립트
Python 은 _ prefix 가 관례, JS/TS 는 관례 약함 → 주석 + eslint 룰.
9. 자주 걸리는 자리
- 폴더 깊이 과잉 —
src/lib/utils/helpers/string/format.ts는 찾기 어려움. 3~4 depth 권장 utils/만능 폴더 — 무엇을 담는지 불명. 도메인별로 분리- 순환 의존 — A 폴더가 B import, B 가 A import. 공통을 별도 추출
- 규약 혼재 — 한 프로젝트에 kebab · camel · Pascal 섞임
10. ESLint 로 강제
// eslint.config.mjs
import boundaries from "eslint-plugin-boundaries";
export default {
plugins: { boundaries },
rules: {
"boundaries/element-types": ["error", {
rules: [
{ from: "frontend", disallow: ["backend"] },
{ from: "shared", disallow: ["domains"] },
],
}],
},
};
경계 위반을 린트 단계에서 차단.
하고픈 말
폴더 구조는 "남이 찾기 좋은 방식" 이 정답. 내 기억에 의존한 이름 · 깊이는 3 개월 뒤 나 자신도 못 찾습니다.
Next
- 04-sql-as-ssot