확장 가능하고 유지보수가 용이한 Next.js 15 애플리케이션을 구축하기 위해서는 체계적인 폴더 구조가 필수입니다. 이 가이드에서는 소규모부터 대규모 프로젝트까지 모든 상황에 적용할 수 있는 표준 폴더 구조와 설계 원칙을 제시합니다.
기본 App Router 구조
핵심 폴더와 파일
Next.js 15 App Router의 기본 구조를 이해하는 것부터 시작합니다:
project-root/
├── app/ # App Router 루트 디렉토리
│ ├── globals.css # 전역 스타일
│ ├── layout.tsx # 루트 레이아웃
│ ├── page.tsx # 홈페이지 (/)
│ ├── loading.tsx # 전역 로딩 UI
│ ├── error.tsx # 전역 에러 UI
│ ├── not-found.tsx # 404 페이지
│ └── template.tsx # 루트 템플릿 (선택적)
├── public/ # 정적 파일들
├── next.config.js # Next.js 설정
├── package.json
├── tailwind.config.ts # Tailwind CSS 설정
├── tsconfig.json # TypeScript 설정
└── .env.local # 환경 변수
특수 파일 역할
App Router에서 특별한 의미를 가지는 파일들입니다:
layout.tsx: 페이지들 간 공유되는 UI 레이아웃page.tsx: 라우트의 실제 페이지 컴포넌트loading.tsx: 로딩 상태 UIerror.tsx: 에러 상태 UInot-found.tsx: 404 에러 페이지template.tsx: 레이아웃과 유사하지만 매번 새 인스턴스 생성route.tsx: API 라우트 핸들러
소규모 프로젝트 구조
기본 구조 (1-5페이지)
간단한 랜딩 페이지나 포트폴리오 사이트에 적합한 구조입니다:
my-portfolio/
├── app/
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx # 홈페이지
│ ├── about/
│ │ └── page.tsx # 소개 페이지
│ ├── projects/
│ │ ├── page.tsx # 프로젝트 목록
│ │ └── [id]/
│ │ └── page.tsx # 프로젝트 상세
│ ├── contact/
│ │ └── page.tsx # 연락처
│ └── api/
│ └── contact/
│ └── route.ts # 연락 폼 API
├── components/
│ ├── ui/
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ └── Input.tsx
│ ├── Header.tsx
│ ├── Footer.tsx
│ └── ContactForm.tsx
├── lib/
│ ├── utils.ts
│ └── validations.ts
├── types/
│ └── index.ts
├── data/
│ ├── projects.ts
│ └── skills.ts
└── public/
├── images/
└── icons/
소규모 프로젝트 구성 예시
// app/layout.tsx
import './globals.css'
import { Inter } from 'next/font/google'
import Header from '@/components/Header'
import Footer from '@/components/Footer'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: '홍길동 포트폴리오',
description: '프론트엔드 개발자 홍길동의 포트폴리오입니다.'
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ko">
<body className={inter.className}>
<Header />
<main className="min-h-screen">
{children}
</main>
<Footer />
</body>
</html>
)
}
// components/Header.tsx
import Link from 'next/link'
const navigation = [
{ name: '홈', href: '/' },
{ name: '소개', href: '/about' },
{ name: '프로젝트', href: '/projects' },
{ name: '연락처', href: '/contact' }
]
export default function Header() {
return (
<header className="bg-white shadow-sm">
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex">
<Link href="/" className="flex items-center">
<span className="text-xl font-bold">홍길동</span>
</Link>
</div>
<div className="flex space-x-8">
{navigation.map((item) => (
<Link
key={item.name}
href={item.href}
className="inline-flex items-center text-gray-500 hover:text-gray-700"
>
{item.name}
</Link>
))}
</div>
</div>
</nav>
</header>
)
}
중규모 프로젝트 구조
확장된 구조 (10-50페이지)
블로그, 소규모 전자상거래, SaaS 애플리케이션에 적합한 구조입니다:
my-saas-app/
├── app/
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx # 랜딩 페이지
│ ├── loading.tsx
│ ├── error.tsx
│ ├── not-found.tsx
│ ├── (marketing)/ # 라우트 그룹
│ │ ├── about/
│ │ │ └── page.tsx
│ │ ├── pricing/
│ │ │ └── page.tsx
│ │ ├── blog/
│ │ │ ├── page.tsx
│ │ │ ├── [slug]/
│ │ │ │ └── page.tsx
│ │ │ └── category/
│ │ │ └── [category]/
│ │ │ └── page.tsx
│ │ └── contact/
│ │ └── page.tsx
│ ├── (auth)/ # 인증 관련
│ │ ├── login/
│ │ │ └── page.tsx
│ │ ├── signup/
│ │ │ └── page.tsx
│ │ └── forgot-password/
│ │ └── page.tsx
│ ├── dashboard/ # 대시보드
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── loading.tsx
│ │ ├── projects/
│ │ │ ├── page.tsx
│ │ │ ├── [id]/
│ │ │ │ └── page.tsx
│ │ │ └── new/
│ │ │ └── page.tsx
│ │ ├── settings/
│ │ │ ├── page.tsx
│ │ │ ├── profile/
│ │ │ │ └── page.tsx
│ │ │ └── billing/
│ │ │ └── page.tsx
│ │ └── analytics/
│ │ └── page.tsx
│ └── api/ # API 라우트
│ ├── auth/
│ │ ├── login/
│ │ │ └── route.ts
│ │ └── register/
│ │ └── route.ts
│ ├── projects/
│ │ ├── route.ts
│ │ └── [id]/
│ │ └── route.ts
│ ├── users/
│ │ └── route.ts
│ └── webhooks/
│ └── stripe/
│ └── route.ts
├── components/
│ ├── ui/ # 기본 UI 컴포넌트
│ │ ├── button/
│ │ │ ├── index.tsx
│ │ │ └── button.stories.tsx
│ │ ├── input/
│ │ │ ├── index.tsx
│ │ │ └── input.stories.tsx
│ │ ├── modal/
│ │ └── toast/
│ ├── forms/ # 폼 컴포넌트
│ │ ├── LoginForm.tsx
│ │ ├── ProjectForm.tsx
│ │ └── ContactForm.tsx
│ ├── layout/ # 레이아웃 컴포넌트
│ │ ├── Header.tsx
│ │ ├── Footer.tsx
│ │ ├── Sidebar.tsx
│ │ └── DashboardLayout.tsx
│ ├── marketing/ # 마케팅 페이지 컴포넌트
│ │ ├── Hero.tsx
│ │ ├── Features.tsx
│ │ ├── Testimonials.tsx
│ │ └── CTA.tsx
│ └── dashboard/ # 대시보드 컴포넌트
│ ├── ProjectCard.tsx
│ ├── StatsCard.tsx
│ └── Charts/
│ ├── LineChart.tsx
│ └── BarChart.tsx
├── lib/ # 유틸리티 라이브러리
│ ├── auth.ts
│ ├── database.ts
│ ├── stripe.ts
│ ├── email.ts
│ ├── utils.ts
│ ├── validations.ts
│ └── constants.ts
├── hooks/ # 커스텀 훅
│ ├── useAuth.ts
│ ├── useLocalStorage.ts
│ └── useDebounce.ts
├── types/ # TypeScript 타입 정의
│ ├── auth.ts
│ ├── project.ts
│ ├── user.ts
│ └── api.ts
├── styles/ # 스타일 파일
│ ├── components.css
│ └── utilities.css
├── data/ # 정적 데이터
│ ├── plans.ts
│ ├── features.ts
│ └── testimonials.ts
├── config/ # 설정 파일
│ ├── database.ts
│ ├── auth.ts
│ └── site.ts
└── public/
├── images/
├── icons/
└── docs/
중규모 프로젝트 구성 예시
// app/(marketing)/layout.tsx
import MarketingHeader from '@/components/layout/MarketingHeader'
import MarketingFooter from '@/components/layout/MarketingFooter'
export default function MarketingLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
<MarketingHeader />
{children}
<MarketingFooter />
</>
)
}
// app/dashboard/layout.tsx
import { redirect } from 'next/navigation'
import { getServerSession } from 'next-auth'
import DashboardSidebar from '@/components/layout/DashboardSidebar'
import DashboardHeader from '@/components/layout/DashboardHeader'
import { authOptions } from '@/lib/auth'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await getServerSession(authOptions)
if (!session) {
redirect('/login')
}
return (
<div className="flex h-screen bg-gray-100">
<DashboardSidebar />
<div className="flex-1 flex flex-col overflow-hidden">
<DashboardHeader user={session.user} />
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-50">
{children}
</main>
</div>
</div>
)
}
// lib/auth.ts
import { NextAuthOptions } from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import { prisma } from '@/lib/database'
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
session: async ({ session, token }) => {
if (session?.user) {
session.user.id = token.sub!
}
return session
},
jwt: async ({ user, token }) => {
if (user) {
token.sub = user.id
}
return token
},
},
session: {
strategy: 'jwt',
},
pages: {
signIn: '/login',
error: '/auth/error',
},
}
대규모 프로젝트 구조
엔터프라이즈급 구조 (100+ 페이지)
대규모 팀과 복잡한 비즈니스 로직을 가진 애플리케이션에 적합한 구조입니다:
enterprise-app/
├── app/
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ ├── template.tsx
│ ├── loading.tsx
│ ├── error.tsx
│ ├── global-error.tsx
│ ├── not-found.tsx
│ ├── (public)/ # 공개 영역
│ │ ├── layout.tsx
│ │ ├── page.tsx # 랜딩 페이지
│ │ ├── about/
│ │ ├── contact/
│ │ ├── blog/
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ ├── loading.tsx
│ │ │ ├── [slug]/
│ │ │ ├── category/
│ │ │ └── @sidebar/ # Parallel Route
│ │ ├── docs/
│ │ │ └── [...slug]/
│ │ └── pricing/
│ ├── (auth)/ # 인증 관련
│ │ ├── layout.tsx
│ │ ├── login/
│ │ ├── register/
│ │ ├── forgot-password/
│ │ ├── reset-password/
│ │ └── verify-email/
│ ├── (dashboard)/ # 일반 사용자 대시보드
│ │ ├── layout.tsx
│ │ ├── @sidebar/ # Parallel Route - 사이드바
│ │ ├── @breadcrumb/ # Parallel Route - 브레드크럼
│ │ ├── overview/
│ │ ├── projects/
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ ├── loading.tsx
│ │ │ ├── error.tsx
│ │ │ ├── [id]/
│ │ │ │ ├── page.tsx
│ │ │ │ ├── edit/
│ │ │ │ ├── members/
│ │ │ │ ├── settings/
│ │ │ │ └── @modal/ # Parallel Route - 모달
│ │ │ │ └── (.)edit/
│ │ │ └── new/
│ │ ├── teams/
│ │ │ ├── layout.tsx
│ │ │ ├── [teamId]/
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── members/
│ │ │ │ ├── projects/
│ │ │ │ └── settings/
│ │ │ └── create/
│ │ ├── analytics/
│ │ │ ├── layout.tsx
│ │ │ ├── overview/
│ │ │ ├── reports/
│ │ │ └── insights/
│ │ ├── settings/
│ │ │ ├── layout.tsx
│ │ │ ├── profile/
│ │ │ ├── security/
│ │ │ ├── billing/
│ │ │ ├── notifications/
│ │ │ └── integrations/
│ │ └── help/
│ ├── (admin)/ # 관리자 영역
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ ├── error.tsx
│ │ ├── dashboard/
│ │ ├── users/
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ ├── [id]/
│ │ │ └── bulk-actions/
│ │ ├── organizations/
│ │ ├── billing/
│ │ ├── analytics/
│ │ ├── system/
│ │ │ ├── logs/
│ │ │ ├── monitoring/
│ │ │ └── maintenance/
│ │ └── settings/
│ └── api/ # API Routes
│ ├── auth/
│ │ ├── login/
│ │ ├── register/
│ │ ├── refresh/
│ │ └── logout/
│ ├── v1/ # API 버전 관리
│ │ ├── users/
│ │ │ ├── route.ts
│ │ │ └── [id]/
│ │ │ ├── route.ts
│ │ │ ├── avatar/
│ │ │ └── preferences/
│ │ ├── projects/
│ │ │ ├── route.ts
│ │ │ ├── [id]/
│ │ │ │ ├── route.ts
│ │ │ │ ├── members/
│ │ │ │ ├── tasks/
│ │ │ │ └── comments/
│ │ │ └── search/
│ │ ├── teams/
│ │ ├── analytics/
│ │ └── admin/
│ ├── webhooks/
│ │ ├── stripe/
│ │ ├── github/
│ │ └── slack/
│ ├── upload/
│ │ ├── avatar/
│ │ ├── documents/
│ │ └── images/
│ └── cron/ # 크론 작업
│ ├── daily-reports/
│ ├── cleanup/
│ └── backup/
├── src/ # 소스 코드 (app 외부)
│ ├── components/
│ │ ├── ui/ # 기본 UI 컴포넌트
│ │ │ ├── forms/
│ │ │ │ ├── Input/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── Input.test.tsx
│ │ │ │ │ ├── Input.stories.tsx
│ │ │ │ │ └── Input.module.css
│ │ │ │ ├── Select/
│ │ │ │ ├── TextArea/
│ │ │ │ └── Checkbox/
│ │ │ ├── feedback/
│ │ │ │ ├── Alert/
│ │ │ │ ├── Toast/
│ │ │ │ ├── Modal/
│ │ │ │ └── Loading/
│ │ │ ├── navigation/
│ │ │ │ ├── Breadcrumb/
│ │ │ │ ├── Pagination/
│ │ │ │ ├── Tabs/
│ │ │ │ └── Menu/
│ │ │ └── data/
│ │ │ ├── Table/
│ │ │ ├── Chart/
│ │ │ ├── List/
│ │ │ └── Card/
│ │ ├── layout/ # 레이아웃 컴포넌트
│ │ │ ├── Header/
│ │ │ ├── Footer/
│ │ │ ├── Sidebar/
│ │ │ ├── Navigation/
│ │ │ └── Container/
│ │ ├── business/ # 비즈니스 로직 컴포넌트
│ │ │ ├── project/
│ │ │ │ ├── ProjectCard/
│ │ │ │ ├── ProjectForm/
│ │ │ │ ├── ProjectList/
│ │ │ │ └── ProjectDetails/
│ │ │ ├── user/
│ │ │ │ ├── UserProfile/
│ │ │ │ ├── UserCard/
│ │ │ │ ├── UserList/
│ │ │ │ └── UserForm/
│ │ │ ├── team/
│ │ │ ├── analytics/
│ │ │ └── billing/
│ │ └── providers/ # Context 프로바이더
│ │ ├── AuthProvider/
│ │ ├── ThemeProvider/
│ │ ├── QueryProvider/
│ │ └── ToastProvider/
│ ├── lib/ # 유틸리티 라이브러리
│ │ ├── auth/
│ │ │ ├── index.ts
│ │ │ ├── providers.ts
│ │ │ ├── middleware.ts
│ │ │ └── utils.ts
│ │ ├── database/
│ │ │ ├── index.ts
│ │ │ ├── schema.ts
│ │ │ ├── migrations/
│ │ │ └── seeds/
│ │ ├── api/
│ │ │ ├── client.ts
│ │ │ ├── endpoints.ts
│ │ │ ├── middleware.ts
│ │ │ └── types.ts
│ │ ├── payments/
│ │ │ ├── stripe.ts
│ │ │ └── utils.ts
│ │ ├── email/
│ │ │ ├── templates/
│ │ │ ├── sender.ts
│ │ │ └── types.ts
│ │ ├── storage/
│ │ │ ├── s3.ts
│ │ │ ├── local.ts
│ │ │ └── types.ts
│ │ ├── analytics/
│ │ │ ├── google.ts
│ │ │ ├── mixpanel.ts
│ │ │ └── utils.ts
│ │ ├── monitoring/
│ │ │ ├── sentry.ts
│ │ │ ├── logger.ts
│ │ │ └── metrics.ts
│ │ └── utils/
│ │ ├── format.ts
│ │ ├── validation.ts
│ │ ├── date.ts
│ │ ├── string.ts
│ │ └── constants.ts
│ ├── hooks/ # 커스텀 훅
│ │ ├── auth/
│ │ │ ├── useAuth.ts
│ │ │ ├── usePermissions.ts
│ │ │ └── useSession.ts
│ │ ├── api/
│ │ │ ├── useQuery.ts
│ │ │ ├── useMutation.ts
│ │ │ └── useInfiniteQuery.ts
│ │ ├── ui/
│ │ │ ├── useModal.ts
│ │ │ ├── useToast.ts
│ │ │ ├── useTheme.ts
│ │ │ └── useLocalStorage.ts
│ │ └── business/
│ │ ├── useProjects.ts
│ │ ├── useUsers.ts
│ │ ├── useTeams.ts
│ │ └── useAnalytics.ts
│ ├── store/ # 상태 관리
│ │ ├── index.ts
│ │ ├── auth/
│ │ │ ├── authSlice.ts
│ │ │ └── authThunks.ts
│ │ ├── projects/
│ │ ├── users/
│ │ └── ui/
│ │ ├── modalSlice.ts
│ │ ├── themeSlice.ts
│ │ └── toastSlice.ts
│ ├── types/ # TypeScript 타입
│ │ ├── api/
│ │ │ ├── auth.ts
│ │ │ ├── users.ts
│ │ │ ├── projects.ts
│ │ │ └── common.ts
│ │ ├── database/
│ │ │ ├── models.ts
│ │ │ └── schema.ts
│ │ ├── ui/
│ │ │ └── components.ts
│ │ └── global.ts
│ ├── styles/ # 스타일 파일
│ │ ├── components/
│ │ │ ├── forms.css
│ │ │ ├── navigation.css
│ │ │ └── feedback.css
│ │ ├── layouts/
│ │ │ ├── dashboard.css
│ │ │ └── marketing.css
│ │ ├── utilities.css
│ │ └── themes/
│ │ ├── light.css
│ │ └── dark.css
│ ├── config/ # 설정 파일
│ │ ├── app.ts
│ │ ├── database.ts
│ │ ├── auth.ts
│ │ ├── email.ts
│ │ ├── storage.ts
│ │ └── monitoring.ts
│ ├── constants/ # 상수 정의
│ │ ├── api.ts
│ │ ├── routes.ts
│ │ ├── permissions.ts
│ │ └── ui.ts
│ ├── utils/ # 유틸리티 함수
│ │ ├── api/
│ │ │ ├── request.ts
│ │ │ ├── response.ts
│ │ │ └── validation.ts
│ │ ├── auth/
│ │ │ ├── permissions.ts
│ │ │ └── tokens.ts
│ │ ├── format/
│ │ │ ├── date.ts
│ │ │ ├── currency.ts
│ │ │ └── text.ts
│ │ └── validation/
│ │ ├── schemas.ts
│ │ └── rules.ts
│ └── data/ # 정적 데이터 및 목업
│ ├── mock/
│ │ ├── users.ts
│ │ ├── projects.ts
│ │ └── analytics.ts
│ ├── static/
│ │ ├── plans.ts
│ │ ├── features.ts
│ │ └── countries.ts
│ └── fixtures/
│ └── test-data.ts
├── docs/ # 프로젝트 문서
│ ├── README.md
│ ├── CONTRIBUTING.md
│ ├── DEPLOYMENT.md
│ ├── api/
│ │ └── README.md
│ ├── components/
│ │ └── README.md
│ └── architecture/
│ └── FOLDER_STRUCTURE.md
├── tests/ # 테스트 파일
│ ├── __mocks__/
│ ├── setup.ts
│ ├── utils/
│ │ └── test-utils.tsx
│ ├── components/
│ │ └── ui/
│ ├── pages/
│ │ └── api/
│ ├── e2e/
│ │ ├── auth.spec.ts
│ │ └── projects.spec.ts
│ └── integration/
├── scripts/ # 빌드/배포 스크립트
│ ├── build.sh
│ ├── deploy.sh
│ ├── db-migrate.js
│ └── seed-data.js
├── .github/ # GitHub Actions
│ └── workflows/
│ ├── ci.yml
│ ├── deploy.yml
│ └── test.yml
└── public/
├── images/
│ ├── avatars/
│ ├── logos/
│ ├── illustrations/
│ └── backgrounds/
├── icons/
│ ├── favicons/
│ └── ui/
├── docs/
└── locales/
├── ko.json
├── en.json
└── ja.json
특수한 폴더 구조 패턴
Route Groups 활용
관련 라우트들을 논리적으로 그룹화하는 패턴입니다:
app/
├── (marketing)/ # 마케팅 페이지들
│ ├── layout.tsx # 마케팅 레이아웃
│ ├── page.tsx # 홈페이지
│ ├── about/
│ ├── pricing/
│ └── contact/
├── (app)/ # 애플리케이션 페이지들
│ ├── layout.tsx # 앱 레이아웃
│ ├── dashboard/
│ ├── projects/
│ └── settings/
├── (admin)/ # 관리자 페이지들
│ ├── layout.tsx # 관리자 레이아웃
│ ├── users/
│ ├── analytics/
│ └── system/
└── (auth)/ # 인증 페이지들
├── login/
├── register/
└── forgot-password/
Parallel Routes 구조
동시에 여러 컴포넌트를 렌더링하는 구조입니다:
app/
├── layout.tsx
├── page.tsx
├── @modal/ # 모달 슬롯
│ ├── default.tsx
│ └── login/
│ └── page.tsx
├── @sidebar/ # 사이드바 슬롯
│ ├── default.tsx
│ ├── loading.tsx
│ └── page.tsx
├── dashboard/
│ ├── layout.tsx # 슬롯들을 사용하는 레이아웃
│ ├── @charts/ # 차트 슬롯
│ │ ├── default.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
│ └── @metrics/ # 메트릭 슬롯
│ ├── default.tsx
│ ├── loading.tsx
│ └── page.tsx
Intercepting Routes 패턴
모달이나 오버레이를 위한 라우트 인터셉션입니다:
app/
├── photos/
│ ├── page.tsx # 사진 갤러리
│ └── [id]/
│ └── page.tsx # 사진 상세 페이지
├── @modal/ # 모달 슬롯
│ ├── default.tsx
│ └── (.)photos/ # photos 라우트 인터셉트
│ └── [id]/
│ └── page.tsx # 모달로 표시될 사진 상세
└── layout.tsx # 모달 슬롯을 포함한 레이아웃
베스트 프랙티스
네이밍 컨벤션
// 파일 네이밍
// ✅ 좋은 예
components/
├── UserProfile.tsx # PascalCase for components
├── useUserData.ts # camelCase for hooks
├── api-client.ts # kebab-case for utilities
└── user-types.ts # kebab-case for types
// ✅ 폴더 네이밍
app/
├── user-management/ # kebab-case for routes
├── api/
│ └── user-profile/ # kebab-case for API routes
└── (auth)/ # parentheses for route groups
절대 경로 설정
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"],
"@/types/*": ["./src/types/*"],
"@/hooks/*": ["./src/hooks/*"],
"@/styles/*": ["./src/styles/*"],
"@/config/*": ["./src/config/*"],
"@/utils/*": ["./src/utils/*"]
}
}
}
컴포넌트 구조 패턴
components/
├── ui/
│ └── Button/
│ ├── index.tsx # 메인 컴포넌트
│ ├── Button.test.tsx # 테스트
│ ├── Button.stories.tsx # Storybook
│ ├── Button.module.css # 스타일
│ └── types.ts # 타입 정의
// components/ui/Button/index.tsx
import React from 'react'
import styles from './Button.module.css'
import { ButtonProps } from './types'
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
children,
...props
}) => {
return (
<button
className={`${styles.button} ${styles[variant]} ${styles[size]}`}
{...props}
>
{children}
</button>
)
}
export default Button
export type { ButtonProps } from './types'
API 조직화
app/api/
├── v1/ # 버전 관리
│ ├── users/
│ │ ├── route.ts # GET, POST /api/v1/users
│ │ └── [id]/
│ │ ├── route.ts # GET, PUT, DELETE /api/v1/users/[id]
│ │ ├── avatar/
│ │ │ └── route.ts # POST /api/v1/users/[id]/avatar
│ │ └── preferences/
│ │ └── route.ts # GET, PUT /api/v1/users/[id]/preferences
│ └── projects/
├── auth/ # 인증 관련 API
│ ├── login/
│ ├── logout/
│ └── refresh/
└── webhooks/ # 웹훅 처리
├── stripe/
└── github/
팀별 폴더 구조 전략
기능별 조직화 (Feature-based)
src/
├── features/
│ ├── authentication/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ ├── types/
│ │ └── index.ts
│ ├── projects/
│ │ ├── components/
│ │ │ ├── ProjectCard/
│ │ │ ├── ProjectForm/
│ │ │ └── ProjectList/
│ │ ├── hooks/
│ │ │ ├── useProjects.ts
│ │ │ └── useProjectMutations.ts
│ │ ├── services/
│ │ │ ├── api.ts
│ │ │ └── validation.ts
│ │ ├── types/
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── users/
│ └── billing/
└── shared/ # 공유 리소스
├── components/
├── hooks/
├── utils/
└── types/
레이어별 조직화 (Layer-based)
src/
├── presentation/ # 프레젠테이션 레이어
│ ├── components/
│ ├── pages/
│ └── hooks/
├── business/ # 비즈니스 로직 레이어
│ ├── services/
│ ├── models/
│ └── rules/
├── data/ # 데이터 레이어
│ ├── repositories/
│ ├── api/
│ └── cache/
└── infrastructure/ # 인프라스트럭처 레이어
├── database/
├── external-services/
└── monitoring/
도메인별 조직화 (Domain-based)
src/
├── domains/
│ ├── user-management/
│ │ ├── entities/
│ │ ├── repositories/
│ │ ├── services/
│ │ ├── components/
│ │ └── api/
│ ├── project-management/
│ │ ├── entities/
│ │ ├── repositories/
│ │ ├── services/
│ │ ├── components/
│ │ └── api/
│ └── billing/
└── shared-kernel/ # 공유 도메인 로직
├── entities/
├── value-objects/
└── services/
성능 최적화를 위한 구조
코드 스플리팅 전략
// 동적 임포트를 위한 구조
app/
├── dashboard/
│ ├── page.tsx # 기본 대시보드
│ ├── analytics/
│ │ └── page.tsx # 분석 페이지 (지연 로딩)
│ └── reports/
│ └── page.tsx # 리포트 페이지 (지연 로딩)
// components/LazyComponents.tsx
import dynamic from 'next/dynamic'
import LoadingSpinner from './LoadingSpinner'
export const AnalyticsChart = dynamic(
() => import('./AnalyticsChart'),
{
loading: () => <LoadingSpinner />,
ssr: false // 클라이언트에서만 로드
}
)
export const HeavyDataTable = dynamic(
() => import('./HeavyDataTable'),
{
loading: () => <div>테이블을 로드하는 중...</div>
}
)
메타데이터 관리
app/
├── sitemap.ts # 사이트맵 생성
├── robots.ts # 로봇 규칙
├── opengraph-image.tsx # OpenGraph 이미지
├── favicon.ico # 파비콘
└── manifest.ts # 웹 앱 매니페스트
국제화 (i18n) 구조
locales/
├── ko/
│ ├── common.json
│ ├── auth.json
│ ├── dashboard.json
│ └── errors.json
├── en/
│ ├── common.json
│ ├── auth.json
│ ├── dashboard.json
│ └── errors.json
└── ja/
// i18n 설정
lib/
├── i18n/
│ ├── config.ts
│ ├── server.ts
│ ├── client.ts
│ └── middleware.ts
마무리
Next.js 15 App Router의 폴더 구조는 프로젝트 규모와 팀 구성, 비즈니스 요구사항에 따라 유연하게 설계되어야 합니다.
핵심 원칙:
- 일관성: 팀 전체가 따를 수 있는 명확한 규칙
- 확장성: 프로젝트 성장에 따른 확장 가능성
- 가독성: 새로운 팀원이 쉽게 이해할 수 있는 구조
- 유지보수성: 코드 변경과 리팩토링이 용이한 구조
- 성능: 번들 크기와 로딩 성능을 고려한 구조
선택 기준:
- 소규모 프로젝트: 단순하고 직관적인 구조
- 중규모 프로젝트: 기능별 분리와 재사용성 고려
- 대규모 프로젝트: 도메인 중심 설계와 레이어 분리
적절한 폴더 구조는 개발 생산성을 높이고 코드 품질을 향상시키는 핵심 요소입니다. 프로젝트의 특성에 맞는 구조를 선택하고 지속적으로 개선해나가는 것이 중요합니다.