diff --git a/apps/_template/README.md b/apps/_template/README.md index 7e14528f8..c48c1534a 100644 --- a/apps/_template/README.md +++ b/apps/_template/README.md @@ -1,4 +1,4 @@ -# AS Application +# Template Application Next.js 애플리케이션 with FSD(Feature Sliced Design) 아키텍처 @@ -13,46 +13,40 @@ Next.js 애플리케이션 with FSD(Feature Sliced Design) 아키텍처 - **HTTP 클라이언트**: Axios - **날짜 처리**: Day.js -## 프로젝트 구조 (FSD) +## 프로젝트 구조 (FSD + Next.js App Router) -이 프로젝트는 **Feature-Sliced Design v2.1**을 따르며, 계층적 아키텍처로 구성되어 있습니다. +이 프로젝트는 **Feature-Sliced Design v2.1**과 **FSD 공식 Next.js 가이드**에 맞춰 구성되어 있습니다. Next.js `app/`은 라우트만 담당하고, 실제 레이어는 `src/` 아래에 있습니다. ``` -as/ -├── app/ # App 레이어 (Next.js App Router) -│ ├── (routes)/ # 라우트 구조 -│ ├── layout.tsx # 루트 레이아웃 -│ ├── page.tsx # 홈 페이지 -│ ├── globals.css # 글로벌 스타일 -│ └── providers.tsx # 전역 providers +_template/ +├── app/ # Next.js App Router (루트) — 라우트·레이아웃 진입점만 +│ ├── layout.tsx # 루트 레이아웃 (src/app에서 providers, styles import) +│ └── (with-base-layout)/ +│ ├── layout.tsx # BaseLayout 래퍼 (src/app/layouts에서 import) +│ └── page.tsx # re-export: export { MainPage as default } from '@/pages/home' │ -├── widgets/ # Widgets 레이어 -│ └── [widget-name]/ # 큰 독립적 UI 블록 (여러 페이지에서 재사용) -│ ├── ui/ -│ ├── model/ -│ └── index.ts +├── pages/ # 빈 폴더 + README (Next.js가 src/pages를 Pages Router로 쓰지 않도록) +│ └── README.md │ -├── features/ # Features 레이어 -│ └── [feature-name]/ # 사용자 상호작용 기능 (여러 페이지에서 재사용) -│ ├── ui/ -│ ├── api/ -│ ├── model/ -│ └── index.ts -│ -├── entities/ # Entities 레이어 -│ └── [entity-name]/ # 비즈니스 엔티티 (User, Product 등) -│ ├── model/ -│ ├── api/ -│ ├── ui/ -│ └── index.ts -│ -└── shared/ # Shared 레이어 - ├── ui/ # UI Kit (비즈니스 로직 없는 공통 컴포넌트) - ├── api/ # API 클라이언트 설정 - ├── lib/ # 내부 라이브러리 (dates, colors 등) - ├── config/ # 환경 설정, feature flags - ├── types/ # 공통 타입 정의 - └── routes/ # 라우트 상수 +└── src/ # FSD 레이어 (실제 코드) + ├── app/ # App 레이어 (슬라이스 없음, 세그먼트만) + │ ├── providers/ # 전역 Providers (QueryClient 등) + │ ├── styles/ # 글로벌 스타일 (globals.css) + │ └── layouts/ # 공통 레이아웃 컴포넌트 (BaseLayoutWrapper) + │ + ├── pages/ # Pages 레이어 (페이지 슬라이스) + │ └── home/ + │ ├── ui/ + │ └── index.ts # Public API + │ + ├── widgets/ # Widgets 레이어 + ├── features/ # Features 레이어 + ├── entities/ # Entities 레이어 + └── shared/ # Shared 레이어 + ├── ui/ + ├── api/ + ├── lib/ + └── config/ ``` ### 계층 구조 이해하기 @@ -86,10 +80,10 @@ shared/ ↓ 가장 낮은 책임, 의존성 없음 - **특징**: 슬라이스가 없고 세그먼트로만 구성 - **의존성**: 모든 하위 레이어 사용 가능 -### 2. Pages (app 디렉토리) +### 2. Pages (src/pages) - **목적**: 웹사이트나 애플리케이션을 구성하는 페이지/화면 - **내용**: 페이지 UI, 로딩 상태, 에러 바운더리, 데이터 fetching -- **특징**: 한 페이지당 한 슬라이스 (유사한 페이지는 그룹화 가능) +- **특징**: 한 페이지당 한 슬라이스 (유사한 페이지는 그룹화 가능). Next.js `app/`의 route 파일에서 re-export - **의존성**: `widgets`, `features`, `entities`, `shared` 사용 가능 > 💡 **FSD v2.1 권장사항**: "pages-first" 접근 방식을 따라, 재사용되지 않는 큰 UI 블록과 비즈니스 로직은 페이지 슬라이스에 유지하세요. @@ -118,29 +112,6 @@ shared/ ↓ 가장 낮은 책임, 의존성 없음 - **특징**: 비즈니스 로직 없음, 슬라이스가 없고 세그먼트로만 구성 - **의존성**: 외부 라이브러리만 사용 가능 (다른 레이어 불가) -## 레이어 간 Import 규칙 - -> **핵심 규칙**: 슬라이스 내의 모듈(파일)은 **오직 하위 레이어**에 있는 다른 슬라이스만 import할 수 있습니다. - -예시: -- `features/auth-login/ui/form.tsx`는 `entities/user`와 `shared/ui`를 import 가능 -- 하지만 다른 `features/post-create`나 상위 레이어인 `widgets`는 import 불가 -- 같은 슬라이스 내부(`features/auth-login/`)의 다른 파일들은 자유롭게 import 가능 - -## Public API 규칙 - -> **슬라이스 외부의 모듈은 슬라이스의 Public API만 참조할 수 있으며, 내부 파일 구조에 직접 접근할 수 없습니다.** - -모든 슬라이스(및 슬라이스가 없는 레이어의 세그먼트)는 반드시 `index.ts`를 통해 Public API를 정의해야 합니다. - -```typescript -// ✅ 올바른 import -import { LoginForm } from 'features/auth-login'; - -// ❌ 잘못된 import (내부 구조 직접 접근) -import { LoginForm } from 'features/auth-login/ui/login-form'; -``` - ## 시작하기 ```bash @@ -174,16 +145,16 @@ NEXT_PUBLIC_API_URL=http://localhost:3000/api ```typescript // ✅ 올바른 import (하위 레이어) // features/auth-login/ui/form.tsx -import { User } from 'entities/user'; -import { Button } from 'shared/ui/button'; +import { User } from "entities/user"; +import { Button } from "shared/ui/button"; // ❌ 잘못된 import (같은 레이어) // features/auth-login/ui/form.tsx -import { PostForm } from 'features/post-create'; // 금지! +import { PostForm } from "features/post-create"; // 금지! // ❌ 잘못된 import (상위 레이어) // features/auth-login/ui/form.tsx -import { Header } from 'widgets/header'; // 금지! +import { Header } from "widgets/header"; // 금지! ``` ### 2. Public API 규칙 @@ -192,10 +163,10 @@ import { Header } from 'widgets/header'; // 금지! ```typescript // ✅ 올바른 import (Public API 사용) -import { LoginForm } from 'features/auth-login'; +import { LoginForm } from "features/auth-login"; // ❌ 잘못된 import (내부 구조 직접 접근) -import { LoginForm } from 'features/auth-login/ui/login-form'; +import { LoginForm } from "features/auth-login/ui/login-form"; ``` **Public API 작성 시 주의사항**: @@ -205,44 +176,12 @@ import { LoginForm } from 'features/auth-login/ui/login-form'; ```typescript // ❌ 나쁜 예시 -export * from './ui/Comment'; -export * from './model/comments'; +export * from "./ui/Comment"; +export * from "./model/comments"; // ✅ 좋은 예시 -export { Comment } from './ui/comment'; -export { CommentForm } from './ui/comment-form'; -export { useComments } from './model/use-comments'; -export type { Comment, CommentFormData } from './model/types'; -``` - -### 3. 슬라이스 독립성 - -- **Zero Coupling**: 같은 레이어의 슬라이스들은 서로 독립적이어야 합니다 -- **High Cohesion**: 슬라이스는 주요 목표와 관련된 대부분의 코드를 포함해야 합니다 -- 예외: Entities 레이어에서 필요한 경우 `@x` 표기법으로 cross-import 가능 - -### 4. 세그먼트 명명 규칙 - -세그먼트 이름은 **목적**을 설명해야 하며, **본질**을 설명하면 안 됩니다: - -- ❌ **나쁜 이름**: `components`, `hooks`, `types` (무엇인지만 설명) -- ✅ **좋은 이름**: `ui`, `api`, `model`, `lib` (왜 있는지 설명) - -## 코드 규칙 - -- TypeScript 사용 (strict mode) -- 함수형 컴포넌트 사용 -- Named exports 선호 -- Tailwind CSS 사용 -- ESLint + Prettier 적용 -- Public API는 명시적으로 정의 (와일드카드 export 지양) - -## 아키텍처 린팅 - -코드베이스의 FSD 규칙 준수를 자동으로 검증하려면 [Steiger](https://github.com/feature-sliced/steiger)를 사용하세요. - -```bash -# Steiger 설치 및 실행 -pnpm add -D @feature-sliced/steiger -npx steiger src +export { Comment } from "./ui/comment"; +export { CommentForm } from "./ui/comment-form"; +export { useComments } from "./model/use-comments"; +export type { Comment, CommentFormData } from "./model/types"; ``` diff --git a/apps/_template/app/(with-base-layout)/layout.tsx b/apps/_template/app/(with-base-layout)/layout.tsx index 1d182a0ee..1ff36f8ec 100644 --- a/apps/_template/app/(with-base-layout)/layout.tsx +++ b/apps/_template/app/(with-base-layout)/layout.tsx @@ -1,31 +1,10 @@ -"use client"; - -import { useCallback } from "react"; -import { usePathname, useRouter } from "next/navigation"; -import { useQueryClient } from "@tanstack/react-query"; -import { BaseLayout } from "@common/components"; -import type { MenuItem } from "@/entities/menu"; -import { PageLayout } from "@/shared/ui/page-layout"; - -export default function WithBaseLayout({ children }: { children: React.ReactNode }) { - const router = useRouter(); - const pathname = usePathname(); - const queryClient = useQueryClient(); - - const handleMenuClick = useCallback( - (item: MenuItem) => { - router.push(item.urlPath); - }, - [router], - ); - - return ( - - {children} - - ); -} +import type { ReactNode } from "react"; +import { BaseLayoutWrapper } from "@/app/layouts/base-layout"; + +interface WithBaseLayoutProps { + children: ReactNode; +} + +export default function WithBaseLayout({ children }: WithBaseLayoutProps) { + return {children}; +} diff --git a/apps/_template/app/(with-base-layout)/page.tsx b/apps/_template/app/(with-base-layout)/page.tsx index e118e732e..82a918b59 100644 --- a/apps/_template/app/(with-base-layout)/page.tsx +++ b/apps/_template/app/(with-base-layout)/page.tsx @@ -1,9 +1 @@ -"use client"; - -export default function MainPage() { - return ( -
-

