Pink Bobblehead Bunny FSD์•„ํ‚คํ…์ฒ˜(Feature-Sliced Design) ๊ฐ€์ด๋“œ
 

FSD์•„ํ‚คํ…์ฒ˜(Feature-Sliced Design) ๊ฐ€์ด๋“œ

๐Ÿ“Œ FSD(Feature-Sliced Design)๋ž€?

ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ๋ณต์žก๋„๋Š” ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ์ฆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. "์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์–ด๋””์— ์žˆ์ง€?", "์ด ๊ธฐ๋Šฅ์„ ์ˆ˜์ •ํ•˜๋ฉด ์–ด๋””๊ฐ€ ์˜ํ–ฅ๋ฐ›์„๊นŒ?", "์ƒˆ๋กœ์šด ํŒ€์›์ด ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•˜๋Š”๋ฐ ์–ผ๋งˆ๋‚˜ ๊ฑธ๋ฆด๊นŒ?" ๊ฐ™์€ ๊ณ ๋ฏผ์ด ๋Š์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Feature-Sliced Design(FSD)์€ ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ํƒ„์ƒํ•œ ํ”„๋ก ํŠธ์—”๋“œ ์•„ํ‚คํ…์ฒ˜ ๋ฐฉ๋ฒ•๋ก ์ž…๋‹ˆ๋‹ค. ์ง์—ญํ•˜๋ฉด '๊ธฐ๋Šฅ ๋ถ„ํ•  ์„ค๊ณ„'๋กœ, ์ฝ”๋“œ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๊ทœ์น™๊ณผ ๊ด€๋ก€์˜ ์ง‘ํ•ฉ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐ŸŽฏ FSD๊ฐ€ ํ•ด๊ฒฐํ•˜๋Š” ๋ฌธ์ œ

๊ธฐ์กด ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋กœ์ ํŠธ์˜ ๊ณ ์งˆ์ ์ธ ๋ฌธ์ œ๋“ค

  1. ๋ถˆ๋ช…ํ™•ํ•œ ์ฝ”๋“œ ๊ตฌ์กฐ ๐Ÿ“‚
    • ์ปดํฌ๋„ŒํŠธ, ํ›…, ์œ ํ‹ธ๋ฆฌํ‹ฐ๊ฐ€ ๋’ค์„ž์—ฌ ์žˆ์–ด ์ฐพ๊ธฐ ์–ด๋ ค์›€
    • ๊ฐ์ž์˜ ์Šคํƒ€์ผ๋กœ ํด๋”๋ฅผ ๊ตฌ์„ฑํ•ด ์ผ๊ด€์„ฑ ๋ถ€์กฑ
  2. ๋†’์€ ๊ฒฐํ•ฉ๋„ ๐Ÿ”—
    • ํ•œ ๊ธฐ๋Šฅ์„ ์ˆ˜์ •ํ•˜๋ฉด ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ณณ์—์„œ ๋ฒ„๊ทธ ๋ฐœ์ƒ
    • ์ฝ”๋“œ ๊ฐ„ ์˜์กด์„ฑ์ด ๋ณต์žกํ•˜๊ฒŒ ์–ฝํ˜€์žˆ์Œ
  3. ๋‚ฎ์€ ์žฌ์‚ฌ์šฉ์„ฑ โ™ป๏ธ
    • ๋น„์Šทํ•œ ๊ธฐ๋Šฅ์„ ์—ฌ๋Ÿฌ ๊ณณ์—์„œ ์ค‘๋ณต์œผ๋กœ ๊ตฌํ˜„
    • ๊ณตํ†ต ๋กœ์ง์„ ์ถ”์ถœํ•˜๊ธฐ ์–ด๋ ค์›€
  4. ํ™•์žฅ์˜ ์–ด๋ ค์›€ ๐Ÿ“ˆ
    • ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ๋•Œ๋งˆ๋‹ค ๊ธฐ์กด ๊ตฌ์กฐ์™€ ์ถฉ๋Œ
    • ๋ฆฌํŒฉํ† ๋ง์ด ๋‘๋ ค์›Œ ๊ธฐ์ˆ  ๋ถ€์ฑ„๊ฐ€ ์Œ“์ž„

FSD๋Š” ์ด ๋ชจ๋“  ๋ฌธ์ œ๋ฅผ ๊ณ„์ธตํ™”๋œ ๊ตฌ์กฐ์™€ ๋ช…ํ™•ํ•œ ์˜์กด์„ฑ ๊ทœ์น™์œผ๋กœ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ—๏ธ FSD์˜ ํ•ต์‹ฌ ๊ตฌ์กฐ: 3๋‹จ๊ณ„ ๊ณ„์ธต

