๐ Next.js 2์ฅ: App Router ํด๋ถํ (ํ์ผ ์์คํ ๋ผ์ฐํ )
๐ ๊ฐ์
App Router์ ํ์ผ ์์คํ ๋ผ์ฐํ ๊ตฌ์กฐ์ layout, page, loading, error ํ์ผ์ ์ญํ ์ ํํค์นฉ๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐
page.tsx์layout.tsx์ ๊ณต์ ๊ด๊ณ ๐ข - ๐งฉ
layout.tsxvstemplate.tsxโ ๋ฌด์์ด ๋ค๋ฅผ๊น? ๐ก - ๐
layout.tsx+template.tsxํจ๊ป ์ฐ๋ฉด? ๐ก - ๐ญ ๋ผ์ฐํธ ๊ทธ๋ฃน
(folder)์ ๋ง๋ฒ ๐ก - ๐งช ๋ฐ๋ผํด๋ณด๊ธฐ: ๋ ๋๋ง ์ ์ง ํ์ธํ๊ธฐ
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 15๋ถ(์ ์ฒด) / ํต์ฌ ํํธ๋ง: 8๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
ํ์ผ ๊ธฐ๋ฐ ๋ผ์ฐํ
์ ์๋ฆฌ โ Layout ๊ณผ Template ์ ์ฐจ์ด โ Route Group โ ์ค์ ์ค์ต ( ์์๋ค ์ปค๋ฎค๋ํฐ ์์ )
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
-
layout.tsx์template.tsx๋ฅผ ์ํฉ์ ๋ง๊ฒ ๊ณจ๋ผ์ ์ธ ์ ์๋ค. - ์ํ(State) ๊ฐ ์ ์ง๋์ด์ผ ํ๋ UI ์ ์๋ก ๊ฐฑ์ ๋๋ UI ๋ฅผ ํด๋ ๊ตฌ์กฐ๋ก ๋ถ๋ฆฌ ์ค๊ณํ ์ ์๋ค.
- ๋ผ์ฐํธ ๊ทธ๋ฃน์ ํ์ฉํด ๋ณต์กํ ๋์๋ณด๋์ ์ ์ ํ์ด์ง์ ๋ ์ด์์์ ์ฐ์ํ๊ฒ ๊ณ ๋ฆฝ์ํฌ ์ ์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ๐ฃ ์์ฒ ( ์ ์ ): "๋ฆฌ๋ ๋! ์คํฐ๋ ์นดํ ๊ณ ๋ฆฌ ํญ์ ๋๋ฅผ ๋๋ง๋ค ํ๋ฉด์ด ๋์ปน๊ฑฐ๋ฆฌ๋ฉด์, ํ๋ค๊ฒ ๊ฒ์ํด๋ ์ฌ์ด๋๋ฐ ํํฐ๊ฐ์ด ์๊พธ ์ด๊ธฐํ๋ผ์! ์ ์ ๊ฒฝ๋ก๊ฐ ๋ฐ๋๋ฉด ์ปดํฌ๋ํธ๊ฐ ๋ค ์ฃฝ๋ ๊ฑด๊ฐ์?"
- ๐ฆ ์ํธ ( ๋ฆฌ๋ ): "์์ฒ ๋, ๊ทธ๊ฑด ๋ณํ์ง ์๋ ๋ฐฐ๊ฒฝ(Layout) ๊ณผ ๋ณํ๋ ์ฃผ์ธ๊ณต(Page) ์ ํ ํ์ผ์ ๋ค ๋๋ ค ๋ฃ์ด์ ๊ทธ๋์. ๋ฆฌ์กํธ ๋ผ์ฐํฐ ์์ ์ ๊ด์ต์ ์์ง ๋ชป ๋ฒ๋ฆฌ์ จ๊ตฐ์."
- ๐ ์์ ( PM ): "์์ฒ ์จ, ํํฐ๊ฐ ๋งค๋ฒ ์ด๊ธฐํ๋๋ฉด ์ ์ ๋ค์ด ์คํฐ๋ ์ฐพ๋ค๊ฐ ํ๋์ ์ฑ ์ง์ธ๊ฑธ์? ์ด๊ฑฐ ๋น์ฆ๋์ค์ ์ผ๋ก ์น๋ช ์ ์ ๋๋ค."
- ๐จ ์์ ( UX ): "๋ง์์. ๊ทธ๋ฆฌ๊ณ ํญ์ ๋๊ธธ ๋๋ง๋ค ํ๋ฉด์ด ์์
~ ์์๊ฒ ๋ํ๋๋ฉด ์ข๊ฒ ๋๋ฐ,
layout์ ์ ๋๋ก ๋ค์ ์ ๊ทธ๋ ค์ง๋ค๋ฉด์์? ์ ์ ๋๋ฉ์ด์ ์ ์ด์ฉ์ฃ ?" - ๐ฆ ์ํธ ( ๋ฆฌ๋ ): "์, ๋ค๋ค ์ฃผ๋ชฉ! ๊ทธ๋์ ์ฐ๋ฆฌ์๊ฒ Layout ๊ณผ Template, ๊ทธ๋ฆฌ๊ณ Route Group ์ด๋ผ๋ ๋๊ตฌ๊ฐ ์๋ ๊ฒ๋๋ค. ์ค๋ ์ด ๋ ์๋ค์ ํด๋ถํ์ ์ ๋๋ก ํํค์ณ ๋ณด์ฃ ."
๐ค ์ ์์์ผ ํ๋๊ฐ
์์๋ค ์ปค๋ฎค๋ํฐ("๊ฐ๋ฐ์ ์คํฐ๋ ๋งค์นญ ์ฑ") ํ๋ก์ ํธ ์ํฉ์ด์ผ.
์์ฒ (์๋ก ์จ ์ฃผ๋์ด) ์ด๊ฐ ์คํฐ๋ ์ฐพ๊ธฐ ํ์ด์ง(/studies) ์์ ๋ค๋น๊ฒ์ด์
ํญ(ํ๋ก ํธ์๋ ๋ชจ์ง, ๋ฐฑ์๋ ๋ชจ์ง) ์ ๋๋ฅผ ๋๋ง๋ค ํ๋ฉด ์ ์ฒด๊ฐ ๊น๋นก์ด๋ฉด์ ์ฌ์ด๋๋ฐ ํํฐ๊น์ง ์ด๊ธฐํ๋๋ ๋ฌธ์ ๋ฅผ ๊ฒช์์ด. ์์ฒ ์ด๋ React Router ์ฒ ํ๋๋ก ๊ฑฐ๋ํ App.tsx ์์ ๋ชจ๋ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ค๋ค ์ ์ง๋ณด์ ์ง์ฅ(Maintenance Nightmare) ์ ๋น ์ง๊ณ ๋ง ๊ฑฐ์ผ.
์ํธ(FE ๋ฆฌ๋) ๊ฐ ํ์จ์ ์ฌ๋ฉฐ ๋ค๊ฐ์์ด. "์์ฒ ์, Next.js App Router ์ layout.tsx ๊ฐ ์ ์กด์ฌํ๋์ง ์์ง ๋ชจ๋ฅด๋๊ตฌ๋. ๋ณํ์ง ์๋ ๋ฐฐ๊ฒฝ๊ณผ ๋ณํ๋ ์ฃผ์ธ๊ณต์ ํ์ผ ๋จ์๋ก ์ชผ๊ฐ์ผ ํด."
Next.js App Router ๋ "ํด๋ ์ด๋ฆ์ด ๊ณง URL ์ด๊ณ , ํ์ผ ์ด๋ฆ์ด ๊ณง ์ญํ ์ด๋ค" ๋ผ๋ ๊ฐ๋ ฅํ ์์น ์ ๊ณตํด. ํน์ ํ ์์ฝ์ด ํ์ผ๋ค(layout.tsx, template.tsx, loading.tsx) ์ ๋์ ๋ฐฉ์๊ณผ ์์กด ์ฃผ๊ธฐ(์๋ช
์ฃผ๊ธฐ) ๋ฅผ ๋ชจ๋ฅด๋ฉด, ๋ถํ์ํ ์ฌ๋ ๋๋ง์ ์์ํ ๊ณ ํต๋ฐ๊ฒ ๋ ๊ฑฐ์ผ.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
์ฐ๋ฆฌ๊ฐ ์ฌ๋ ์ง์ ๋ ์ฌ๋ ค๋ณด์. ๊ฑฐ์ค์ ๋ฒฝ์ง๋ ๊ทธ๋๋ก์ธ๋ฐ, TV ์ ํ๋ฉด๋ง ๋ฐ๋๋ ๊ฒ๊ณผ ๊ฑฐ์ค ์ธํ ๋ฆฌ์ด ์ ์ฒด๋ฅผ ๋งค๋ฒ ๋ค ๋ฏ์ด๊ณ ์น๋ ๊ฒ ์ค ๋ฌด์์ด ๋ ํจ์จ์ ์ผ๊น?
๐ผ๏ธ ๋งํธ๋ฃ์์นด ์ธํ๊ณผ ์ก์ ๋น์
Next.js ์ ๋ผ์ฐํ ๊ตฌ์กฐ๋ ๊ฒน๊ฒน์ด ์์ธ ์ก์ ํ๋ ์ ๊ณผ ๊ฐ์.
layout.tsx: ๊ฐ์ฅ ๋ฐ๊นฅ์ชฝ์ ํผํผํ ๋๋ฌด ์ก์ ํ. ์์ ๊ทธ๋ฆผ์ ์๋ฌด๋ฆฌ ๋ฐ๊ฟ ๋ผ์๋, ๋ฐ๊นฅ ์ก์ ํ์ ์ ํ ์์ง์ด๊ฑฐ๋ ๋ถ์์ง์ง ์์. (์ํ๊ฐ ์์ํ ์ ์ง๋จ)page.tsx: ์ก์ ์ ์๋งน์ด ๊ทธ๋ฆผ. URL ์ด ๋ฐ๋ ๋๋ง๋ค ๊ต์ฒด๋๋ ํต์ฌ ๋ด์ฉ์ด์ผ.template.tsx: ์๊น์๋layout๊ณผ ๋น์ทํ๋ฐ, ๊ทธ๋ฆผ ๊ต์ฒดํ ๋๋ง๋ค ์ก์ ํ๊น์ง ํ์ฌ์ญ ๋ถ์ด๋ฒ๋ฆฌ๊ณ ์ ์ก์๋ก ๋ค์ ๊ฐ์ ๋ผ์ฐ๋ ์๋ฏผํ ๋ ์ ์ด์ผ. ์ ๋๋ฉ์ด์ ์คํ์ด๋ ์ด๊ธฐํ๋ฅผ ๊ฐ์ ํ ๋ ์ฐ์ง.
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
"๋ ์ด์์์ ํผํผํ ์ฐ๋ฆฌ ์ง ๋ฒฝ์ด์ผ. ์ฐ๋ฆฌ๊ฐ ๊ฑฐ์ค์์ ๋ฐฉ์ผ๋ก ๊ฐ๋ ๋ฒฝ์ ๊ทธ๋๋ก์ง? ํ์ง๋ง ํ ํ๋ฆฟ์ ์ฐ๋ฆฌ๊ฐ ๋ฐฉ์ ๋ค์ด๊ฐ ๋๋ง๋ค ๋งค๋ฒ ์๋ก ํฐ๋จ๋ ค์ฃผ๋ ํ๋ คํ ํญ์ฃฝ ๊ฐ์ ๊ฑฐ์ผ!"
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
Layout ์ ๋ณด์กด, Template ์ ์ฌ์์ฑ์ด๋ค.
๐ page.tsx์ layout.tsx์ ๊ณต์ ๊ด๊ณ ๐ข
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- Next.js ๊ฐ ํ๋ฉด์ ์กฐ๋ฆฝํ ๋ ๋ถ๋ชจ-์์ ๊ด๊ณ๊ฐ ์ด๋ป๊ฒ ์ ์ง๋๋์ง ์ค๋ช ํ ์ ์๋ค.
- ์ํ ๋ณด์กด์ ์ํด ์ Layout ์ ์จ์ผ ํ๋์ง ๊ธฐ์ ์ ์ผ๋ก ์ฆ๋ช ํ ์ ์๋ค.
1. page.tsx: ๋ผ์ฐํธ์ ์ ์ผํ ์ฃผ์ธ๊ณต
ํด๋น ์ฃผ์(/studies)๋ก ์ ๊ทผํ์ ๋ ๊ทธ๋ ค์ง๋ ๋ฉ์ธ UI์ผ.
2. layout.tsx: ๋ณํ์ง ์๋ ๋ ๋ ํ ๋ฐฐ๊ฒฝ
์์ ๋ผ์ฐํธ ์ฌ์ด๋ฅผ ์ด๋ํ ๋ ์ํ(State)๋ฅผ ๋ณด์กดํ๊ณ ๋ฆฌ๋ ๋๋ง์ ์ฐจ๋จํด์ฃผ๋ ์ผ๋ฑ ๊ณต์ ์ด์ผ. ์์ฒ ์ด์ ๊ณ ๋ฏผ์ด ์ฌ๊ธฐ์ ํด๊ฒฐ๋์ง.
โ ์์งํ ์ฝ๋ (Naive Approach)
ํ์ด์ง ํธ๋์ง์
๋ก์ง์ page.tsx ์ต์๋จ ๋ถ๋ชจ์์ ๋ฌด๋ฆฌํ๊ฒ ์กฐ์ํด ํ๋ฉด ๊น๋นก์ ์ ๋ฐ.
โ ์ฐ์ํ ์ฝ๋ ( Pro Approach ) - ์ํธ์ ํด๊ฒฐ์ฑ
// app/studies/layout.tsx
// ๐ฆ ์ํธ: "์ด ํ์ผ์ URL์ด ๋ฐ๋์ด๋ ์ ๋ ๋ค์ ๋ ๋๋์ง ์๊ณ ๊ตณ๊ฑดํ ๋ฒํ๋๋ค."
export default function StudiesLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
{/* ๐ฃ ์์ฒ : "์! ์ด์ ํํฐ์ ์ ์ด๋ ๊ฒ์์ด๊ฐ ์ ์ฌ๋ผ์ ธ์!" */}
<nav>๊ฐ๋ ฅํ ์คํฐ๋ ๊ฒ์ ์ฌ์ด๋๋ฐ ํํฐ (์ํ ์ ์ง๋จ)</nav>
{/* ๐ฆ ์ํธ: "children ์๋ฆฌ์ page.tsx๋ง ๋ถ๋๋ฝ๊ฒ ๊ต์ฒด๋๋ ๊ฒ๋๋ค." */}
<main className="flex-1">{children}</main>
</div>
)
}๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
Page ๋ ๋งค๋ฒ ๋ฐ๋๋ ์ฃผ์ธ๊ณต, Layout ์ ๊ทธ ์ฃผ์ธ๊ณต์ ์ง์ผ์ฃผ๋ ๋ฌด๋ ๋ฐฐ๊ฒฝ์ด๋ค.
๐งฉ layout.tsx vs template.tsx โ ๋ฌด์์ด ๋ค๋ฅผ๊น? ๐ก
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
์์(๋์์ด๋)์ด๊ฐ ์๊ตฌํ์ด. "์คํฐ๋ ํญ ํ์ด์ง ์ด๋ํ ๋๋ง๋ค ํ๋ฉด์ด ์ผ์ชฝ์์ ์์ ~ ๋ํ๋๊ฒ (Framer Motion) ํด์ฃผ์ธ์! ๊ทธ๋ฆฌ๊ณ ์ด๋ํ ๋๋ง๋ค ํ์ด์ง ๋ทฐ(GA) ์ด๋ฒคํธ๋ ์์์ฃผ์๊ณ ์."
์ด๊ฑธ ์ ๋ ๋ฆฌ๋ ๋๋ง๋์ง ์๋ ์ฒ ๋ฒฝ๋ฐฉ์ดlayout.tsx์ ๋ถ์ฌ๋๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
์ด๊ฒ ๋ญ๊ฐ?
template.tsx๋ ๋ฌธ๋ฒ์ ์ผ๋ก layout๊ณผ ์์ ๋์ผ({ children } ํจํด)ํ์ง๋ง, ์ฌ์ฉ์๊ฐ ์์ ๋ผ์ฐํธ๋ก ์ง์
/๋ณ๊ฒฝํ ๋๋ง๋ค DOM ์์ฒด๋ฅผ ์ฐข๊ณ ์๋ก ์์ฑ(Re-mount)ํด๋ฒ๋ฆฌ๋ ํ์ผ์ด์ผ.
์ค๋ฌด์์ ์ด๋ป๊ฒ ํ์ฉ๋๋๊ฐ?
useEffect ๋ฅผ ์ด์ฉํด '๊ฒฝ๋ก ๋ฐฉ๋ฌธ ์ 1ํ ๋ฐ๋' ํ๋ ๋ก์ง์ด๋ ํธ๋์ง์
์ ๋๋ฉ์ด์
์ ๋ถ์ผ ๋ ์๋ฒฝํด.
// app/studies/template.tsx
'use client' // ๐จ ์์: "์ ๋๋ฉ์ด์
์ ํด๋ผ์ด์ธํธ์์ ๋์๊ฐ์ผ ํ๋๊น์!"
import { useEffect } from 'react'
export default function StudyTemplate({ children }: { children: React.ReactNode }) {
useEffect(() => {
// ๐ฆ ์ํธ: "ํญ์ ๋๋ฅผ ๋๋ง๋ค ์ฌ๊ธฐ๊ฐ ๋ฐ๋๋๋ ๋ง๋ฒ์ ๋ณด์ธ์."
console.log('๐ ํ์ด์ง ๋ทฐ ์ ํ ์ด๋ฒคํธ ์์ง ๋ฐ๋!')
}, [])
return <div className="animate-slide-in">{children}</div>
}๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
ํํฐ ๊ฐ์ ์งํค๊ณ ์ถ๋ค๋ฉด Layout, ํด๋ฆญํ ๋๋ง๋ค ๋ถ๊ฝ๋์ด๋ฅผ ํฐ๋จ๋ฆฌ๊ณ ์ถ๋ค๋ฉด Template ์ด๋ค.
๐ layout.tsx + template.tsx ํจ๊ป ์ฐ๋ฉด? ๐ก
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๊ฐ์ ํด๋์
layout.tsx์template.tsx๊ฐ ๊ณต์กดํ ๋ ๋ ๋ ์์๋ฅผ ์ค๋ช ํ ์ ์๋ค- ๋ ํ์ผ์ ์ญํ ์ ๋ถ๋ฆฌํด์ ์ค๋ฌด์ ์์ฉํ ์ ์๋ค
๋์ด ๊ณต์กดํ ์ ์์ด?
๊ฒฐ๋ก ๋ถํฐ ๋งํ๋ฉด ์๋ฒฝํ๊ฒ ๊ณต์กด ๊ฐ๋ฅํด. Next.js๋ ๊ฐ์ ํด๋์ layout.tsx์ template.tsx๊ฐ ๋ชจ๋ ์์ผ๋ฉด ์ด ์์๋๋ก ๊ฐ์ธ์ ๋ ๋ํด:
layout.tsx
โโ template.tsx
โโ page.tsx์ฆ layout์ด ๊ฐ์ฅ ๋ฐ๊นฅ์ชฝ, template์ด ๊ทธ ์์ชฝ, page๊ฐ ์๋งน์ด ๊ตฌ์กฐ์ผ.
์ธ์ ์ด๋ ๊ฒ ์ธ๊น?
์์๋ค ์ปค๋ฎค๋ํฐ ์ํฉ์ด์ผ. ์ํธ๊ฐ ์คํฐ๋ ๋ฆฌ์คํธ ํ์ด์ง์ ๋ ๊ฐ์ง๋ฅผ ๋์์ ์ํด:
- ๊ฒ์ ํํฐ ์ฌ์ด๋๋ฐ ์ํ๋ฅผ URL ์ด๋ ์ค์๋ ์ ์งํด์ผ ํ๋ค (โ
layout) - ํญ์ ๋๋ฌ ์ ์คํฐ๋ ๋ชฉ๋ก์ผ๋ก ์ด๋ํ ๋๋ง๋ค ํ์ด์ง ์ง์
์ ๋๋ฉ์ด์
๊ณผ GA ์ด๋ฒคํธ๋ฅผ ์ด์ผ ํ๋ค (โ
template)
// app/studies/layout.tsx โ ๋ฐ๊นฅ์ชฝ (์๊ตฌ ์์กด, ์ํ ๋ณด์กด)
export default function StudiesLayout({ children }) {
return (
<div className="flex">
<Sidebar /> {/* URL์ด ๋ฐ๋์ด๋ ์ด ์ฌ์ด๋๋ฐ๋ ์ ๋ ์ฌ๋ผ์ง์ง ์์ */}
<main>{children}</main>
</div>
)
}// app/studies/template.tsx โ ์์ชฝ (ํญ ์ด๋๋ง๋ค ๊ฐ์ ์ฌ๋ง์ดํธ)
'use client'
import { useEffect } from 'react'
export default function StudiesTemplate({ children }) {
useEffect(() => {
// ํญ์ ๋๋ฅผ ๋๋ง๋ค(URL ๋ณ๊ฒฝ ์) ์ฌ๊ธฐ๊ฐ ๋ฐ๋๋จ โ template์ด๊ธฐ ๋๋ฌธ
sendGAEvent('page_view', { path: window.location.pathname })
}, [])
return (
<div className="animate-fade-in"> {/* ์์
~ ํ์ด๋์ธ ์ ๋๋ฉ์ด์
*/}
{children}
</div>
)
}// app/studies/[category]/page.tsx โ ์๋งน์ด (์นดํ
๊ณ ๋ฆฌ๋ณ๋ก ๊ต์ฒด๋จ)
export default function StudyListPage({ params }) {
return <StudyList category={params.category} />
}๊ฒฐ๊ณผ:
- ์ฌ์ด๋๋ฐ ํํฐ ์ํ โ
layout์ด ๋ฒํ จ์ค์ URL ์ด๋ ํ์๋ ๋ณด์กด โ - ํ์ด๋์ธ ์ ๋๋ฉ์ด์
+ GA ์ด๋ฒคํธ โ
template์ด ๋งค๋ฒ ์ฌ๋ง์ดํธ๋๋ฉฐ ๋ฐ๋ โ
๋ ๋ ์์ ์ ๋ฆฌ
๐ค ์ ๊น, ๋จผ์ ์๊ฐํด๋ด
๋ง์ฝlayout์์template์ด ์๋ ๊ฒ ์๋๋ผ, ๋ฐ๋๋กtemplate์ดlayout์ ๊ฐ์ธ๊ณ ์๋ค๋ฉด ์ด๋ค ๋ฌธ์ ๊ฐ ์๊ธธ๊น? (ํํธ: ์ํ ์ ์ง)
[URL: /studies/frontend ์ง์
]
1. layout.tsx ๋ง์ดํธ (์ต์ด 1ํ, ์ดํ ๋ณด์กด)
2. template.tsx ๋ง์ดํธ (์ง์
๋ง๋ค ์๋ก ๋ง์ดํธ)
3. page.tsx ๋ ๋ (URL ๋ณ๊ฒฝ๋ง๋ค ๊ต์ฒด)
[URL: /studies/backend ์ด๋ ์]
1. layout.tsx โ ์ ์ง (์ฌ์ด๋๋ฐ ์ํ ์ด์์์) โ
2. template.tsx โ ํ๊ดด ํ ์๋ก ๋ง์ดํธ (์ ๋๋ฉ์ด์
/GA ๋ฐ๋) ๐
3. page.tsx โ ์ ์นดํ
๊ณ ๋ฆฌ๋ก ๊ต์ฒด ๐๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
Layout ์ ์๊ตฌ์ ์ธ ๊ทผ๊ฑฐ์ง, Template ์ ๊ทธ ์์์ ํญ๋ง๋ค ์๋ก ์ง์ด์ง๋ ํ ํธ์ ๊ฐ๋ค.
๐ญ ๋ผ์ฐํธ ๊ทธ๋ฃน (folder)์ ๋ง๋ฒ ๐ก
์์๋ค ์ปค๋ฎค๋ํฐ์๋ ์ผ๋ฐ ์ ์ ๋ค์ด ๋ณด๋ **"์คํ ์ด/์คํฐ๋ ๋ฉ์ธ ์์ญ"**๊ณผ ์์๋ง ๋ค์ด๊ฐ๋ "๊ด๋ฆฌ์ ์ด๋๋ฏผ(admin)" ์์ญ์ด ์์ด.
์ด ๋์ ์ฌ์ด๋๋ฐ ๋ชจ์, ํค๋๊น์ง 180๋ ๋ค๋ฅด๊ฒ ์๊ฒผ์ง.
โ ์์งํ ์ฝ๋ (Naive Approach)
๋ฃจํธ ์ต์๋จ app/layout.tsx ํ์ผ ๋ด์์ ๊ธธ๊ณ ์ง์ ๋ถํ if (pathname.includes('admin')) return AdminLayout ํํ์ ์ผํญ ์ฐ์ฐ์๋ฅผ ๋ฐ์๋ฒ๋ฆฌ๋ ๊ตฌ์๋์ ๋ถ๊ธฐ ์ฒ๋ฆฌ. ์๋ฒ์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ ๊ฐ ์ค์ผ ๋ฐ์.
โ
์ฐ์ํ ์ฝ๋ ( Pro Approach ) - ๋ผ์ฐํธ ๊ทธ๋ฃน ์ ์ฉ
ํด๋ ์ด๋ฆ ์๋ค์ ๊ดํธ () ๋ฅผ ๋ฌถ์ด์ฃผ์ด "URL ๊ฒฝ๋ก์์๋ ์์ ํ ์๋ต" ํ๋ฉด์ "์กฐ์ง์ ์ธ ํ์ผ ๊ทธ๋ฃน" ์ ๋ง๋ค์ด๋ด.
app/
โโ (consumer)/
โ โโ layout.tsx // ๐จ ์์: "์ผ๋ฐ ์ ์ ์ฉ ๊น๋ํ ๋ ์ด์์"
โ โโ page.tsx // url: '/'
โ โโ studies/page.tsx// url: '/studies'
โ
โโ (admin)/
โโ layout.tsx // ๐ ์์: "์์ปค๋จผ ์ด๋๋ฏผ ์ ์ฉ ์ฌ์ด๋๋ฐ ๋ ์ด์์!"
โโ dashboard/page.tsx // url: '/dashboard'์ด ๊ตฌ์กฐ ๋๋ถ์, ์๋ฒฝํ ๊ณ ๋ฆฝ๋ ์ต์์ ๋ ์ด์์์ 2๊ฐ ์ด์ ๊ฐ์ง ์ ์๊ฒ ๋ผ!
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
Route Group ์ URL ์ ํ์ ์ ๋จ๊ธฐ์ง ์๋ ์ ๋ น ํด๋์ด์, ๋ ์ด์์ ๋ถ๋ฆฌ ์๊ฑฐํจ์ด๋ค.
๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
โ You cannot define a route with the same specificity...
์์ธ: (consumer)/page.tsx ์ (admin)/page.tsx ๋ฅผ ๋ ๋ค ๋ง๋ค์ด ๋ฒ๋ ธ์.
๊ดํธ ๋ ์ด์ด๋ URL์์ ๋ฌด์๋๋ฏ๋ก, ๋ ํ์ด์ง ๋ชจ๋ ์ธํฐ๋ท ์ฃผ์๋ก๋ / ์ต์์๋ฅผ ๋ฐ๋ผ๋ณด๊ฒ ๋์ด ์ถฉ๋์ด ๋ฐ์ํด.
ํด๊ฒฐ์ฑ
: ๊น์ด๊ฐ ๊ฒน์น๋ ๋ฃจํธ ํ์ด์ง ๋ผ์ฐํ
์์๋ ๊ทธ๋ฃน ์ค ํ๋์๋ง ๋ฐฐ์นํ๊ฑฐ๋ ํด๋ ๊ฒฝ๋ก๋ฅผ ๋ช
์ํด์ผ ํด.
๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
์ค๋ ๋ฐฐ์ด ํต์ฌ์ ํ๋์ ์ ๋ฆฌํด๋ณผ๊น? ์ค๋ฌด์์ ๊ธธ์ ์์์ ๋ ์ด๊ฒ๋ง ๋ด๋ ๋ผ.
๐ ํน์ ํ์ผ๋ช ์ญํ ์์ฝ
| ํน์ ํ์ผ๋ช | ์ฃผ๋ ๋ชฉ์ | ๊ฒฝ๋ก ์ด๋ ์ ๋ง์ดํธ ์ ์ง ์ฌ๋ถ |
|---|---|---|
layout.tsx | ๊ตฌ์กฐ ์ ์ง, ์ฌ๋ ๋๋ง ๋ฐฉ์ง (์ํ ๋ณด์กด) | โ ์ ์ง๋จ (์๋ฒฝ ๋ฐฉ์ด) |
template.tsx | ํ์ด์ง ์ง์ ์ ๋๋ฉ์ด์ , ๊ฐ์ ์ด๊ธฐํ | โ ํ๊ดด ํ ์๋กญ๊ฒ ๋ง์ดํธ |
page.tsx | URL ์ ์ต์ข ์ฃผ์ธ๊ณต(๋ฉ์ธ ์ฝํ ์ธ ) | - |
(folder) | URL ์ ์ํฅ ์ ์ฃผ๊ณ ๋ ์ด์์ ๊ทธ๋ฃนํ ๋ถ๋ฆฌ | - |
โ ๏ธ ์ ๋ ํ์ง ๋ง ๊ฒ ( ์ํฐ ํจํด )
| ์ํฉ | โ ๋์ ์ | โ ์ข์ ์ |
|---|---|---|
| ๋ ์ด์์ ๋ถ๊ธฐ | pathname ์ฒดํฌํด์ ๊ฑฐ๋ํ if ๋ฌธ ๋๋ฆฌ๊ธฐ | Route Group ์ผ๋ก ๋ ์ด์์ ๋ฌผ๋ฆฌ์ ๋ถ๋ฆฌ |
| ์ํ ์ ์ง | ๋ฐ์ดํฐ ๋ณด์กด์ด ํ์ํ๋ฐ template ์ฐ๊ธฐ | ๋ฐ๋์ Layout ์ ์ฌ์ฉํ์ฌ ๋ฆฌ๋ ๋๋ง ์ฐจ๋จ |
| ํ์ด์ง ๊ตฌ์กฐ | ํ ํ์ผ์ ๋ชจ๋ ๋ฉ๋ด์ ๋ด์ฉ์ ๋ค ๋ฃ๊ธฐ | ์์ ๊ตฌ์กฐ์ ๋ง์ถฐ layout ๊ณผ page ๋ก ์ ์ ํ ์ชผ๊ฐ๊ธฐ |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ์์(๋์์ด๋) ์ด๊ฐ ์๊ตฌํ๋ค. "์ ์ ๊ฐ ๋ฌธ์ ํผ ์์ฑ ์ค์ ๋ค๋ก ๊ฐ๊ธฐ๋ฅผ ๋๋ฅด๊ฑฐ๋ ๋ค๋ฅธ ํญ์ผ๋ก ๋์ด๊ฐ๋ค ์๋, ์ ์ด๋์๋ ๊ธ์๋ค์ด ์ ๋(!!!) ๋ ์๊ฐ์ง ์๊ณ ์ ์ง๋์ด์ผ ํด์." ์์ฒ ์ด๋ ์ด ํผ ์ปดํฌ๋ํธ์ ๋ฃจํธ ์๋จ ๊ตฌ์กฐ๋ฅผ ์ด๋ค ํ์ผ ํฌ๋งท์ผ๋ก ์ธํ
ํด์ผ ํ ๊น? layout.tsx ์ template.tsx ์ค ํ๋๋ฅผ ๊ณ ๋ฅด๊ณ ์ด์ ๋ฅผ ์ค๋ช
ํด๋ผ.
โ
์ ๋ต: layout.tsx
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: ํผ ๋ฐ์ดํฐ(State) ๋ฅผ ๋ณด์กดํ๋ ค๋ฉด URL ์ด๋ ๊ฐ ์ปดํฌ๋ํธ๊ฐ ํ๊ดด(Unmount) ๋์ง ์์์ผ ํด.
layout.tsx๋ ์์ ๊ฒฝ๋ก๊ฐ ๋ฐ๋์ด๋ ์ํ๋ฅผ ๋๊น์ง ์ง์ผ์ฃผ๋ ์ฒ ๋ฒฝ ๋ฐฉ์ด๋ง ์ญํ ์ ํ๊ธฐ ๋๋ฌธ์ด์ผ. - ์ค๋ต ํผ๋๋ฐฑ: "์์ฒ ๋,
template.tsx๋ฅผ ์ฐ๋ ์๊ฐ ์ ์ ๊ฐ ํญ์ ๋๋ฅด์๋ง์ ์ ์ฑ๊ป ์ด ๊ธ์๊ฐ ์๊ฐ์ฒ๋ผ ์ฆ๋ฐํด ๋ฒ๋ฆด ๊ฑฐ์์!" - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "๊ธ์ชฝ๊ฐ์ ์ ์ ๋ฐ์ดํฐ๋ ๋ ์ด์์์ ๋งก๊ฒจ๋ผ."
Q2. ์ํธ(๋ฆฌ๋) ๊ฐ ํน์ ํ์ผ ํธ๋ฆฌ ์์ (auth) ๋ผ๋ ํด๋๋ฅผ ํ ๊ณ ๋ด๋ถ๋ก ๋ก๊ทธ์ธ์ ์ฎ๊ฒผ๋ค. ์ด๋ ๊ฒ ๋ผ์ฐํธ ๊ทธ๋ฃน์ ์์ฑํจ์ผ๋ก์จ ์ป๋ ๊ฐ์ฅ ๊ฐ๋ ฅํ "์ํคํ
์ฒ์ ์ฅ์ "์ ๋ฌด์์ธ์ง URL ์ ์ด์ ๊ด์ ์ด ์๋ layout ์ ๊ด์ ์์ ์ค๋ช
ํ๋ผ.
โ ์ ๋ต: ํน์ ๋ชฉ์ (์ธ์ฆ, ์ด๋๋ฏผ ๋ฑ) ์ ๋ง๊ฒ ๋ผ๋(Layout) ๋ฅผ ์์ ํ ์ชผ๊ฐ์ ๊ณ ๋ฆฝ์ํฌ ์ ์๋ค.
๐ก ์์ธ ํด์ค:
- ๊ด์ฌ์ฌ ๋ถ๋ฆฌ: ๋ก๊ทธ์ธ ํ์ด์ง(์ฌ๋ฐฑ ๊ฐ๋) ์ ๋ฉ์ธ ๋์๋ณด๋(์ฌ์ด๋๋ฐ ๋ณต์ก) ์ ๋ ์ด์์์ ํ๋์ ๋ถ๋ชจ ๋ฐ์์ ์ง์ ๋ถํ ์กฐ๊ฑด๋ฌธ ์์ด ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ถ๋ฆฌํ ์ ์์ด.
- ๋น์ฆ๋์ค ๊ฐ์น: ์ธ์ฆ ์์ญ์๋ง ํน๋ณํ ๋ณด์ ํค๋๋ ๋ก์ง์ ์ ์ฉํ๊ธฐ๊ฐ ๋งค์ฐ ์์ํด์ ธ์ ์ ์ง๋ณด์๊ฐ ์ฐ์ํด์ง์ง.
- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: "๋ผ์ฐํธ ๊ทธ๋ฃน์ ๋ ์ด์์์ ๊ตฌ์์๋ค."
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋ App Router์ ๊ตฌ์กฐ๋ฅผ ๋ฐฐ์ฐ๋ฉด์ ๋ฅ์คํธ์ ๊ฑฐ๋ํ ๋ณด๋ฌผ์ ๋ฐ๊ฒฌํ ๊ธฐ๋ถ์ด์ผ! ํนํ '๊ณต์ ๋๋ ๋ฐ์ฐฌํต(Layout)' ๊ณผ '๋งค๋ฒ ์๋ก ์ฐจ๋ฆฌ๋ ๊ตญ๋ฐฅ(Page)' ์ ์กฐํ๋ ์ ๋ง ์์ ์ด๋๋ผ๊ณ .
๐ก ์ค๋์ ๊ตํ: "๋ณํ๋ ๊ฒ(Page) ๊ณผ ๋ณํ์ง ์๋ ๊ฒ(Layout) ์ ๋ช ํํ ๋ถ๋ฆฌํ์ฌ ๋ธ๋ผ์ฐ์ ์ ์ผ์์ ๋์ด์ฃผ์!"
๊ทธ๋์ ํ์ด์ง๋ฅผ ์ฎ๊ธธ ๋๋ง๋ค ๋ชจ๋ ๊ฑธ ๋ค ์ง์ ๋ค ๋ค์ ๊ทธ๋ ธ๋ ๋ด ์ง๋๋ ๋ค์ด ์ํธ ๋ฆฌ๋ ๋์ ํฉํญ๊ณผ ํจ๊ป ์ค์ณ ์ง๋๊ฐ์ง๋ง... ๋ญ ์ด๋! ์ด์ ๋ผ๋ ์ ๋๋ก ๋ ๊ตฌ์กฐ๋ฅผ ์์์ผ๋ ๋์ง! ์ค๋ ์ง์ค๋ ฅ ์ข์๋ค. ์ง์ ๊ฐ์ ์์ํ ๋งฅ์ฃผ ํ ์บ ๋ฐ๊ณ ํน ์ฌ์ด์ผ๊ฒ ์ด. ๋ด์ผ์ ๋ด๊ฐ ์ค๋ ๋ฐฐ์ด ๋ ์ด์์ ์ค๊ณ ๋ฅ๋ ฅ์ ๋ฝ๋ด์ ์ํธ ๋์ ๊น์ง ๋๋ผ๊ฒ ํด๋๋ ค์ผ์ง! ๐ฃ