Home

-
- ); -} +export { MainPage as default } from "@/pages/home"; diff --git a/apps/_template/app/layout.tsx b/apps/_template/app/layout.tsx index d152e0da3..5f5f63ad8 100644 --- a/apps/_template/app/layout.tsx +++ b/apps/_template/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Noto_Sans_KR } from "next/font/google"; -import { Providers } from "./providers"; -import "./globals.css"; +import { Providers } from "@/app/providers"; +import "@/app/styles/globals.css"; const notoSansKR = Noto_Sans_KR({ subsets: ["latin"], diff --git a/apps/as/pages/README.md b/apps/_template/pages/README.md similarity index 98% rename from apps/as/pages/README.md rename to apps/_template/pages/README.md index 287ea7ddb..e011944c3 100644 --- a/apps/as/pages/README.md +++ b/apps/_template/pages/README.md @@ -1,9 +1,9 @@ -# 이 폴더는 비워 둡니다 (FSD + Next.js App Router) - -Next.js가 `src/pages`를 **Pages Router**용 `pages` 디렉터리로 인식하지 않도록, -프로젝트 루트에 **빈 `pages` 폴더**를 두는 것이 FSD 공식 가이드 권장사항입니다. - -- 실제 페이지(FSD Pages 레이어)는 **`src/pages`**에 있습니다. -- Next.js 라우트는 **`app/`** 에서 `src/pages`를 re-export 하는 방식으로 연결됩니다. - -[Feature-Sliced Design - Usage with Next.js (App Router)](https://feature-sliced.design/docs/guides/tech/with-nextjs) +# 이 폴더는 비워 둡니다 (FSD + Next.js App Router) + +Next.js가 `src/pages`를 **Pages Router**용 `pages` 디렉터리로 인식하지 않도록, +프로젝트 루트에 **빈 `pages` 폴더**를 두는 것이 FSD 공식 가이드 권장사항입니다. + +- 실제 페이지(FSD Pages 레이어)는 **`src/pages`**에 있습니다. +- Next.js 라우트는 **`app/`** 에서 `src/pages`를 re-export 하는 방식으로 연결됩니다. + +[Feature-Sliced Design - Usage with Next.js (App Router)](https://feature-sliced.design/docs/guides/tech/with-nextjs) diff --git a/apps/_template/src/app/layouts/base-layout.tsx b/apps/_template/src/app/layouts/base-layout.tsx new file mode 100644 index 000000000..1081134e0 --- /dev/null +++ b/apps/_template/src/app/layouts/base-layout.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useCallback } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { useQueryClient } from "@tanstack/react-query"; +import { BaseLayout } from "@common/components"; +import type { MenuItem } from "@/entities/menu"; +import { PageLayout } from "@/shared/ui/page-layout"; + +interface BaseLayoutWrapperProps { + children: React.ReactNode; +} + +export function BaseLayoutWrapper({ children }: BaseLayoutWrapperProps) { + const router = useRouter(); + const pathname = usePathname(); + const queryClient = useQueryClient(); + + const handleMenuClick = useCallback( + (item: MenuItem) => { + router.push(item.urlPath); + }, + [router], + ); + + return ( + + {children} + + ); +} diff --git a/apps/_template/app/providers.tsx b/apps/_template/src/app/providers/index.tsx similarity index 96% rename from apps/_template/app/providers.tsx rename to apps/_template/src/app/providers/index.tsx index bc12c443b..05bac42cc 100644 --- a/apps/_template/app/providers.tsx +++ b/apps/_template/src/app/providers/index.tsx @@ -1,18 +1,18 @@ -"use client"; - -import { QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import { queryClient } from "@/shared/lib"; - -interface ProvidersProps { - children: React.ReactNode; -} - -export function Providers({ children }: ProvidersProps) { - return ( - - {children} - - - ); -} +"use client"; + +import { QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { queryClient } from "@/shared/lib"; + +interface ProvidersProps { + children: React.ReactNode; +} + +export function Providers({ children }: ProvidersProps) { + return ( + + {children} + + + ); +} diff --git a/apps/_template/app/globals.css b/apps/_template/src/app/styles/globals.css similarity index 100% rename from apps/_template/app/globals.css rename to apps/_template/src/app/styles/globals.css diff --git a/apps/_template/entities/README.md b/apps/_template/src/entities/README.md similarity index 100% rename from apps/_template/entities/README.md rename to apps/_template/src/entities/README.md diff --git a/apps/_template/entities/menu/index.ts b/apps/_template/src/entities/menu/index.ts similarity index 100% rename from apps/_template/entities/menu/index.ts rename to apps/_template/src/entities/menu/index.ts diff --git a/apps/_template/entities/menu/model/types.ts b/apps/_template/src/entities/menu/model/types.ts similarity index 100% rename from apps/_template/entities/menu/model/types.ts rename to apps/_template/src/entities/menu/model/types.ts diff --git a/apps/_template/features/README.md b/apps/_template/src/features/README.md similarity index 100% rename from apps/_template/features/README.md rename to apps/_template/src/features/README.md diff --git a/apps/as/src/pages/home/index.ts b/apps/_template/src/pages/home/index.ts similarity index 97% rename from apps/as/src/pages/home/index.ts rename to apps/_template/src/pages/home/index.ts index d11882ce5..97a463408 100644 --- a/apps/as/src/pages/home/index.ts +++ b/apps/_template/src/pages/home/index.ts @@ -1 +1 @@ -export { MainPage } from "./ui/main-page"; +export { MainPage } from "./ui/main-page"; diff --git a/apps/as/src/pages/home/ui/main-page.tsx b/apps/_template/src/pages/home/ui/main-page.tsx similarity index 95% rename from apps/as/src/pages/home/ui/main-page.tsx rename to apps/_template/src/pages/home/ui/main-page.tsx index 0370d5814..f757224dc 100644 --- a/apps/as/src/pages/home/ui/main-page.tsx +++ b/apps/_template/src/pages/home/ui/main-page.tsx @@ -1,9 +1,9 @@ -"use client"; - -export function MainPage() { - return ( -
-

Home

-
- ); -} +"use client"; + +export function MainPage() { + return ( +
+

Home

+
+ ); +} diff --git a/apps/_template/shared/README.md b/apps/_template/src/shared/README.md similarity index 100% rename from apps/_template/shared/README.md rename to apps/_template/src/shared/README.md diff --git a/apps/_template/shared/api/client.ts b/apps/_template/src/shared/api/client.ts similarity index 100% rename from apps/_template/shared/api/client.ts rename to apps/_template/src/shared/api/client.ts diff --git a/apps/_template/shared/api/index.ts b/apps/_template/src/shared/api/index.ts similarity index 100% rename from apps/_template/shared/api/index.ts rename to apps/_template/src/shared/api/index.ts diff --git a/apps/_template/shared/config/env.ts b/apps/_template/src/shared/config/env.ts similarity index 100% rename from apps/_template/shared/config/env.ts rename to apps/_template/src/shared/config/env.ts diff --git a/apps/_template/shared/config/index.ts b/apps/_template/src/shared/config/index.ts similarity index 100% rename from apps/_template/shared/config/index.ts rename to apps/_template/src/shared/config/index.ts diff --git a/apps/_template/shared/lib/dayjs.ts b/apps/_template/src/shared/lib/dayjs.ts similarity index 100% rename from apps/_template/shared/lib/dayjs.ts rename to apps/_template/src/shared/lib/dayjs.ts diff --git a/apps/_template/shared/lib/index.ts b/apps/_template/src/shared/lib/index.ts similarity index 100% rename from apps/_template/shared/lib/index.ts rename to apps/_template/src/shared/lib/index.ts diff --git a/apps/_template/shared/lib/query-client.ts b/apps/_template/src/shared/lib/query-client.ts similarity index 100% rename from apps/_template/shared/lib/query-client.ts rename to apps/_template/src/shared/lib/query-client.ts diff --git a/apps/_template/shared/model/index.ts b/apps/_template/src/shared/model/index.ts similarity index 100% rename from apps/_template/shared/model/index.ts rename to apps/_template/src/shared/model/index.ts diff --git a/apps/_template/shared/ui/index.ts b/apps/_template/src/shared/ui/index.ts similarity index 100% rename from apps/_template/shared/ui/index.ts rename to apps/_template/src/shared/ui/index.ts diff --git a/apps/_template/shared/ui/page-layout.tsx b/apps/_template/src/shared/ui/page-layout.tsx similarity index 100% rename from apps/_template/shared/ui/page-layout.tsx rename to apps/_template/src/shared/ui/page-layout.tsx diff --git a/apps/_template/widgets/README.md b/apps/_template/src/widgets/README.md similarity index 100% rename from apps/_template/widgets/README.md rename to apps/_template/src/widgets/README.md diff --git a/apps/_template/tsconfig.json b/apps/_template/tsconfig.json index 15b023415..dab9b0b8e 100644 --- a/apps/_template/tsconfig.json +++ b/apps/_template/tsconfig.json @@ -19,13 +19,7 @@ } ], "paths": { - "@/*": ["./*"], - "@/app/*": ["./app/*"], - "@/shared/*": ["./shared/*"], - "@/entities/*": ["./entities/*"], - "@/features/*": ["./features/*"], - "@/widgets/*": ["./widgets/*"], - "@/pages/*": ["./pages/*"], + "@/*": ["./src/*"], "@common/components": ["../../packages/common/components/dist/"], "@common/network": ["../../packages/common/network/"] }