FSD๋Š” ํ”„๋กœ์ ํŠธ๋ฅผ Layer(๋ ˆ์ด์–ด)Slice(์Šฌ๋ผ์ด์Šค) Segment(์„ธ๊ทธ๋จผํŠธ)์˜ 3๋‹จ๊ณ„๋กœ ๊ตฌ์กฐํ™”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ฆ ํ”„๋กœ์ ํŠธ
โ”œโ”€โ”€ ๐Ÿ“‚ Layer (๋ ˆ์ด์–ด) - ์ตœ์ƒ์œ„ ๊ณ„์ธต
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ Slice (์Šฌ๋ผ์ด์Šค) - ๋น„์ฆˆ๋‹ˆ์Šค ๋„๋ฉ”์ธ๋ณ„ ๋ถ„๋ฆฌ
โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ Segment (์„ธ๊ทธ๋จผํŠธ) - ๊ธฐ๋Šฅ๋ณ„ ๋ถ„๋ฆฌ
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“„ ํŒŒ์ผ๋“ค

๐Ÿ“š Layer(๋ ˆ์ด์–ด): 7๊ฐ€์ง€ ๊ณ„์ธต์˜ ์„ธ๊ณ„

FSD๋Š” ์ด 7๊ฐœ์˜ ๋ ˆ์ด์–ด๋ฅผ ์ •์˜ํ•˜์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” 6๊ฐœ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. (Processes ๋ ˆ์ด์–ด๋Š” ํ˜„์žฌ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ)

๊ณ„์ธต ๊ตฌ์กฐ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ

โฌ†๏ธ ์ƒ์œ„ ๋ ˆ์ด์–ด (์‚ฌ์šฉ์ž์™€ ๊ฐ€๊นŒ์›€)
     โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
โ”‚   App   โ”‚  ๐Ÿ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ง„์ž…์  & ์ „์—ญ ์„ค์ •
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
     โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
โ”‚  Pages  โ”‚  ๐Ÿ“„ ๊ฐœ๋ณ„ ํŽ˜์ด์ง€ (๋ผ์šฐํŠธ ๋‹จ์œ„)
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
     โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
โ”‚ Widgets โ”‚  ๐Ÿงฉ ๋…๋ฆฝ์ ์ธ UI ๋ธ”๋ก (์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ)
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
     โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Features โ”‚  โšก ๋น„์ฆˆ๋‹ˆ์Šค ๊ธฐ๋Šฅ (์‚ฌ์šฉ์ž ํ–‰๋™)
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜
     โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Entities โ”‚  ๐Ÿ“ฆ ๋น„์ฆˆ๋‹ˆ์Šค ์—”ํ‹ฐํ‹ฐ (๋ฐ์ดํ„ฐ ๋ชจ๋ธ)
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜
     โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
โ”‚ Shared  โ”‚  ๐Ÿ”ง ๊ณตํ†ต ๋ฆฌ์†Œ์Šค (UI ์ปดํฌ๋„ŒํŠธ, ์œ ํ‹ธ)
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
     โ”‚
     โฌ‡๏ธ ํ•˜์œ„ ๋ ˆ์ด์–ด (๊ธฐ์ˆ ์  ๊ธฐ๋ฐ˜)

1๏ธโƒฃ App Layer (์• ํ”Œ๋ฆฌ์ผ€์ด์…˜)

์—ญํ• : ํ”„๋กœ์ ํŠธ์˜ ์ง„์ž…์ ์ด์ž ์ „์—ญ ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๋Š” ์ตœ์ƒ์œ„ ๋ ˆ์ด์–ด

ํฌํ•จ ์š”์†Œ:

  • ๋ผ์šฐํŒ… ์„ค์ • ๐Ÿ›ฃ๏ธ
  • ์ „์—ญ ์Šคํƒ€์ผ ๐ŸŽจ
  • Provider (Redux, Theme ๋“ฑ) ๐Ÿ”Œ
  • ์ „์—ญ ์ƒํƒœ ์ดˆ๊ธฐํ™” โš™๏ธ

ํŠน์ง•: ์Šฌ๋ผ์ด์Šค ์—†์ด ์„ธ๊ทธ๋จผํŠธ๋งŒ ํฌํ•จ

๐Ÿ“‚ app/
โ”œโ”€โ”€ ๐Ÿ“‚ providers/       // Redux Provider, Theme Provider
โ”œโ”€โ”€ ๐Ÿ“‚ styles/          // Global CSS, Reset CSS
โ”œโ”€โ”€ ๐Ÿ“‚ router/          // React Router ์„ค์ •
โ””โ”€โ”€ ๐Ÿ“„ index.tsx        // ์•ฑ ์ง„์ž…์ 

์‹ค์ œ ์˜ˆ์‹œ:

// app/index.tsx
import { ThemeProvider } from '@/app/providers/theme'
import { Router } from '@/app/router'

