๐Ÿš€ Next.js 15์žฅ: ํ”„๋กœ์ ํŠธ ์•„ํ‚คํ…์ฒ˜ & ํด๋” ๊ตฌ์กฐ โ€” ์‹ค๋ฌด์—์„œ ์‚ด์•„๋‚จ๋Š” ์„ค๊ณ„

๐Ÿ“‹ ๊ฐœ์š”

์‹ค๋ฌด์—์„œ ์‚ด์•„๋‚จ๋Š” Next.js ํด๋” ๊ตฌ์กฐ์™€ Feature ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ ์›์น™์„ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 15๋ถ„ (์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 8๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ์˜์ฒ (์‹ ์ž…): "์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ์ด 50๊ฐœ ๋„˜์–ด๊ฐ€๋‹ˆ๊นŒ ์–ด๋””์— ๋ญ˜ ๋„ฃ์–ด์•ผ ํ• ์ง€ ๋ชจ๋ฅด๊ฒ ์–ด์š”. ๊ณตํ†ต ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ๋„ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ ์–ด๋”” ํด๋”์— ๋„ฃ์–ด์•ผ ํ•˜๊ณ , API ํ˜ธ์ถœํ•˜๋Š” ํ•จ์ˆ˜๋Š” ์–ด๋””์— ์žˆ์–ด์•ผ ํ•˜๊ณ , ์œ ํ‹ธ ํ•จ์ˆ˜๋Š” ๋˜ ์–ด๋””์—... utils/components/helpers ํด๋”๊ฐ€ ๋’ค์„ž์—ฌ์„œ ์–ด๋””์„œ ๋ญ˜ ์ฐพ์•„์•ผ ํ• ์ง€ ๋จธ๋ฆฟ์†์ด ํ˜ผ๋ˆ์ด์—์š”."
  • ์˜ํ˜ธ(๋ฆฌ๋“œ): "์˜์ฒ  ๋‹˜, ํด๋” ์ด๋ฆ„์ด ์•„๋‹ˆ๋ผ '์ด ํŒŒ์ผ์ด ํ•˜๋Š” ์ผ'๋กœ ๋ถ„๋ฅ˜ํ•ด์•ผ ํ•ด์š”. /lib์€ '์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋˜๋Š” ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง', /components/ui๋Š” '๋„๋ฉ”์ธ์„ ๋ชจ๋ฅด๋Š” ์ˆœ์ˆ˜ UI', /components/features๋Š” '๋น„์ฆˆ๋‹ˆ์Šค๋ฅผ ์•„๋Š” UI'. ์ด ์„ธ ๊ฐ€์ง€๋งŒ ์ง€์ผœ๋„ 50๊ฐœ๊ฐ€ 1000๊ฐœ ๋ผ๋„ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด์š”."

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
์‹ค๋ฌด ํด๋” ๊ธฐ์ค€ ๊ตฌ์กฐ โ†’ ๊ณ„์ธต๋ณ„ ์—ญํ•  ๊ทœ์น™ โ†’ DAL ํŒจํ„ด โ†’ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฅ˜ โ†’ ์˜์กด์„ฑ ๋ฐฉํ–ฅ

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • ์‹ค๋ฌด์—์„œ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ๋Š” Next.js ํ”„๋กœ์ ํŠธ ํด๋” ๊ตฌ์กฐ๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ์ƒˆ ํŒŒ์ผ์„ ๋งŒ๋“ค ๋•Œ ์–ด๋А ํด๋”์— ๋„ฃ์–ด์•ผ ํ•˜๋Š”์ง€ ๋ฐ”๋กœ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค
  • DAL ํŒจํ„ด์œผ๋กœ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๋กœ์ง์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก ํด๋” ๊ตฌ์กฐ๋Š” ์ ์  ์ค‘์š”ํ•ด์ ธ. ์ž˜๋ชป๋œ ๊ตฌ์กฐ๋กœ ์‹œ์ž‘ํ•˜๋ฉด:

  • ๊ฐ™์€ ํŒŒ์ผ์„ ๋‘ ๋ช…์ด ๋™์‹œ์— ์ˆ˜์ •ํ•˜๋Š” ์ถฉ๋Œ์ด ๋นˆ๋ฒˆํ•ด์ง
  • ์–ด๋””์— ๋ญ๊ฐ€ ์žˆ๋Š”์ง€ ๋ชฐ๋ผ์„œ ๋งค๋ฒˆ ์ „์ฒด ๊ฒ€์ƒ‰
  • ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ์ฝ”๋“œ๊ฐ€ ๋’ค์„ž์—ฌ ๋ณด์•ˆ ์ทจ์•ฝ์  ๋ฐœ์ƒ (์„œ๋ฒ„ ๋น„๋ฐ€ํ‚ค๊ฐ€ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์—!)
  • ์ˆœํ™˜ ์ฐธ์กฐ๋กœ ๋นŒ๋“œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ

"์ฒ˜์Œ์— ๋Œ€์ถฉ ๋งŒ๋“ค๊ณ  ๋‚˜์ค‘์— ์ •๋ฆฌํ•˜์ž"๋Š” ์ƒ๊ฐ์€ ํ”„๋กœ์ ํŠธ๊ฐ€ ํด์ˆ˜๋ก ๊ธฐ์ˆ  ๋ถ€์ฑ„๋กœ ๋Œ์•„์™€. ์ง€๊ธˆ ์˜ฌ๋ฐ”๋ฅธ ๊ตฌ์กฐ๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒŒ ๋‚˜์ค‘์— ๋ฆฌํŒฉํ† ๋ง 3๋ฒˆ ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋น ๋ฅด๋‹ค.


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?
๋„์„œ๊ด€์„ ์ƒ๊ฐํ•ด๋ด. ์ฑ…์„ ๊ทธ๋ƒฅ ์•„๋ฌด ๋ฐ๋‚˜ ๊ฝ‚์œผ๋ฉด ๋‚˜์ค‘์— ์ฐพ์„ ์ˆ˜ ์—†์–ด.
์ธ๋ฌธํ•™, ๊ณผํ•™, ์†Œ์„ค์ฒ˜๋Ÿผ ๋ถ„๋ฅ˜๊ฐ€ ์žˆ์–ด์•ผ ํ•ด.

ํ”„๋กœ์ ํŠธ ํด๋”๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์•ผ. "์ด ์ฝ”๋“œ๊ฐ€ ํ•˜๋Š” ์ผ"์— ๋”ฐ๋ผ ๋ถ„๋ฅ˜ํ•˜๋ฉด ์–ด๋””์— ์žˆ์„์ง€ ๋ฐ”๋กœ ์•Œ ์ˆ˜ ์žˆ์–ด.
lib์€ ๋ฐฑ๊ณผ์‚ฌ์ „ ๊ตฌ์—ญ(์„œ๋ฒ„ ํ•ต์‹ฌ ๋กœ์ง), components/ui๋Š” ์žก์ง€ ๊ตฌ์—ญ(์ˆœ์ˆ˜ UI), app์€ ์—ด๋žŒ์‹ค(๋ผ์šฐํŒ…).


๐Ÿ—‚๏ธ ์‹ค๋ฌด ํด๋” ๊ตฌ์กฐ์˜ ์ •์„ ๐ŸŸข

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • 2026๋…„ Next.js ์‹ค๋ฌด์—์„œ ๊ถŒ์žฅํ•˜๋Š” ํด๋” ๊ตฌ์กฐ๋ฅผ ์ดํ•ดํ•˜๊ณ  ๋ฐ”๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค
my-app/
โ”œโ”€โ”€ app/                          # ๋ผ์šฐํŒ… ์ „์šฉ โ€” ์ตœ๋Œ€ํ•œ ์–‡๊ฒŒ ์œ ์ง€
โ”‚   โ”œโ”€โ”€ (auth)/                   # ๋ผ์šฐํŠธ ๊ทธ๋ฃน (URL์— ์˜ํ–ฅ ์—†์Œ)
โ”‚   โ”‚   โ”œโ”€โ”€ login/page.tsx
โ”‚   โ”‚   โ””โ”€โ”€ signup/page.tsx
โ”‚   โ”œโ”€โ”€ (dashboard)/
โ”‚   โ”‚   โ”œโ”€โ”€ layout.tsx            # ๋Œ€์‹œ๋ณด๋“œ ๊ณตํ†ต ๋ ˆ์ด์•„์›ƒ
โ”‚   โ”‚   โ”œโ”€โ”€ page.tsx
โ”‚   โ”‚   โ””โ”€โ”€ settings/page.tsx
โ”‚   โ”œโ”€โ”€ posts/
โ”‚   โ”‚   โ”œโ”€โ”€ page.tsx
โ”‚   โ”‚   โ””โ”€โ”€ [id]/
โ”‚   โ”‚       โ”œโ”€โ”€ page.tsx
โ”‚   โ”‚       โ””โ”€โ”€ opengraph-image.tsx
โ”‚   โ”œโ”€โ”€ api/                      # Route Handlers
โ”‚   โ”‚   โ”œโ”€โ”€ posts/route.ts
โ”‚   โ”‚   โ””โ”€โ”€ webhooks/stripe/route.ts
โ”‚   โ”œโ”€โ”€ layout.tsx                # ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ
โ”‚   โ””โ”€โ”€ globals.css
โ”‚
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ ui/                       # ๋„๋ฉ”์ธ ๋ชจ๋ฅด๋Š” ์ˆœ์ˆ˜ UI ์›์ž ๋‹จ์œ„
โ”‚   โ”‚   โ”œโ”€โ”€ Button.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ Input.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ Modal.tsx
โ”‚   โ”‚   โ””โ”€โ”€ Badge.tsx
โ”‚   โ””โ”€โ”€ features/                 # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์•„๋Š” ๋„๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ
โ”‚       โ”œโ”€โ”€ auth/
โ”‚       โ”‚   โ”œโ”€โ”€ LoginForm.tsx
โ”‚       โ”‚   โ””โ”€โ”€ SignupForm.tsx
โ”‚       โ””โ”€โ”€ posts/
โ”‚           โ”œโ”€โ”€ PostCard.tsx
โ”‚           โ”œโ”€โ”€ PostList.tsx
โ”‚           โ””โ”€โ”€ PostEditor.tsx
โ”‚
โ”œโ”€โ”€ lib/                          # ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋˜๋Š” ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
โ”‚   โ”œโ”€โ”€ dal.ts                    # Data Access Layer (DB ์ ‘๊ทผ + ์ธ์ฆ ๊ฒ€์ฆ)
โ”‚   โ”œโ”€โ”€ db.ts                     # DB ํด๋ผ์ด์–ธํŠธ (Prisma, Drizzle ๋“ฑ)
โ”‚   โ”œโ”€โ”€ auth.ts                   # ์„ธ์…˜ ์ƒ์„ฑ/๊ฒ€์ฆ ํ•จ์ˆ˜
โ”‚   โ”œโ”€โ”€ actions/                  # Server Actions ๋ชจ์Œ
โ”‚   โ”‚   โ”œโ”€โ”€ post.actions.ts
โ”‚   โ”‚   โ””โ”€โ”€ auth.actions.ts
โ”‚   โ””โ”€โ”€ schemas/                  # Zod ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์Šคํ‚ค๋งˆ
โ”‚       โ”œโ”€โ”€ post.schema.ts
โ”‚       โ””โ”€โ”€ auth.schema.ts
โ”‚
โ”œโ”€โ”€ hooks/                        # ํด๋ผ์ด์–ธํŠธ ์ „์šฉ ์ปค์Šคํ…€ ํ›…
โ”‚   โ”œโ”€โ”€ useAuth.ts
โ”‚   โ””โ”€โ”€ useInfiniteScroll.ts
โ”‚
โ”œโ”€โ”€ types/                        # TypeScript ํƒ€์ž… ์ •์˜
โ”‚   โ”œโ”€โ”€ post.types.ts
โ”‚   โ””โ”€โ”€ user.types.ts
โ”‚
โ”œโ”€โ”€ public/                       # ์ •์  ํŒŒ์ผ (์ด๋ฏธ์ง€, ํฐํŠธ ๋“ฑ)
โ”‚   โ””โ”€โ”€ fonts/
โ”‚
โ””โ”€โ”€ middleware.ts                 # Edge Middleware (ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ)

๐Ÿงฉ ๊ณ„์ธต๋ณ„ ์—ญํ• ๊ณผ ๊ทœ์น™ โ€” ๋ฌด์—‡์ด ์–ด๋””์— ์žˆ์–ด์•ผ ํ•˜๋Š”๊ฐ€ ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ๊ฐ ํด๋”์˜ ์—ญํ• ์„ ํ•œ ์ค„๋กœ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ์ƒˆ ํŒŒ์ผ์„ ๋งŒ๋“ค ๋•Œ ์–ด๋А ํด๋”์— ๋„ฃ์–ด์•ผ ํ• ์ง€ ์ฆ‰์‹œ ํŒ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค

ํŒ๋‹จ ๊ธฐ์ค€: ์ด ์ฝ”๋“œ๊ฐ€ ์–ด๋””์„œ ์‹คํ–‰๋˜๊ณ , ๋ฌด์—‡์„ ์•„๋Š”๊ฐ€?

ํด๋”์‹คํ–‰ ํ™˜๊ฒฝ๋น„์ฆˆ๋‹ˆ์Šค ์•„๋Š”๊ฐ€์—ญํ• 
app/์„œ๋ฒ„ (๊ธฐ๋ณธ)O๋ผ์šฐํŒ…, ๋ ˆ์ด์•„์›ƒ, ํŽ˜์ด์ง€ ์กฐํ•ฉ
lib/์„œ๋ฒ„ ์ „์šฉODB ์ ‘๊ทผ, ์„ธ์…˜, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
components/features/์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธO๋„๋ฉ”์ธ ํฌํ•จ ๋ณตํ•ฉ UI
components/ui/์„œ๋ฒ„/ํด๋ผ์ด์–ธํŠธX์ˆœ์ˆ˜ UI, ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์›์ž ๋‹จ์œ„
hooks/ํด๋ผ์ด์–ธํŠธ๋งŒ๊ฒฝ์šฐ์— ๋”ฐ๋ผReact ์ƒํƒœ, ์ด๋ฒคํŠธ ํ•ธ๋“ค๋ง
types/์—†์Œ (ํƒ€์ž…๋งŒ)OTypeScript ํƒ€์ž… ์ •์˜

app/ ํด๋”๋Š” ์–‡๊ฒŒ ์œ ์ง€ํ•ด์•ผ ํ•ด:

// โŒ ์˜์ฒ ์ด์˜ ๋น„๋Œ€ํ•œ page.tsx โ€” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด page์— ์ง์ ‘
// app/posts/page.tsx
export default async function PostsPage() {
  // ์ด๋Ÿฐ ๋กœ์ง์ด page.tsx์— ์žˆ์œผ๋ฉด ์•ˆ ๋ผ
  const session = await cookies().get('session')
  const user = await prisma.user.findUnique({ where: { sessionId: session } })
  const posts = await prisma.post.findMany({ where: { authorId: user.id } })
  // ...
}
 
// โœ… ์˜ํ˜ธ๊ฐ€ ๊ถŒ์žฅํ•˜๋Š” ํŒจํ„ด โ€” page๋Š” ์กฐํ•ฉ๋งŒ, ๋กœ์ง์€ lib/dal.ts์—
// app/posts/page.tsx
import { getMyPosts } from '@/lib/dal'
 
export default async function PostsPage() {
  const posts = await getMyPosts()   // ์ธ์ฆ + DB ์กฐํšŒ + ๊ถŒํ•œ ์ฒดํฌ๊ฐ€ dal์—์„œ ์ฒ˜๋ฆฌ๋จ
  return <PostList posts={posts} />
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
app/ ํด๋”๋Š” "๋ ˆ๊ณ  ์กฐ๋ฆฝ ์„ค๋ช…์„œ"์•ผ. ๋ธ”๋ก์„ ๋งŒ๋“œ๋Š” ๊ณต์žฅ(lib, components)๊ณผ ์กฐ๋ฆฝํ•˜๋Š” ์‚ฌ๋žŒ(app)์€ ๋‹ฌ๋ผ์•ผ ํ•ด.


๐Ÿ”’ DAL (Data Access Layer) ํŒจํ„ด ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • DAL์ด ์™œ ํ•„์š”ํ•œ์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค
  • lib/dal.ts์— ์ธ์ฆ + ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์„ ๊ฒฐํ•ฉํ•œ ์•ˆ์ „ํ•œ ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿ“– ์šฉ์–ด: DAL (Data Access Layer) โ€” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ ‘๊ทผ ๋กœ์ง์„ ํ•œ ๊ณณ์— ๋ชจ์•„๋‘” ๊ณ„์ธต์ด์•ผ. ํŽ˜์ด์ง€๋‚˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ง์ ‘ DB๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ณ , ๋ฐ˜๋“œ์‹œ DAL์„ ํ†ตํ•˜๋„๋ก ํ•ด.

DAL์ด ์—†์œผ๋ฉด ์–ด๋–ค ์ผ์ด ์ƒ๊ธฐ๋‚˜:

// โŒ DAL ์—†๋Š” ์„ธ๊ณ„ โ€” ๋ชจ๋“  page.tsx๊ฐ€ ์ง์ ‘ DB์— ์ ‘๊ทผ
// app/posts/[id]/page.tsx
const post = await prisma.post.findUnique({ where: { id } })
// ์—ฌ๊ธฐ์„œ ์‹ค์ˆ˜๋กœ ๊ถŒํ•œ ์ฒดํฌ๋ฅผ ๋น ๋œจ๋ฆฌ๋ฉด? ๋‹ค๋ฅธ ์‚ฌ๋žŒ ๊ฒŒ์‹œ๊ธ€์„ ๋ˆ„๊ตฌ๋‚˜ ๋ณผ ์ˆ˜ ์žˆ๋Š” ๋ณด์•ˆ ๊ตฌ๋ฉ!
 
// app/posts/page.tsx์—์„œ๋„ ๋˜ prisma ์ง์ ‘ ํ˜ธ์ถœ...
// app/dashboard/page.tsx์—์„œ๋„ ๋˜...
// ๊ถŒํ•œ ์ฒดํฌ ๋กœ์ง์ด 20๊ตฐ๋ฐ์— ํฉ์–ด์ ธ ์žˆ์œผ๋ฉด ํ•˜๋‚˜๋ผ๋„ ๋น ๋œจ๋ฆด ์ˆ˜ ์žˆ์–ด
// โœ… DAL ํŒจํ„ด โ€” lib/dal.ts
import 'server-only'  // ์ด ํŒŒ์ผ์ด ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ์ ˆ๋Œ€ ํฌํ•จ๋˜์ง€ ์•Š๋„๋ก ๊ฐ•์ œ
import { cookies } from 'next/headers'
import { decrypt } from '@/lib/auth'
import { db } from '@/lib/db'
 
// ์„ธ์…˜ ๊ฒ€์ฆ ํ•จ์ˆ˜ โ€” DAL์˜ ๋ชจ๋“  ํ•จ์ˆ˜๋Š” ์ด๊ฑธ ๋จผ์ € ํ˜ธ์ถœํ•ด์•ผ ํ•ด
export async function verifySession() {
  const cookieStore = await cookies()
  const sessionCookie = cookieStore.get('session')?.value
 
  if (!sessionCookie) return null
 
  const session = await decrypt(sessionCookie)
  if (!session?.userId) return null
 
  return session
}
 
// ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก โ€” ์„ธ์…˜ ๊ฒ€์ฆ ํฌํ•จ
export async function getMyPosts() {
  const session = await verifySession()
  if (!session) throw new Error('๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ด์š”')
 
  return db.post.findMany({
    where: { authorId: session.userId },
    orderBy: { createdAt: 'desc' },
  })
}
 
// ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ โ€” ์ž‘์„ฑ์ž ๋˜๋Š” ๊ณต๊ฐœ ๊ฒŒ์‹œ๊ธ€๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
export async function getPost(id: string) {
  const session = await verifySession()
 
  const post = await db.post.findUnique({ where: { id } })
  if (!post) throw new Error('๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š”')
 
  // ๋น„๊ณต๊ฐœ ๊ฒŒ์‹œ๊ธ€์€ ์ž‘์„ฑ์ž๋งŒ ๋ณผ ์ˆ˜ ์žˆ์–ด
  if (post.isPrivate && post.authorId !== session?.userId) {
    throw new Error('์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์–ด์š”')
  }
 
  // DTO ํŒจํ„ด: ๋ฏผ๊ฐํ•œ ํ•„๋“œ(๋‚ด๋ถ€ ID, ๋น„๋ฐ€ ๋“ฑ) ์ œ๊ฑฐํ•˜๊ณ  ๋ฐ˜ํ™˜
  return {
    id: post.id,
    title: post.title,
    content: post.content,
    createdAt: post.createdAt,
    // authorInternalId: post.internalId  โ† ์ด๋Ÿฐ ๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ๋Š” ์ œ์™ธ
  }
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
DAL์€ "DB๋กœ ๊ฐ€๋Š” ์œ ์ผํ•œ ๋ฌธ"์ด์•ผ. ๋ชจ๋“  ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์€ ์ด ๋ฌธ์„ ํ†ตํ•ด์•ผ ํ•ด. ๋ฌธ ์•ž์— ํ•ญ์ƒ ๊ฒฝ๋น„์›(์„ธ์…˜ ๊ฒ€์ฆ)์ด ์„œ ์žˆ์–ด.


๐Ÿ“ฆ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฅ˜: ui vs features ๐ŸŸก

components/ui/ โ€” "๋„๋ฉ”์ธ์„ ๋ชจ๋ฅด๋Š” ์ˆœ์ˆ˜ UI"

// components/ui/Button.tsx
// โœ… ์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ "์ „ํ˜€ ๋ชจ๋ฅธ๋‹ค"๋Š” ๊ฒŒ ํ•ต์‹ฌ
// variant, size, onClick๋งŒ ์žˆ๊ณ  "๊ฒŒ์‹œ๊ธ€", "์œ ์ €", "๋Œ“๊ธ€" ๊ฐ™์€ ๊ฐœ๋…์ด ์ „ํ˜€ ์—†์Œ
 
interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
  isLoading?: boolean
  children: React.ReactNode
  onClick?: () => void
}
 
export function Button({ variant = 'primary', size = 'md', ...props }: ButtonProps) {
  return <button className={`btn btn-${variant} btn-${size}`} {...props} />
}

components/features/ โ€” "๋น„์ฆˆ๋‹ˆ์Šค๋ฅผ ์•„๋Š” ๋„๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ"

// components/features/posts/PostCard.tsx
// โœ… "๊ฒŒ์‹œ๊ธ€"์ด๋ผ๋Š” ๋„๋ฉ”์ธ์„ ์•Œ๊ณ  ์žˆ์Œ
// PostType, ์ข‹์•„์š”, ๋Œ“๊ธ€ ์ˆ˜, ์ž‘์„ฑ์ž ๊ฐ™์€ ๋น„์ฆˆ๋‹ˆ์Šค ๊ฐœ๋…์ด ํฌํ•จ๋จ
 
import { Button } from '@/components/ui/Button'  // ui ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
import type { Post } from '@/types/post.types'
 
interface PostCardProps {
  post: Post
  onLike: (postId: string) => void
}
 
export function PostCard({ post, onLike }: PostCardProps) {
  return (
    <div className="post-card">
      <h3>{post.title}</h3>
      <span>{post.author.name}</span>
      <Button variant="secondary" onClick={() => onLike(post.id)}>
        ์ข‹์•„์š” {post.likeCount}
      </Button>
    </div>
  )
}

๐Ÿšซ ์˜์กด์„ฑ ๋ฐฉํ–ฅ์˜ ๊ทœ์น™ โ€” ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€ ๐Ÿ”ด

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ์˜ฌ๋ฐ”๋ฅธ ์˜์กด์„ฑ ๋ฐฉํ–ฅ(app โ†’ components โ†’ lib)์„ ์ดํ•ดํ•˜๊ณ  ์œ„๋ฐ˜ ์ผ€์ด์Šค๋ฅผ ํƒ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค
์˜ฌ๋ฐ”๋ฅธ ์˜์กด์„ฑ ๋ฐฉํ–ฅ (๋‹จ๋ฐฉํ–ฅ, ํ•˜ํ–ฅ):

app/               โ†’ (์‚ฌ์šฉ) components/, lib/, hooks/
components/features โ†’ (์‚ฌ์šฉ) components/ui/, hooks/, types/
components/ui/     โ†’ (์‚ฌ์šฉ) types/
lib/               โ†’ (์‚ฌ์šฉ) types/, (์™ธ๋ถ€ ํŒจํ‚ค์ง€)
hooks/             โ†’ (์‚ฌ์šฉ) types/, (React ํ›…)

โŒ ์ ˆ๋Œ€ ๊ธˆ์ง€ โ€” ์—ญ๋ฐฉํ–ฅ ์ฐธ์กฐ:

lib/ โ†’ components/   (์„œ๋ฒ„ ๋กœ์ง์ด UI ์ปดํฌ๋„ŒํŠธ๋ฅผ import? ๋ง์ด ์•ˆ ๋ผ)
components/ui/ โ†’ components/features/  (ui๊ฐ€ ๋„๋ฉ”์ธ์„ ์•Œ๊ฒŒ ๋˜๋Š” ์ˆœ๊ฐ„ ์žฌ์‚ฌ์šฉ ๋ถˆ๊ฐ€)

์ˆœํ™˜ ์ฐธ์กฐ ์‹ค์ œ ์—๋Ÿฌ ์˜ˆ์‹œ:

// โŒ ์ˆœํ™˜ ์ฐธ์กฐ ์ง€์˜ฅ
// components/features/PostList.tsx
import { getMyPosts } from '@/lib/dal'  // features๊ฐ€ lib์„ import โ†’ ๋‚˜์˜์ง„ ์•Š์Œ
 
// lib/dal.ts
import { PostCard } from '@/components/features/posts/PostCard'  // โ† ์ด๊ฑด ๊ธˆ์ง€!
// lib(์„œ๋ฒ„ ๋กœ์ง)์ด UI ์ปดํฌ๋„ŒํŠธ๋ฅผ importํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ/์„œ๋ฒ„ ๊ฒฝ๊ณ„๊ฐ€ ๋ฌด๋„ˆ์ง

server-only ํŒจํ‚ค์ง€๋กœ ์‹ค์ˆ˜ ๋ฐฉ์ง€:

// lib/dal.ts ์ตœ์ƒ๋‹จ์— ์ถ”๊ฐ€
import 'server-only'
// ์ด ํŒŒ์ผ์ด ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ import๋˜๋ฉด ๋นŒ๋“œ ์—๋Ÿฌ๋ฅผ ๋‚ด์ค˜
// ์‹ค์ˆ˜๋กœ ์„œ๋ฒ„ ๋น„๋ฐ€์ด ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ๋“ค์–ด๊ฐ€๋Š” ๊ฑธ ๋ฐฉ์ง€

๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ


โŒ You're importing a component that needs... only works in a Client Component

์›์ธ: lib/ ์•ˆ์˜ ์„œ๋ฒ„ ์ „์šฉ ํ•จ์ˆ˜๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ import.

ํ•ด๊ฒฐ์ฑ…: ์„œ๋ฒ„ ๋กœ์ง์„ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋งŒ ํ˜ธ์ถœํ•˜๊ณ , ๊ฒฐ๊ณผ๋ฅผ props๋กœ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•ด.


โŒ Module not found โ€” tsconfig paths๊ฐ€ ์•ˆ ์ž‘๋™

์›์ธ: @/ ๊ฒฝ๋กœ ๋ณ„์นญ์ด ์„ค์ •๋˜์ง€ ์•Š์Œ.

ํ•ด๊ฒฐ์ฑ…:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  }
}

๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

๐Ÿ“‹ ํŒŒ์ผ ๋ฐฐ์น˜ ๊ฒฐ์ • ๊ธฐ์ค€

์ƒˆ ํŒŒ์ผ์˜ ํŠน์„ฑ์–ด๋””์— ๋„ฃ์–ด์•ผ ํ•˜๋Š”๊ฐ€
DB ์ง์ ‘ ์ ‘๊ทผ, ์ธ์ฆ ํ•„์š”lib/dal.ts (ํ•จ์ˆ˜ ์ถ”๊ฐ€)
Server Action (use server)lib/actions/*.ts
๋„๋ฉ”์ธ ๋ชจ๋ฅด๋Š” ์ˆœ์ˆ˜ ๋ฒ„ํŠผ/์ž…๋ ฅ/์นด๋“œcomponents/ui/
๊ฒŒ์‹œ๊ธ€/์œ ์ € ๊ฐ™์€ ๋„๋ฉ”์ธ ์•„๋Š” UIcomponents/features/[๋„๋ฉ”์ธ]/
useState, useEffect ํฌํ•จ ์ปค์Šคํ…€ ํ›…hooks/
TypeScript ํƒ€์ž…, ์ธํ„ฐํŽ˜์ด์Šค๋งŒtypes/
ํŽ˜์ด์ง€ ์กฐํ•ฉ, ๋ ˆ์ด์•„์›ƒapp/

โš ๏ธ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ

์ƒํ™ฉโŒ ๋‚˜์œ ์˜ˆโœ… ์ข‹์€ ์˜ˆ
page์—์„œ DB ์ง์ ‘ ์ ‘๊ทผpage.tsx์— Prisma ์ฝ”๋“œlib/dal.ts ํ•จ์ˆ˜ ํ˜ธ์ถœ
ui๊ฐ€ ๋„๋ฉ”์ธ ์•Œ๊ธฐButton์ด Post ํƒ€์ž… ์•Œ๊ธฐprops๋กœ ๋ฒ”์šฉ interface ์‚ฌ์šฉ
์—ญ๋ฐฉํ–ฅ ์ฐธ์กฐlib์ด components importlib์€ ์™ธ๋ถ€ ํŒจํ‚ค์ง€๋งŒ ์‚ฌ์šฉ

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์˜ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์„ DB์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜๋ฅผ ์–ด๋””์— ๋‘๋Š” ๊ฒƒ์ด ๋งž๋Š”๊ฐ€?

  • A) app/posts/page.tsx ์•ˆ์— inline์œผ๋กœ
  • B) components/features/posts/PostList.tsx ์•ˆ์—
  • C) lib/dal.ts ์•ˆ์— export ํ•จ์ˆ˜๋กœ
  • D) hooks/usePosts.ts ์•ˆ์—

โœ… ์ •๋‹ต: C

์˜ค๋‹ต ํ•ด์„ค:

  • A โ€” page๋Š” ์–‡๊ฒŒ ์œ ์ง€. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํฌํ•จ ๊ธˆ์ง€
  • B โ€” features ์ปดํฌ๋„ŒํŠธ๋Š” UI ๋‹ด๋‹น. DB ์ง์ ‘ ์ ‘๊ทผ์€ ์„œ๋ฒ„ ์ „์šฉ ๊ณ„์ธต์—์„œ
  • D โ€” hooks๋Š” ํด๋ผ์ด์–ธํŠธ ์ „์šฉ. DB ์ ‘๊ทผ ๋ถˆ๊ฐ€

๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: "DB ๊ฑด๋“œ๋ฆฌ๋Š” ๊ฑด ๋ฌด์กฐ๊ฑด lib/."


Q2. components/ui/Button.tsx์—์„œ importํ•˜๋ฉด ์•ˆ ๋˜๋Š” ๊ฒƒ์€?

  • A) React, ReactNode
  • B) CSS ๋ชจ๋“ˆ ํŒŒ์ผ
  • C) @/types/button.types.ts
  • D) @/components/features/posts/PostCard.tsx

โœ… ์ •๋‹ต: D

ํ•ด์„ค: ui ์ปดํฌ๋„ŒํŠธ๋Š” ๋„๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ(features)๋ฅผ ์•Œ๋ฉด ์•ˆ ๋ผ. ์žฌ์‚ฌ์šฉ์„ฑ์ด ์‚ฌ๋ผ์ง€๊ณ  ์˜์กด์„ฑ์ด ์—‰ํ‚ค๊ธฐ ์‹œ์ž‘ํ•ด.

๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: ui โ†’ features ๋ฐฉํ–ฅ import๋Š” ๊ธˆ์ง€. ui๋Š” ํ•ญ์ƒ "์•„๋ž˜ ๋ฐฉํ–ฅ"์œผ๋กœ๋งŒ.


Q3. ์นœ๊ตฌ์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

DAL ํŒจํ„ด์„ ์“ฐ์ง€ ์•Š์œผ๋ฉด ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋Š”์ง€ ๋น„์œ ๋กœ ์„ค๋ช…ํ•ด๋ด.

์˜ˆ์‹œ ๋‹ต๋ณ€:

"์€ํ–‰์— ๋น„์œ ํ•˜๋ฉด ์ด๋ž˜. DAL ์—†์ด ๋ชจ๋“  ์ฐฝ๊ตฌ(page)๊ฐ€ ์ง์ ‘ ๊ธˆ๊ณ (DB)์— ์ ‘๊ทผํ•˜๋ฉด, ์–ด๋А ์ฐฝ๊ตฌ๊ฐ€ ๊ถŒํ•œ ์ฒดํฌ๋ฅผ ๋น ๋œจ๋ ธ๋Š”์ง€ ์•Œ ์ˆ˜๊ฐ€ ์—†์–ด. DAL์€ ๊ธˆ๊ณ  ์•ž์— ๋”ฑ ํ•˜๋‚˜ ์žˆ๋Š” ๊ฒฝ๋น„์‹ค์ด์•ผ. ๊ฒฝ๋น„์‹ค ํ†ตํ•˜์ง€ ์•Š๊ณ ๋Š” ๊ธˆ๊ณ ์— ๋ชป ๋“ค์–ด๊ฐ€. ๊ถŒํ•œ ์ฒดํฌ๋ฅผ ๋น ๋œจ๋ฆด ์ˆ˜๊ฐ€ ์—†๋Š” ๊ตฌ์กฐ์•ผ."


๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์˜ค๋Š˜์€ ๋“œ๋””์–ด ๊ฐ€์ด๋“œ์˜ ๋งˆ์ง€๋ง‰ ์žฅ์ธ '์•„ํ‚คํ…์ฒ˜'๊นŒ์ง€ ๋งˆ์ณค์–ด! ์ฒ˜์Œ์—” ์ปดํฌ๋„ŒํŠธ ํ•˜๋‚˜ ํด๋”์— ๋„ฃ๋Š” ๊ฒƒ๋„ ๊ณ ๋ฏผ์ด์—ˆ๋Š”๋ฐ, ์ด์ œ๋Š” ์„œ๋น„์Šค๊ฐ€ ์•„๋ฌด๋ฆฌ ์ปค์ ธ๋„ ํ”๋“ค๋ฆฌ์ง€ ์•Š์„ ๋‹จ๋‹จํ•œ ๋ผˆ๋Œ€๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ž์‹ ๊ฐ์ด ์ƒ๊ฒผ์–ด.

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "ํด๋” ์ด๋ฆ„์ด ์•„๋‹ˆ๋ผ ํŒŒ์ผ์˜ ์—ญํ• ๋กœ ๋ถ„๋ฅ˜ํ•˜์ž. app์€ ์–‡๊ฒŒ, lib์€ ํŠผํŠผํ•˜๊ฒŒ! ๋‹จ๋‹จํ•œ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ์„œ๋น„์Šค๋ฅผ ์‚ด๋ฆฐ๋‹ค."

๋‹จ์ˆœํžˆ ๊ธฐ๋Šฅ์ด ๋Œ์•„๊ฐ€๋Š” ๊ฑธ ๋„˜์–ด, ๋ณด์•ˆ์„ ์ƒ๊ฐํ•˜๊ณ  ๋™๋ฃŒ๋“ค์ด ์ฐพ๊ธฐ ์‰ฌ์šด ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒŒ ์ง„์งœ '์‹œ๋‹ˆ์–ด'์˜ ๊ธธ์ด๋ผ๋Š” ์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜์˜ ๋ง์”€์ด ๊ฐ€์Šด์— ๊นŠ๊ฒŒ ๋ฐ•ํ˜”์–ด. ์•ž์œผ๋กœ ์ €ํฌ ์„œ๋น„์Šค๋ฅผ ์ธ์Šคํƒ€๋ณด๋‹ค ๋” ๋ฉ‹์ง€๊ฒŒ ๋งŒ๋“ค๋ฉด ์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜์ด ๋ง›์žˆ๋Š” ์ˆ˜์œก ์‚ฌ์ฃผ์‹œ๊ฒ ์ง€? ์˜ค๋Š˜์€ ๊ฐ€์ด๋“œ ์™„๋… ๊ธฐ๋…์œผ๋กœ ์ผ๊ธฐ์žฅ ๋ฎ๊ณ  ์ผ์ฐ ํ‡ด๊ทผํ•ด์„œ ๋„ทํ”Œ๋ฆญ์Šค๋‚˜ ๋ณด๋ฉด์„œ ํ‘น ์‰ฌ์–ด์•ผ๊ฒ ๋‹ค! ๐Ÿฃ


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