export const App = () => (
  <ThemeProvider>
    <Router />
  </ThemeProvider>
)

2๏ธโƒฃ Pages Layer (ํŽ˜์ด์ง€)

์—ญํ• : URL ๋ผ์šฐํŠธ์— ๋งคํ•‘๋˜๋Š” ๊ฐœ๋ณ„ ํŽ˜์ด์ง€๋ฅผ ๊ตฌ์„ฑ

ํŠน์ง•:

  • ๊ฐ ํŽ˜์ด์ง€๋Š” ํ•˜๋‚˜์˜ ์Šฌ๋ผ์ด์Šค
  • Widgets์™€ Features๋ฅผ ์กฐํ•ฉํ•ด ์™„์„ฑ๋œ ํŽ˜์ด์ง€ ๊ตฌ์„ฑ
  • ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋ณด๋‹ค๋Š” UI ์กฐํ•ฉ์— ์ง‘์ค‘
๐Ÿ“‚ pages/
โ”œโ”€โ”€ ๐Ÿ“‚ home/            // ํ™ˆ ํŽ˜์ด์ง€ ("/")
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ ui/
โ”‚   โ””โ”€โ”€ ๐Ÿ“„ index.ts
โ”œโ”€โ”€ ๐Ÿ“‚ user-profile/    // ํ”„๋กœํ•„ ํŽ˜์ด์ง€ ("/profile/:id")
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ ui/
โ”‚   โ””โ”€โ”€ ๐Ÿ“„ index.ts
โ””โ”€โ”€ ๐Ÿ“‚ settings/        // ์„ค์ • ํŽ˜์ด์ง€ ("/settings")
    โ”œโ”€โ”€ ๐Ÿ“‚ ui/
    โ””โ”€โ”€ ๐Ÿ“„ index.ts

์‹ค์ œ ์˜ˆ์‹œ:

// pages/user-profile/ui/UserProfilePage.tsx
import { UserHeader } from '@/widgets/user-header'
import { UserPosts } from '@/widgets/user-posts'
import { FollowButton } from '@/features/follow-user'

export const UserProfilePage = () => (
  <div>
    <UserHeader />
    <FollowButton />
    <UserPosts />
  </div>
)

3๏ธโƒฃ Widgets Layer (์œ„์ ฏ)

์—ญํ• : ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋Š” ํฐ UI ๋ธ”๋ก

ํŠน์ง•:

  • ์—ฌ๋Ÿฌ Features์™€ Entities๋ฅผ ์กฐํ•ฉ
  • ๋‹ค์–‘ํ•œ ํŽ˜์ด์ง€์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • ์ž์ฒด์ ์œผ๋กœ ์™„๊ฒฐ๋œ ๊ธฐ๋Šฅ ์ œ๊ณต

์˜ˆ์‹œ:

  • ๊ฒ€์ƒ‰๋ฐ” (SearchBar) ๐Ÿ”
  • ํ—ค๋”/๋„ค๋น„๊ฒŒ์ด์…˜ (Header, Sidebar) ๐Ÿ“
  • ๋Œ“๊ธ€ ์„น์…˜ (CommentSection) ๐Ÿ’ฌ
  • ๋Œ€์‹œ๋ณด๋“œ ์นด๋“œ (DashboardCard) ๐Ÿ“Š
๐Ÿ“‚ widgets/
โ”œโ”€โ”€ ๐Ÿ“‚ user-header/     // ์‚ฌ์šฉ์ž ํ—ค๋” ์œ„์ ฏ
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ ui/
โ”‚   โ”‚   โ””โ”€โ”€ UserHeader.tsx
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ model/
โ”‚   โ””โ”€โ”€ ๐Ÿ“„ index.ts
โ””โ”€โ”€ ๐Ÿ“‚ post-feed/       // ํฌ์ŠคํŠธ ํ”ผ๋“œ ์œ„์ ฏ
    โ”œโ”€โ”€ ๐Ÿ“‚ ui/
    โ”œโ”€โ”€ ๐Ÿ“‚ model/
    โ””โ”€โ”€ ๐Ÿ“„ index.ts

์‹ค์ œ ์˜ˆ์‹œ:

// widgets/user-header/ui/UserHeader.tsx
import { UserAvatar } from '@/entities/user'
import { FollowButton } from '@/features/follow-user'
import { EditProfileButton } from '@/features/edit-profile'

export const UserHeader = ({ userId }) => (
  <header>
    <UserAvatar userId={userId} />
    <FollowButton userId={userId} />
    <EditProfileButton userId={userId} />
  </header>
)

4๏ธโƒฃ Features Layer (๊ธฐ๋Šฅ)

์—ญํ• : ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ธฐ๋Šฅ ๋‹จ์œ„

ํŠน์ง•:

  • ์‚ฌ์šฉ์ž ํ–‰๋™๊ณผ ์ง์ ‘ ์—ฐ๊ฒฐ๋œ ๊ธฐ๋Šฅ
  • ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ ๊ฐ€๋Šฅ
  • ์—ฌ๋Ÿฌ ํŽ˜์ด์ง€/์œ„์ ฏ์—์„œ ์žฌ์‚ฌ์šฉ

์˜ˆ์‹œ:

  • ์ข‹์•„์š” ๋ฒ„ํŠผ (LikePost) โค๏ธ
  • ํŒ”๋กœ์šฐ/์–ธํŒ”๋กœ์šฐ (FollowUser) ๐Ÿ‘ฅ
  • ๋Œ“๊ธ€ ์ž‘์„ฑ (AddComment) ๐Ÿ’ฌ
  • ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ (AuthUser) ๐Ÿ”
๐Ÿ“‚ features/
โ”œโ”€โ”€ ๐Ÿ“‚ follow-user/     // ํŒ”๋กœ์šฐ ๊ธฐ๋Šฅ
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ ui/
โ”‚   โ”‚   โ””โ”€โ”€ FollowButton.tsx
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ model/       // ํŒ”๋กœ์šฐ ์ƒํƒœ ๊ด€๋ฆฌ
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ api/         // ํŒ”๋กœ์šฐ API ํ˜ธ์ถœ
โ”‚   โ””โ”€โ”€ ๐Ÿ“„ index.ts
โ””โ”€โ”€ ๐Ÿ“‚ like-post/       // ์ข‹์•„์š” ๊ธฐ๋Šฅ
    โ”œโ”€โ”€ ๐Ÿ“‚ ui/
    โ”œโ”€โ”€ ๐Ÿ“‚ model/
    โ”œโ”€โ”€ ๐Ÿ“‚ api/
    โ””โ”€โ”€ ๐Ÿ“„ index.ts

์‹ค์ œ ์˜ˆ์‹œ:

// features/follow-user/ui/FollowButton.tsx
import { useFollowUser } from '../model/useFollowUser'
import { Button } from '@/shared/ui/button'

export const FollowButton = ({ userId }) => {
  const { isFollowing, toggle } = useFollowUser(userId)
  
  return (
    <Button onClick={toggle}>
      {isFollowing ? '์–ธํŒ”๋กœ์šฐ' : 'ํŒ”๋กœ์šฐ'}
    </Button>
  )
}

5๏ธโƒฃ Entities Layer (์—”ํ‹ฐํ‹ฐ)

์—ญํ• : ๋น„์ฆˆ๋‹ˆ์Šค์˜ ํ•ต์‹ฌ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ๊ด€๋ฆฌ

ํŠน์ง•:

  • ํ”„๋กœ์ ํŠธ์˜ ํ•ต์‹ฌ ๋„๋ฉ”์ธ ๊ฐ์ฒด
  • ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์™€ ๊ธฐ๋ณธ ๋กœ์ง ํฌํ•จ
  • ์‚ฌ์šฉ์ž ํ–‰๋™๊ณผ ๋ฌด๊ด€ํ•œ ์ˆœ์ˆ˜ ๋ฐ์ดํ„ฐ ๊ณ„์ธต

์˜ˆ์‹œ:

  • User (์‚ฌ์šฉ์ž) ๐Ÿ‘ค
  • Post (๊ฒŒ์‹œ๋ฌผ) ๐Ÿ“
  • Comment (๋Œ“๊ธ€) ๐Ÿ’ฌ
  • Product (์ƒํ’ˆ) ๐Ÿ›’
๐Ÿ“‚ entities/
โ”œโ”€โ”€ ๐Ÿ“‚ user/            // ์‚ฌ์šฉ์ž ์—”ํ‹ฐํ‹ฐ
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ ui/
โ”‚   โ”‚   โ”œโ”€โ”€ UserCard.tsx
โ”‚   โ”‚   โ””โ”€โ”€ UserAvatar.tsx
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ model/       // ํƒ€์ž… ์ •์˜, ์ƒํƒœ ๊ด€๋ฆฌ
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ api/         // ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ fetch
โ”‚   โ””โ”€โ”€ ๐Ÿ“„ index.ts
โ””โ”€โ”€ ๐Ÿ“‚ post/            // ๊ฒŒ์‹œ๋ฌผ ์—”ํ‹ฐํ‹ฐ
    โ”œโ”€โ”€ ๐Ÿ“‚ ui/
    โ”œโ”€โ”€ ๐Ÿ“‚ model/
    โ”œโ”€โ”€ ๐Ÿ“‚ api/
    โ””โ”€โ”€ ๐Ÿ“„ index.ts

์‹ค์ œ ์˜ˆ์‹œ:

// entities/user/model/types.ts
export interface User {
  id: string
  name: string
  email: string
  avatar: string
  followersCount: number
}

// entities/user/ui/UserCard.tsx
import { User } from '../model/types'

export const UserCard = ({ user }: { user: User }) => (
  <div className="user-card">
    <img src={user.avatar} alt={user.name} />
    <h3>{user.name}</h3>
    <p>{user.followersCount} followers</p>
  </div>
)

6๏ธโƒฃ Shared Layer (๊ณต์œ )

์—ญํ• : ํ”„๋กœ์ ํŠธ ์ „๋ฐ˜์—์„œ ์žฌ์‚ฌ์šฉ๋˜๋Š” ๊ณตํ†ต ๋ฆฌ์†Œ์Šค

ํŠน์ง•:

  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๋ฌด๊ด€ํ•œ ์ˆœ์ˆ˜ ๊ธฐ์ˆ  ๊ณ„์ธต
  • ์Šฌ๋ผ์ด์Šค ์—†์ด ์„ธ๊ทธ๋จผํŠธ๋งŒ ํฌํ•จ
  • ํ•„์š”ํ•  ๋•Œ๋งˆ๋‹ค ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹

ํฌํ•จ ์š”์†Œ:

  • UI ์ปดํฌ๋„ŒํŠธ (Button, Input, Modal) ๐ŸŽจ
  • ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ (๋‚ ์งœ ํฌ๋งท, ๋ฌธ์ž์—ด ์ฒ˜๋ฆฌ) ๐Ÿ”ง
  • API ํด๋ผ์ด์–ธํŠธ (Axios ์ธ์Šคํ„ด์Šค) ๐ŸŒ
  • ์ƒ์ˆ˜/์„ค์ • (ํ™˜๊ฒฝ ๋ณ€์ˆ˜, ์„ค์ • ๊ฐ’) โš™๏ธ
๐Ÿ“‚ shared/
โ”œโ”€โ”€ ๐Ÿ“‚ ui/              // ๊ธฐ๋ณธ UI ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”œโ”€โ”€ button/
โ”‚   โ”œโ”€โ”€ input/
โ”‚   โ”œโ”€โ”€ modal/
โ”‚   โ””โ”€โ”€ index.ts
โ”œโ”€โ”€ ๐Ÿ“‚ lib/             // ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
โ”‚   โ”œโ”€โ”€ format/
โ”‚   โ”œโ”€โ”€ validation/
โ”‚   โ””โ”€โ”€ index.ts
โ”œโ”€โ”€ ๐Ÿ“‚ api/             // API ํด๋ผ์ด์–ธํŠธ
โ”‚   โ””โ”€โ”€ client.ts
โ””โ”€โ”€ ๐Ÿ“‚ config/          // ์ƒ์ˆ˜, ์„ค์ •
    โ””โ”€โ”€ constants.ts

์‹ค์ œ ์˜ˆ์‹œ:

// shared/ui/button/Button.tsx
export const Button = ({ children, variant = 'primary', ...props }) => (
  <button 
    className={`btn btn-${variant}`}
    {...props}
  >
    {children}
  </button>
)

// shared/lib/format/date.ts
export const formatDate = (date: Date) => {
  return new Intl.DateTimeFormat('ko-KR').format(date)
}

๐Ÿ”’ FSD์˜ ํ•ต์‹ฌ ์›์น™: ๋‹จ๋ฐฉํ–ฅ ์˜์กด์„ฑ

์˜์กด์„ฑ ๊ทœ์น™ (Dependency Rule)

FSD์˜ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ทœ์น™์€ "์ƒ์œ„ ๋ ˆ์ด์–ด๋งŒ ํ•˜์œ„ ๋ ˆ์ด์–ด์— ์˜์กดํ•  ์ˆ˜ ์žˆ๋‹ค"๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

App โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  ↓                            โ”‚
Pages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
  ↓                       โ”‚    โ”‚
Widgets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚    โ”‚
  ↓                   โ”‚   โ”‚    โ”‚
Features โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚   โ”‚    โ”‚
  ↓                โ”‚  โ”‚   โ”‚    โ”‚
Entities โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚  โ”‚   โ”‚    โ”‚
  ↓              โ”‚ โ”‚  โ”‚   โ”‚    โ”‚
Shared ←โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜

โœ… ํ—ˆ์šฉ๋˜๋Š” ์˜์กด์„ฑ

// โœ… Pages → Widgets
import { UserHeader } from '@/widgets/user-header'

// โœ… Widgets → Features
import { FollowButton } from '@/features/follow-user'

// โœ… Features → Entities
import { User } from '@/entities/user'

// โœ… Entities → Shared
import { Button } from '@/shared/ui/button'

โŒ ๊ธˆ์ง€๋˜๋Š” ์˜์กด์„ฑ

// โŒ Entities → Features (ํ•˜์œ„ → ์ƒ์œ„)
import { FollowButton } from '@/features/follow-user'

// โŒ Shared → Entities (ํ•˜์œ„ → ์ƒ์œ„)
import { User } from '@/entities/user'

// โŒ ๊ฐ™์€ ๋ ˆ์ด์–ด์˜ ๋‹ค๋ฅธ ์Šฌ๋ผ์ด์Šค ์ฐธ์กฐ
// features/follow-user → features/like-post
import { LikeButton } from '@/features/like-post'

๐ŸŽฏ ์ด ๊ทœ์น™์˜ ์žฅ์ 

  1. ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ ํ๋ฆ„ ๐Ÿ”ฎ
    • ์˜์กด์„ฑ์ด ํ•ญ์ƒ ํ•œ ๋ฐฉํ–ฅ์œผ๋กœ๋งŒ ํ๋ฆ„
    • ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€
  2. ์•ˆ์ „ํ•œ ๋ฆฌํŒฉํ† ๋ง ๐Ÿ”ง
    • ํ•˜์œ„ ๋ ˆ์ด์–ด ์ˆ˜์ • ์‹œ ์ƒ์œ„์—๋งŒ ์˜ํ–ฅ
    • ์ƒ์œ„ ๋ ˆ์ด์–ด ์ˆ˜์ •์€ ๋‹ค๋ฅธ ๋ ˆ์ด์–ด์— ๋ฌด์˜ํ–ฅ
  3. ๋ช…ํ™•ํ•œ ์ฑ…์ž„ ๋ถ„๋ฆฌ ๐Ÿ“‹
    • ๊ฐ ๋ ˆ์ด์–ด์˜ ์—ญํ• ์ด ๋ช…ํ™•
    • ์ฝ”๋“œ๋ฅผ ์–ด๋””์— ๋‘˜์ง€ ๊ณ ๋ฏผ ๊ฐ์†Œ

๐Ÿ• Slice(์Šฌ๋ผ์ด์Šค): ๋น„์ฆˆ๋‹ˆ์Šค ๋„๋ฉ”์ธ๋ณ„ ๋ถ„๋ฆฌ

์Šฌ๋ผ์ด์Šค๋ž€?

์Šฌ๋ผ์ด์Šค๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋„๋ฉ”์ธ์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ”๋“œ๋ฅผ ๋‚˜๋ˆ„๋Š” ๋‘ ๋ฒˆ์งธ ๊ณ„์ธต์ž…๋‹ˆ๋‹ค.

์ฃผ์˜: App๊ณผ Shared ๋ ˆ์ด์–ด๋Š” ์Šฌ๋ผ์ด์Šค๋ฅผ ๊ฐ€์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค!

์Šฌ๋ผ์ด์Šค ๋ช…๋ช… ๊ทœ์น™

// โœ… ์ข‹์€ ์Šฌ๋ผ์ด์Šค ์ด๋ฆ„ (๋น„์ฆˆ๋‹ˆ์Šค ๋„๋ฉ”์ธ)
features/
  follow-user/        // ์‚ฌ์šฉ์ž ํŒ”๋กœ์šฐ
  like-post/          // ๊ฒŒ์‹œ๋ฌผ ์ข‹์•„์š”
  edit-profile/       // ํ”„๋กœํ•„ ์ˆ˜์ •

entities/
  user/               // ์‚ฌ์šฉ์ž
  post/               // ๊ฒŒ์‹œ๋ฌผ
  comment/            // ๋Œ“๊ธ€

// โŒ ๋‚˜์œ ์Šฌ๋ผ์ด์Šค ์ด๋ฆ„ (๊ธฐ์ˆ ์  ๊ตฌ๋ถ„)
features/
  api/
  components/
  hooks/

์Šฌ๋ผ์ด์Šค ๋…๋ฆฝ์„ฑ ์›์น™

ํ•ต์‹ฌ ๊ทœ์น™: ๊ฐ™์€ ๋ ˆ์ด์–ด์˜ ๋‹ค๋ฅธ ์Šฌ๋ผ์ด์Šค๋ฅผ ์ฐธ์กฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค!

// โŒ ๊ธˆ์ง€: ๊ฐ™์€ ๋ ˆ์ด์–ด ๊ฐ„ ์ฐธ์กฐ
// features/follow-user/ui/FollowButton.tsx
import { LikeButton } from '@/features/like-post'

// โœ… ํ—ˆ์šฉ: ํ•˜์œ„ ๋ ˆ์ด์–ด ์ฐธ์กฐ
// features/follow-user/ui/FollowButton.tsx
import { User } from '@/entities/user'
import { Button } from '@/shared/ui/button'

์ด ๊ทœ์น™์ด ์ค‘์š”ํ•œ ์ด์œ :

  1. ๋‚ฎ์€ ๊ฒฐํ•ฉ๋„ ๐Ÿ”“
    • ๊ฐ ์Šฌ๋ผ์ด์Šค๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
    • ํ•œ ์Šฌ๋ผ์ด์Šค ์ˆ˜์ •์ด ๋‹ค๋ฅธ ์Šฌ๋ผ์ด์Šค์— ์˜ํ–ฅ ์—†์Œ
  2. ๋†’์€ ์‘์ง‘๋„ ๐ŸŽฏ
    • ๊ด€๋ จ๋œ ์ฝ”๋“œ๋Š” ํ•œ ์Šฌ๋ผ์ด์Šค์— ๋ชจ์ž„
    • ๊ธฐ๋Šฅ๋ณ„๋กœ ์™„๊ฒฐ๋œ ๊ตฌ์กฐ
  3. ๋ช…ํ™•ํ•œ ๊ฒฝ๊ณ„ ๐Ÿšง
    • ๊ฐ ๋น„์ฆˆ๋‹ˆ์Šค ๋„๋ฉ”์ธ์˜ ๊ฒฝ๊ณ„๊ฐ€ ๋ช…ํ™•
    • ์ฝ”๋“œ ์†Œ์œ ๊ถŒ์ด ๋ถ„๋ช…

๐Ÿ“ฆ Segment(์„ธ๊ทธ๋จผํŠธ): ๊ธฐ๋Šฅ๋ณ„ ๋ถ„๋ฆฌ

์„ธ๊ทธ๋จผํŠธ๋ž€?

์„ธ๊ทธ๋จผํŠธ๋Š” ์Šฌ๋ผ์ด์Šค ๋‚ด๋ถ€๋ฅผ ๊ธฐ๋Šฅ์  ์—ญํ• ๋กœ ๋‚˜๋ˆ„๋Š” ์„ธ ๋ฒˆ์งธ ๊ณ„์ธต์ž…๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์ธ ์„ธ๊ทธ๋จผํŠธ ์ข…๋ฅ˜

๐Ÿ“‚ user-slice/
โ”œโ”€โ”€ ๐Ÿ“‚ ui/              // ๐ŸŽจ UI ์ปดํฌ๋„ŒํŠธ
โ”‚   โ”œโ”€โ”€ UserCard.tsx
โ”‚   โ””โ”€โ”€ UserAvatar.tsx
โ”œโ”€โ”€ ๐Ÿ“‚ model/           // ๐Ÿ“Š ์ƒํƒœ ๊ด€๋ฆฌ & ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
โ”‚   โ”œโ”€โ”€ types.ts
โ”‚   โ”œโ”€โ”€ store.ts
โ”‚   โ””โ”€โ”€ hooks.ts
โ”œโ”€โ”€ ๐Ÿ“‚ api/             // ๐ŸŒ API ํ†ต์‹ 
โ”‚   โ”œโ”€โ”€ getUser.ts
โ”‚   โ””โ”€โ”€ updateUser.ts
โ”œโ”€โ”€ ๐Ÿ“‚ lib/             // ๐Ÿ”ง ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
โ”‚   โ””โ”€โ”€ validation.ts
โ””โ”€โ”€ ๐Ÿ“„ index.ts         // ๐Ÿ“ค Public API

๊ฐ ์„ธ๊ทธ๋จผํŠธ์˜ ์—ญํ• 

1. UI ์„ธ๊ทธ๋จผํŠธ ๐ŸŽจ

์—ญํ• : ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์ด๋Š” ์ปดํฌ๋„ŒํŠธ

// features/follow-user/ui/FollowButton.tsx
import { useFollowUser } from '../model/useFollowUser'
import { Button } from '@/shared/ui/button'

export const FollowButton = ({ userId }: { userId: string }) => {
  const { isFollowing, toggle, isLoading } = useFollowUser(userId)
  
  return (
    <Button 
      onClick={toggle}
      disabled={isLoading}
      variant={isFollowing ? 'secondary' : 'primary'}
    >
      {isFollowing ? 'ํŒ”๋กœ์ž‰' : 'ํŒ”๋กœ์šฐ'}
    </Button>
  )
}

2. Model ์„ธ๊ทธ๋จผํŠธ ๐Ÿ“Š

์—ญํ• : ์ƒํƒœ ๊ด€๋ฆฌ, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง, ํƒ€์ž… ์ •์˜

// features/follow-user/model/types.ts
export interface FollowState {
  followingIds: string[]
  isLoading: boolean
  error: string | null
}

// features/follow-user/model/useFollowUser.ts
import { useState } from 'react'
import { followUserApi, unfollowUserApi } from '../api'

export const useFollowUser = (userId: string) => {
  const [isFollowing, setIsFollowing] = useState(false)
  const [isLoading, setIsLoading] = useState(false)
  
  const toggle = async () => {
    setIsLoading(true)
    try {
      if (isFollowing) {
        await unfollowUserApi(userId)
      } else {
        await followUserApi(userId)
      }
      setIsFollowing(!isFollowing)
    } finally {
      setIsLoading(false)
    }
  }
  
  return { isFollowing, toggle, isLoading }
}

3. API ์„ธ๊ทธ๋จผํŠธ ๐ŸŒ

์—ญํ• : ์„œ๋ฒ„์™€์˜ ๋ฐ์ดํ„ฐ ํ†ต์‹ 

// features/follow-user/api/followUser.ts
import { apiClient } from '@/shared/api/client'

export const followUserApi = (userId: string) => {
  return apiClient.post(`/users/${userId}/follow`)
}

export const unfollowUserApi = (userId: string) => {
  return apiClient.delete(`/users/${userId}/follow`)
}

4. Lib ์„ธ๊ทธ๋จผํŠธ ๐Ÿ”ง

์—ญํ• : ํ•ด๋‹น ์Šฌ๋ผ์ด์Šค์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ

// features/edit-profile/lib/validation.ts
export const validateUsername = (username: string): boolean => {
  return username.length >= 3 && username.length <= 20
}

export const validateEmail = (email: string): boolean => {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}

๐Ÿ”‘ Public API: ์™ธ๋ถ€ ์ธํ„ฐํŽ˜์ด์Šค ๊ด€๋ฆฌ

Public API๋ž€?

๊ฐ ์Šฌ๋ผ์ด์Šค๋Š” index.ts ํŒŒ์ผ์„ ํ†ตํ•ด ์™ธ๋ถ€์— ๋…ธ์ถœํ•  ๊ฒƒ๋งŒ ์„ ๋ณ„์ ์œผ๋กœ ๊ณต๊ฐœํ•ฉ๋‹ˆ๋‹ค.

์žฅ์ 

  1. ์บก์Аํ™” ๐Ÿ“ฆ
    • ๋‚ด๋ถ€ ๊ตฌํ˜„์„ ์ˆจ๊ธฐ๊ณ  ์ธํ„ฐํŽ˜์ด์Šค๋งŒ ๋…ธ์ถœ
  2. ๋ฆฌํŒฉํ† ๋ง ์•ˆ์ •์„ฑ ๐Ÿ”’
    • ๋‚ด๋ถ€ ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์‹œ ์™ธ๋ถ€ ์˜ํ–ฅ ์ตœ์†Œํ™”
  3. ๋ช…ํ™•ํ•œ ๊ณ„์•ฝ ๐Ÿ“‹
    • ๋ฌด์—‡์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๋ช…ํ™•

์˜ˆ์‹œ

// features/follow-user/index.ts

// โœ… ์™ธ๋ถ€์— ๊ณต๊ฐœํ•  ๊ฒƒ๋งŒ export
export { FollowButton } from './ui/FollowButton'
export { useFollowUser } from './model/useFollowUser'
export type { FollowState } from './model/types'

// โŒ ๋‚ด๋ถ€ ๊ตฌํ˜„์€ ๊ณต๊ฐœํ•˜์ง€ ์•Š์Œ
// export { followUserApi } from './api/followUser'
// ๋‹ค๋ฅธ ๋ ˆ์ด์–ด์—์„œ ์‚ฌ์šฉ
import { FollowButton, useFollowUser } from '@/features/follow-user'

// โŒ ๊ธˆ์ง€: ๋‚ด๋ถ€ ํŒŒ์ผ ์ง์ ‘ ์ ‘๊ทผ
// import { FollowButton } from '@/features/follow-user/ui/FollowButton'

Feature-Sliced Design(FSD)๋Š” ๋Œ€๊ทœ๋ชจ ํ”„๋ก ํŠธ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์œ ์ง€๋ณด์ˆ˜์„ฑ๊ณผ ํ™•์žฅ์„ฑ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด ๊ณ ์•ˆ๋œ ์•„ํ‚คํ…์ฒ˜ ๋ฐฉ๋ฒ•๋ก ์ด๋‹ค. FSD์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๊ณผ ์›์น™, ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉ ์‹œ์˜ ์ด์ ์„ ์ •๋ฆฌํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

์ฐธ๊ณ ๋ฌธํ—Œ

https://velog.io/@clydehan/FSDFeature-Sliced-Design-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C

Overview | Feature-Sliced Design

Feature-Sliced Design: The Best Frontend Architecture