๐ Next.js 9์ฅ: Parallel & Intercepting Routes โ ๋ชจ๋ฌ๊ณผ ๋ผ์ฐํ ์ ๊ทนํ
๐ ๊ฐ์
Parallel Routes์ Intercepting Routes๋ก URL ๊ธฐ๋ฐ ๋ชจ๋ฌ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ Parallel Routes (
@folder): ํ ์ง๋ถ ๋ ๊ฐ์กฑ ๐ข - ๐ฑ Intercepting Routes (
(..)): ๋ด๊ฐ ์ฑ๊ฐ๋ ๋ง์ ๐ก - ๐ก๏ธ ์ธ์คํ๊ทธ๋จ ๋ชจ๋ฌ ์ค๊ณ: ๋ ๊ธฐ์ ์ ์๋ฒฝํ ์์๋ธ ๐ด
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 20๋ถ(์ ์ฒด) / ํต์ฌ ํํธ๋ง: 10๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
๋
๋ฆฝ๋ ์์ญ ๋ณ๋ ฌ ๋ ๋๊ธฐ(@) โ ํ๋ฆ ๊ฐ๋ก์ฑ๊ธฐ((..)) โ ์ด ๋์ ์กฐํฉํ ๊ถ๊ทน์ "๊ณต์ URL ๋ชจ๋ฌ" ํจํด ๋ง์คํฐ
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- ์ธ์คํ๊ทธ๋จ, ํํฐ๋ ์คํธ์ฒ๋ผ (1) ํผ๋ ์์์๋ ๋ชจ๋ฌ๋ก ๋จ๊ณ , (2) ๋จ์๊ฒ ์นดํก์ผ๋ก ๊ณต์ ํ URL์ ๋๋ฅด๋ฉด ์ ์ฒดํ๋ฉด ์ ์ฉ ํ์ด์ง๋ก ์ ์๋๋ ์ด์ค ๋ผ์ฐํ ์ํคํ ์ฒ๋ฅผ ๊ตฌํํ ์ ์๋ค.
- ํ๋์
layout.tsx์์์ A, B, C ๊ตฌ์ญ์ด ๊ฐ๊ฐ ๋จ์ ๋์น ์ ๋ณด๊ณ ๋ ๋ฆฝ์ ์ธ ๋ก๋ฉ/์๋ฌ ํ๋ฉด์ ๋ฟ์ด๋ด๋ "๋์๋ณด๋ ๋ณ๋ ฌ ๋ ๋๋ง" ์ ์ค๊ณํ ์ ์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์ฒ (์๋ก ์จ ์ฃผ๋์ด): "๋ฆฌ๋ ๋... ํผ๋์์ ์ฌ์ง์ ํด๋ฆญํ๋ฉด ๋ชจ๋ฌ๋ก ๋์์คฌ๊ฑฐ๋ ์. ๊ทธ๋ฐ๋ฐ ์ฌ์ฉ์๊ฐ ํ์ ์ ๋ซ์ผ๋ ค๊ณ '๋ค๋ก ๊ฐ๊ธฐ'๋ฅผ ๋๋ ๋๋ ์ฌ์ดํธ ๋ฐ์ผ๋ก ํ๊ฒจ ๋๊ฐ๋์! URL์ด ์ ๋ณํด์ ๊ทธ๋ฐ ๊ฒ ๊ฐ์๋ฐ, ๊ทธ๋ ๋ค๊ณ ์ ํ์ด์ง๋ก ์ด๋์ํค์๋ ๋ฐฐ๊ฒฝ ํผ๋๊ฐ ๋ค ์ฌ๋ผ์ ธ์. ๐ญ"
- ์ํธ(FE ๋ฆฌ๋): "์์ฒ ๋... ๋ชจ๋ฌ์ ์ต๊ณ ๋์ ์ธ 'URL ๋๊ธฐํ ๋ ์ด์ด' ๋ฅผ ๋ง์ฃผํ์
จ๊ตฐ์! Next.js์
@folder(๋ณ๋ ฌ ๋ผ์ฐํธ) ์(..)(๊ฐ๋ก์ฑ๊ธฐ ๋ผ์ฐํธ) ๋ฅผ ์์ด ์ฐ๋ฉด, URL์ ์ ํํ ๋ฐ๋๋ฉด์๋ ๋ฐฐ๊ฒฝ ํ๋ฉด์ ๊ทธ๋๋ก ์ ์ง๋๋ ์ธ์คํ๊ทธ๋จ ๋บจ์น๋ ํ์ ์ ๋ง๋ค ์ ์๋ค๊ณ ์!"
๐ค ์ ์์์ผ ํ๋๊ฐ
๊ณผ๊ฑฐ ๋ฆฌ์กํธ ์์ ์ ํ์
/๋ชจ๋ฌ ๋์ฐ๊ธฐ๋ ์ฌ์ ์ด. const [isOpen, setIsOpen] = useState(false) ํ๋ ์ ์ธํ๊ณ ์กฐ๊ฑด๋ถ ๋ ๋๋ง({isOpen && <Modal/>}) ๋๋ฆฌ๋ฉด ๋์ด์์ง.
ํ์ง๋ง ์ด ๋ฐฉ์์ ์น๋ช
์ ์ธ ๊ฒฐํจ์ด ์์๋จ๋ค.
- ๋ค๋ก ๊ฐ๊ธฐ ๋ถ๊ฐ๋ฅ: ๋ธ๋ผ์ฐ์ ํ์คํ ๋ฆฌ(URL)์ ๊ธฐ๋ก์ด ์ ๋จ์์, ๋ค๋ก ๊ฐ๊ธฐ๋ฅผ ๋๋ฅด๋ฉด ๋ชจ๋ฌ๋ง ๊บผ์ง๋ ๊ฒ ์๋๋ผ ์ ํ์ด์ง๋ก ์ด๋ํด๋ฒ๋ฆผ.
- ๊ณต์ ๋ถ๊ฐ๋ฅ: ์ด "์ฌ์ง ๋ชจ๋ฌ์ด ๋ ์๋ ์ํ" ์์ฒด๋ฅผ ์น๊ตฌํํ ๋งํฌ๋ก ๋ณต์ฌํด์ ์ค ์๊ฐ ์์.
- SEO ๋ถ๊ดด: ํฌ๋กค๋ฌ ์
์ฅ์์๋ ์จ๊ฒจ์ง
div๋ฉ์ด๋ฆฌ์ผ ๋ฟ, ๋ ๋ฆฝ๋ ๋ฌธ์(์ฌ์ง ์์ธ ์ ๋ณด)๋ก ์ธ์ํ์ง ๋ชปํจ.
์ด๊ฑธ ๊ณ ์น๋ ค๋ฉด ๋ชจ๋ฌ์ ๋์ธ ๋ ๊ฐ์ ๋ก URL์ ๋ฐ๊ฟ์ผ ํ๋๋ฐ(์: /feed -> /photo/123), ๊ณผ๊ฑฐ์ URL์ด ๋ฐ๋๋ฉด ๋ฐฐ๊ฒฝ์ ๊น๋ ค์๋ ํผ๋ ๋ชฉ๋ก์ด ์น ๋ ์๊ฐ๊ณ ์๋ก์ด ๋ฐฑ์ง ๋ ๋ ์์ญ(๋ชจ๋ฌ๋ง ๋ฉ๊ทธ๋ฌ๋ ์๋ ํ์ด์ง) ์ด ๋ํ๋๋ ํ๊ณ๊ฐ ์์์ด.
์ด ๋ชจ๋ฌ ๋๋ ๋ง๋ฅผ "์๋ฒ ์ปดํฌ๋ํธ ํ๋ ์์ํฌ" ์ฐจ์์์ ๊ฐ์ฅ ์ฐ์ํ๊ฒ, ์๋ฒฝํ๊ฒ ๋ฐ์ด ๋ด๋ฒ๋ฆฐ Next.js์ ์๋์ค๋ฌ์ด ๋ ๊ฐ์ง ๊ณ ๊ธ ํ์ผ ์์คํ ๋ฃฐ์ ๋ฐฐ์๋ณด์.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด? (์์๋ค ๊ตญ๋ฐฅ์ง์ ๋ ๋ฆฝ ํ ์ด๋ธ๊ณผ ์์ ๊ฐ๋ก์ฑ๊ธฐ)
Parallel Routes (
@) : 1์ธ์ฉ ์นธ๋ง์ด ํ ์ด๋ธ
์๋น ํ๊ฐ์ด๋ฐ์ ๋ ๋ฆฝ๋ ๋ฐฅ์ 3๊ฐ(@table1,@table2,@table3) ๊ฐ ๋์ฌ ์๋ ๊ฑฐ์ผ.
1๋ฒ ์๋์ด ๊น๋๊ธฐ๋ฅผ ๋ ๋ฌ๋ผ๊ณ ํด๋(@analytics) , 2๋ฒ ์๋์ ์๊ธฐ ๊ตญ๋ฐฅ์ ์กฐ์ฉํ ๋จน๊ณ ์์ด.
์ ํ ์ด๋ธ์์ ๊ทธ๋ฆ์ ๊นจ๋จ๋ ค๋(@error) , ๋ด ํ ์ด๋ธ ์๋์ ์๋ฌด ์ผ ์๋ค๋ ๋ฏ ๋ฐฅ์ ๋จน์ ์ ์๋ ์๋ฒฝํ ๋ ๋ฆฝ ์์ฌ ๊ณต๊ฐ ์ด์ผ.Intercepting Routes (
(..)): ์ฃผ๋ฐฉ ์ ๊ฐ๋ก์ฑ๊ธฐ ์์ํ
์๋ "ํน๋ ์์ก" ์ฃผ๋ฌธ(photo/1) ์ "์์ชฝ ๋จ์ฒด ๋ฐฉ" ์ผ๋ก ๋ฐฐ๋ฌ๋์ด์ผ ํด.
๊ทธ๋ฐ๋ฐ ์๋์ด ๋ณต๋๋ฅผ ์ง๋๊ฐ๋ค๊ฐ ์ฃผ๋ฐฉ ์ ๊ตฌ์์ "์ ๊น! ๊ทธ ์์ก ๋ ํ ์ ๋ง ๋ง๋ณด๊ฒ ํด์ค!" ํ๊ณ ๋ชฉ์ ์ง๋ก ๊ฐ๋ ์์ก์ ์ค๊ฐ์ ๋์์ฑ์(Intercept) ํ์ฌ ์๋ ๋ณต๋(ํผ๋ ํ๋ฉด) ์์ ํ ์ ๋จน์ด๋ณด๋ ๋ง์ ์ด์ผ.
๋ค ๋จน๊ณ ์ ์๋ฅผ ๋๋ ค์ฃผ๋ฉด(๋ค๋ก ๊ฐ๊ธฐ) , ๋ค์ ์๋ ์๋ฆฌ๋ก ๋์๊ฐ๊ณ , ์์ ์ฒ์๋ถํฐ "๋จ์ฒด ๋ฐฉ" ์ผ๋ก ๋ค์ด์จ ์๋(์ง์ URL ์ ์) ์ ์ ๋๋ก ๋ ์์ก ํ ์์ ๋ฐ๊ฒ ๋ผ.
๐งฉ Parallel Routes (@folder): ํ ์ง๋ถ ๋ ๊ฐ์กฑ ๐ข
๋จผ์ ๋ณ๋ ฌ ๋ผ์ฐํธ์ผ. ํด๋ ์ด๋ฆ ์์ ๊ณจ๋ฑ
์ด(@)๋ฅผ ๋ถ์ด๋ฉด, Next.js๋ ์ด๊ฑธ ๋จ์ํ URL ๊ฒฝ๋ก ์กฐ๊ฐ์ผ๋ก ํด์ํ์ง ์๊ณ "๋ถ๋ชจ Layout์ ๊ฝ์ ๋ฃ์ ๋
๋ฆฝ๋ ์ฌ๋กฏ(Slot)" ์ผ๋ก ์ทจ๊ธํด.
์ํฉ: ๋ณต์กํ ๊ด๋ฆฌ์ ๋์๋ณด๋
๋์๋ณด๋(Root) ์์ 1) ์ฌ์ฉ์ ํต๊ณํ์ 2) ์ต์ ์๋ฆผ์ฐฝ ๋ ๊ฐ์ ๊ฑฐ๋ํ ๊ตฌ์ญ์ด ์์ด.
app/
โโ layout.tsx (๋ผ๋ ๋ ์ด์์)
โโ page.tsx (๊ฐ์ด๋ฐ ๊ธฐ๋ณธ ๊ตฌ์ญ)
โโ @analytics/ (๊ณจ๋ฑ
์ด! ์ฌ๋กฏ A)
โ โโ page.tsx
โโ @notifications/ (๊ณจ๋ฑ
์ด! ์ฌ๋กฏ B)
โโ page.tsx์๋น ๋ ์ด์์(layout.tsx)์ ๋ง๋ฒ ์ฌ๋กฏ ์กฐ๋ฆฝ
@ํ์ผ๋ช
ํด๋๋ฅผ ๋ง๋ค๋ฉด, Next.js ํ๋ ์์ํฌ ์ฝ์ด๊ฐ ์๋์ผ๋ก layout.tsx ์ปดํฌ๋ํธ์ ํ๋ผ๋ฏธํฐ(Props)์ ๊ทธ ์ด๋ฆ๊ณผ ๋๊ฐ์ children์ ๋ชฐ๋ ์ฃผ์
ํด์ค! ์์ฒญ๋ ํ๋ง๋ฒ์ด์ง.
// app/layout.tsx
export default function DashboardLayout({
children, // ์๋ ๋ค์ด์ค๋ ๊ธฐ๋ณธ page.tsx ๊ตฌ์ญ
analytics, // @analytics ํด๋ ๋ด๋ถ์ ๋ ๋๋ง ๊ฒฐ๊ณผ๋ฌผ์ด ํต์งธ๋ก ๋ค์ด์ด! (๋ง์ )
notifications, // @notifications ๋ด๋ถ ๊ฒฐ๊ณผ๋ฌผ
}: {
children: React.ReactNode
analytics: React.ReactNode
notifications: React.ReactNode
}) {
return (
<body>
<main className="grid">
<div className="center"> {children} </div>
{/* ํ ์ง๋ถ(layout) ์๋์ 3๊ฐ์กฑ์ด ๋
๋ฆฝ๋ ์คํฌ๋ฆฐ์ผ๋ก ๊ณต์กดํ๋ค! */}
<aside className="left"> {analytics} </aside>
<aside className="right"> {notifications} </aside>
</main>
</body>
)
}์ด ์ง์ ์ ํ๋์? (์ต๋ ์ฅ์ )
์ 3๊ฐ์ ๊ตฌ์ญ(children, analytics, notifications)์ ์์ ํ ๋จ๋จ์ฒ๋ผ ํ๋ํด.
- ๋ง์ฝ ํต๊ณ(
analytics) ๋ฐ์ดํฐ ์กฐํ๊ฐ 5์ด๋ ๊ฑธ๋ ค์ ๋ก๋ฉ ์ค(์์ฒดloading.tsx๋ณด์ )์ด๊ฑฐ๋ ํฐ์ ธ๋(์์ฒดerror.tsx๋ณด์ ), ์ค๋ฅธ์ชฝ์ ์๋ฆผ์ฐฝ(notifications)์ ๋์น ์ ๋ณด๊ณ 0.1์ด ๋ง์ ๋ ๋๋ง์ ๋๋ด๊ณ ํผ์ ๊ทธ๋ ค์ ธ. ์๋ฒฝํ ๊ฒฉ๋ฆฌ ๋ณด๋ฃจ๊ฐ ๋๋ ๊ฑฐ์ผ!
๐ฑ Intercepting Routes ((..)): ๋ด๊ฐ ์ฑ๊ฐ๋ ๋ง์ ๐ก
์ด๊ฒ ๋ฐ๋ก ์ธ์คํ๊ทธ๋จ์ด ํผ๋ ๋๋ฅผ ๋ ์ ์ฐฝ์ผ๋ก ์ ๊ฐ๊ณ ์ฌ์ง๋ง ๋์ฐ๋ ํต์ฌ ๊ธฐ์ ์ด์ผ. ํด๋ ์ด๋ฆ์ ์๋๊ฒฝ๋ก ์ฉ์ฉ๊ดํธ ๊ธฐํธ (..)๋ฅผ ๋ถ์ฌ์ ๋ง๋ค์ด.
(.) : ๋์ ๊ฐ์ ํด๋ ๋ ๋ฒจ ๊ฒฝ๋ก ๊ฐ๋ก์ฑ๊ธฐ
(..) : ๋๋ณด๋ค ํ ๋จ๊ณ ์๋ฐฉ(๋ถ๋ชจ) ๊ฒฝ๋ก ๊ฐ๋ก์ฑ๊ธฐ
(..)(..) : ๋ ๋จ๊ณ ์๋ฐฉ ๊ฐ๋ก์ฑ๊ธฐ
(...) : ์์ ์ต์๋จ app/ ๊ธฐ์ค ๋ฃจํธ(Root) ๊ฐ๋ก์ฑ๊ธฐ๐ฏ ์์ฒ ์ด์ ์ธ์คํ๊ทธ๋จ ๋ง๋ค๊ธฐ ๋ฏธ์
์ฐ๋ฆฌ๋ ๊ธฐ๋ณธ์ ์ผ๋ก /photo/123 ์ด๋ผ๋ ๊ณ ์ ์ฃผ์๋ฅผ ๊ฐ์ง ์ ์งํ๊ณ ์ปค๋ค๋ "์ฌ์ง ์ ์ฉ ์ ํ์ด์ง"๋ฅผ ๊ฐ์ง๊ณ ์์ด. (๋จํํ
์นดํก์ผ๋ก ๊ณต์ ํ์ ๋ ๋จ์ด์ง๋ ๋์ฐฉ์ง)
app/
โโ feed/
โ โโ page.tsx (๊ฒ์๋ฌผ ๋ชฉ๋ก)
โโ photo/
โโ [id]/
โโ page.tsx (๊ทธ๋ฅ ์ ์ํ๋ฉด ๋จ๋ ์ปค๋ค๋ ์ ์ฒด์ฌ์ง ํ์ด์ง)์ด ์ํ์์ feed/page.tsx ์์ ์ฌ์ง ๋งํฌ <Link href="/photo/123"> ๋ฅผ ํด๋ฆญํ๋ฉด? ์ํ๊น๊ฒ๋ ํ๋ฉด ๋ฐฐ๊ฒฝ์ด ํ์๊ฒ ์น ๋ค ์ง์์ง๋ฉฐ ๋ป๊ฑด ์ ์ฒดํ๋ฉด ์ฌ์ง ํ์ด์ง /photo/123 ์ผ๋ก ๋์ด๊ฐ๋ฒ๋ ค.
์ด๊ฑธ ๋ง์ผ๋ ค๋ฉด? ํผ๋(Feed) ๊ตฌ์ญ ๋ด๋ถ์์, ์๋ /photo/ ๋ฐฉ์ผ๋ก ํ๋ฌ ๋ค์ด๊ฐ์ผ ํ ๋ผ์ฐํ
ํ๋ฆ์ ๊ฐ๋ก์ฑ๋ฉด ๋ผ!
๐ก๏ธ ์ธ์คํ๊ทธ๋จ ๋ชจ๋ฌ ์ค๊ณ: ๋ ๊ธฐ์ ์ ์๋ฒฝํ ์์๋ธ ๐ด
์ด์ ์์์ ๋ฐฐ์ด ๋ ๊ฐ์ ๋ฌด๊ธฐ(๋ณ๋ ฌ @, ๊ฐ๋ก์ฑ๊ธฐ (..))๋ฅผ ํฉ์ฒดํด ์ ์ค์ ๋ฌด๊ธฐ(Soft Navigation Modal)๋ฅผ ๋ฒผ๋ ค๋ด์.
์๋ฒฝํ ๋ชจ๋ฌ ํด๋ ๊ตฌ์กฐ ์ค๊ณ๋
app/
โโ layout.tsx
โโ feed/
โ โโ layout.tsx โ
๋ชจ๋ฌ ์ฌ๋กฏ์ ๊ตฌ๋นํด๋์
โ โโ page.tsx โ
<Link href="/photo/1">๊ฐ ์๋ ๋ชฉ๋ก
โ โโ @modal/ โ
[Parallel] ๋ชจ๋ฌ์ ๋์ธ ์ ์ฉ ๋
๋ฆฝ ๋ณ๋ ฌ ์ฌ๋กฏ
โ โโ default.tsx (ํ์ ๋ก๋ฉ ์ ์ฉ ํฌ๋ช
์ธ๊ฐ ๋ทฐ)
โ โโ (..)photo/ โ
[Intercept] ํ๋ฆ ๊ฐ๋ก์ฑ๊ธฐ ํด๋! (ํผ๋์ ํ์ ์ธ photo๋ฅผ ๋์์ฑ)
โ โโ [id]/
โ โโ page.tsx โ
์ฌ๊ธฐ๊ฐ ๋ฐ๋ก ํผ๋ ๋ฐฐ๊ฒฝ์ ๊น๊ณ ๋จ๋ "๋ชจ๋ฌ ๋ฐ์ค" ์ปดํฌ๋ํธ!
โ
โโ photo/ โ
[Original] ์ฌ๊ธฐ๋ ๋ณํจ ์์ด ์กด์ฌํ๋ "๊ทผ๋ณธ ์ ์ฒดํ๋ฉด" ๋ชฉ์ ์ง.
โโ [id]/
โโ page.tsx โ
์นดํก/์ธ์คํ๋ก ๋ณต์ฌ๋ ๋งํฌ ์ง๊ฒฐ ์ ์ ์ ๋จ๋ ๊ฑฐ๋ํ ํ๋ฉด![๋ง๋ฒ์ด ํผ์ณ์ง๋ ์๊ฐ]
-
์ํฉ 1: ์นดํก์ผ๋ก ๋งํฌ ๊ณต์ ๋ฐ์ ๋ธ๋ผ์ฐ์ ์ ์ฉ์ผ๋ก ๋ณต๋ถ ์ ์ํ์ ๋ (Hard Navigation)
๋๊ฐ/photo/1์ณค๋ค. Next.js ์์ง์ "๊ฐ๋ก์ฑ ์ถ๋ฐ์ง๊ฐ ์๋ค?" ํ๊ณ ์ ์งํ๊ฒ ๊ทผ๋ณธ ๊ฒฝ๋ก์ธ ๋งจ ๋ฐ๋ฐ๋ฅapp/photo/[id]/page.tsx์ฐํ์ด์ง๋ฅผ ์ด์ด์ ๋ณด์ฌ์ค๋ค. -
์ํฉ 2: ์ฌ์ฉ์๊ฐ ์ฌ์ดํธ ๋ด์์ ํผ๋(feed) ์ธ๋ค์ผ์ ๋ง์ฐ์ค๋ก ๋ถ๋๋ฝ๊ฒ ํด๋ฆญํ ๋ (Soft Navigation)
ํผ๋(feed/page.tsx) ์์์<Link href="/photo/1">ํด๋ฆญ!
๊ฒฝ๋ก๊ฐ ์ด๋ํ๋ ค๋๋ฐ, ์!feedํด๋ ๋ด๋ถ์ ์ ๋ณต ๊ฒฝ์ฐฐ(..)photo๊ฐ "์ก์๋ค ์๋!" ํ๊ณ ๋์์ฑ๋ค.
๋์์ฑ ์ด ๊ฐ์ง ๋ชจ๋ฌ ๊ป๋ฐ๊ธฐ๋ ์๊น ๊ณจ๋ฑ ์ด๋ก ๋ซ์ด๋@modal๋ณ๋ ฌ ์ฌ๋กฏ ์์น์(์๋น ๋ ์ด์์ ์์๋ฆฌ์ ๋ผ์ด์) ๋ฟ ํ๊ณ ํ์ด๋์จ๋ค!
๊ฒฐ๊ณผ? URL ์ฐฝ์ /photo/1 ์ด๋ผ๊ณ ์์๊ฒ ์ฐํ์์ง๋ง, ํ๋ฉด์ ์๋ ์๋ feed ๋ชฉ๋ก ์์ ๋ชจ๋ฌ(@modal)์ด ๋ ์๋ ๊ถ๊ทน์ ์ํคํ
์ฒ๊ฐ ์์ฑ๋๋ค. ์ฌ์ฉ์๊ฐ ํฐ ๋ค๋ก ๊ฐ๊ธฐ๋ฅผ ๋๋ฅด๋ฉด ๋ชจ๋ฌ๋ง ๊บผ์ง๊ณ ๋ค์ ๊ทผ์์ง(ํผ๋ URL)๋ก ์ ํ๊ทํ๋ค.
๐ layout.tsx ํฉ์ฒด ์ฝ๋
// app/feed/layout.tsx
export default function FeedLayout({
children,
modal // @modal ํด๋ ์์ฒด๊ฐ Props๋ก ๊ฝํ!
}: { children: React.ReactNode, modal: React.ReactNode }) {
return (
<>
{children} {/* ํผ๋ ๋ชฉ๋ก (๋ฐฐ๊ฒฝ์ ์์ํ ๊น๋ ค์์) */}
{modal} {/* ๊ฐ๋ก์ฑ๊ธฐ ๋ ์ด๋. ํ์์ ๋น์ด์๋ค๊ฐ ๋๋ฅด๋ฉด ์ด ์๋ฆฌ์ ํ์
์ด ๋! */}
</>
)
}๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F๋ก ๊ฒ์ํด๋ด.
โ ๋ชจ๋ฌ ํด๋ ์ธํ ์ ๋ค ํ๋๋ฐ๋ ๋น๋๋ ํ์ด์ง ์ด๋ ์ ํฐ์ง๋๋ค (404 Error)
์์ธ: Parallel Route (@ํด๋)๋ฅผ ์ฌ์ด๋๊ณ , ํ์์ ๋ ๋๋ง์ ํ์ํ default.tsx ๋ฅผ ๋ฏธ๋ฐฐ์นํจ.
ํด๊ฒฐ์ฑ
: Next.js ๋ณ๋ ฌ ๋ผ์ฐํธ๋ "ํ๋ฉด์ ๋์์ค URL ๋์์ด ๋ช
ํํ์ง ์์ ๋" ํ์ํ ๋น ๊นกํต ์คํฌ๋ฆฐ ํ์ผ์ ๊ฐ์ํ๋ค. ์ปดํฌ๋ํธ๋ช
default.tsx ๋ฅผ ๋ง๋ค๊ณ ์ฟจํ๊ฒ return null; ํ๋๋ง ์ ์ด์ ๋ฐํ์ํค๋ฉด ๋ง๋ฒ์ฒ๋ผ ์ถฉ๋์ด ์ฌ๋ผ์ง๋ค.
โ (..) ๊ฒฝ๋ก๋ฅผ ํ ๋ฒ ์ผ๋๋ฐ ๊ฐ๋ก์ฑ๊ธฐ๊ฐ ์๋์ ์ ํ๊ณ ๋ณธ ํ์ด์ง๋ก ์๊พธ ํ๊น๋๋ค.
์์ธ: (..) ์งฌ์งฌ ํด๋์ ๊น์ด ๋งค์นญ ์ค๋ฅ.
์๋ฅผ ๋ค์ด ๊ฐ๋ก์ฑ๋ ค๋ ๋์์ด app/movies/[id] ์ธ๋ฐ, ์ ๋ณต ๊ฒฝ์ฐฐ ์์น๊ฐ app/feed/@modal/(..)movies ๋ผ ์น์.
๊ฒฝ์ฐฐ์ "๋ด ๋ถ๋ชจ(feed)๋ ๊ฐ์ ๊ธ์ ๋ฐฉ(movies)" ๋ง ๋์์ฑ ์ ์๋ค! ๋ง์ฝ ๊น์ด๊ฐ ํ๋๋ผ๋ ์ด๊ธ๋๋ค๋ฉด ๊ฐ๋ก์ฑ๊ธฐ๋ ๋ฐ๋ ์กฐ๊ฑด์ ์ถฉ์กฑํ์ง ๋ชปํด ์ ์งํ ๋งํฌ๋ก ํ๊ฒจ๋ฒ๋ฆฐ๋ค. (๊น์ด๊ฐ ํท๊ฐ๋ฆฌ๋ฉด ํด๋ ํธ๋ฆฌ๋ฅผ ์ข
์ด์ ๊ทธ๋ ค๋ณด๋ ๊ฑธ ์์น์ผ๋ก ํ๋ผ!)
๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
| ๊ธฐ๋ฅ ๊ตฌ๋ถ | ์ฃผ์ ์ญํ ์ค๋ช | ์ค๋ฌด ์ฌ์ฉ ๋น๋ & ์์ |
|---|---|---|
@folder (Parallel) | ํ ํญ ๋ฌธ์ ํ๋ฉด์ ์ฌ๋ฌ ๊ฐ์ "๋ ๋ฆฝ ์์กด ๊ตฌ์ญ"์ผ๋ก ์นผ๋ก ์ชผ๊ฐฌ | ๋์๋ณด๋ 3๋ถํ ๋ทฐ, ๋ก๊ทธ์ธ ๋ค์ด์ผ๋ก๊ทธ ๊ตฌ์ญ ํ๋ณด (๋งค์ฐ ๋์) |
(..) (Intercept) | ์ด๋ํ๋ ค๋ ๋ชฉ์ ์ง์ URL๊ณผ ๋ด์ฉ๋ฌผ์ ๋์์ฑ "ํ์ฌ ์๋ ๋ทฐํฌํธ์ ๋ฎ์ด์" | ์ธ์คํ ์ฌ์ง ํผ๋ ํ๋, ํํฐ๋ ์คํธ ์นด๋ ํด๋ฆญ ์ (๋์) |
| ์ ๋ ๊ฐ ์กฐํฉ ๋ง์ | URL ์ผ์น, ๋ธ๋ผ์ฐ์ ๋ค๋ก ๊ฐ๊ธฐ ๋์, ์ง์ ์ ์(๊ณต์ ) ์ฒ๋ฆฌ๊น์ง ์๋ฒฝ ์ปค๋ฒํ๋ ๋ผ์ฐํ ๋ ๋ฒจ์ ๋ชจ๋ฌ ํ๋ช | ํ๋ก๋ํธ ์ต๊ณ ๋๋ UX ํ์ (Next.js๋ฅผ ๋์ ํ๋ ์ด์ ๊ทธ ์์ฒด!) |
๐ก ํ ์ค๋ก ๊ธฐ์ตํ๊ธฐ
ํ์ ๋ชจ๋ฌ์ ๋ง๋ค ๋useState(false)Boolean ์ค์์น์๋ง ์์กดํ๋ ํ์ ํ ์์ ์ ๋๋ฌ๋ค! ๊ฒฝ๋ก(URL)๋ฅผ ๊ฐ๋ก์ฑ๊ณ ((..)), ๊ทธ ๊ฐ๋ก์ฑ ํ๋ฌผ์ ๋ด ์์ ๋ณ๋ ฌ ๋ถํ(@)๋ก ์ค์ ๋ฐ์ผ๋ฉด ์ง์ง ์ฑ ๊ฐ์ ๋ค์ดํฐ๋ธ ํ์ ๋ผ์ฐํ ๋ชจ๋ฌ์ด ํ์ํ๋ค!
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
๋ฐฐ์ ์ผ๋ฉด ํ ๋ฒ ๋นํ์ด์ ํ์ธํด๋ด์ผ ํด.
Q1. ์์ฒ ์ด๊ฐ ๋ณ๋ ฌ ํด๋๋ฅผ ์ด์ฉํด app/layout.tsx์ @chat ํด๋(์ฐ์ธก ๊ณ ์ ์ฑํ
์ฐฝ ์ฌ๋กฏ)๋ฅผ ๋ง๋ค์๋ค. ๋ฉ์ธ ํผ๋์ธ /feed/page.tsx ๋ด๋ถ์์ ๋งํฌ๋ฅผ ํด๋ฆญํด /user/setting ํ์ด์ง๋ก ์ด๋ํ๋ค. ์ด๋ ์ฐ์ธก์ ๊ณ ์ ์ฑํ
์ฐฝ ์ฌ๋กฏ(@chat)์ ์ด๋ป๊ฒ ๋ ๊น? (์ฑํ
์ฐฝ ๋ด๋ถ URL ๋งคํ์ด ์์ ๊ฒฝ์ฐ)
โ ์ ๋ต: ์ํํธ ๋ด๋น๊ฒ์ด์ (Link ํด๋ฆญ) ์ ๋ณ๋ ฌ ๋ผ์ฐํธ๋ ๊ธฐ์กด ํ๋ฉด(์ํ)์ ๊ธฐ์ตํด ์ ์ง๋๋ค.
๐ก ์์ธ ํด์ค: ์ํํธ ๋ด๋น๊ฒ์ด์
(Link ํด๋ฆญ)์ผ๋ก ํ์ด์ง ์ด๋ ๋ฐ์ ์, ๋ณ๋ ฌ ๋ผ์ฐํธ๋ URL ์ฐฝ์ด ๋ฐ๋๋๋ผ๋ ๊ธฐ์กด ํ๋ฉด(์ํ)์ ๊ธฐ์ตํด ์ ์ง(์ ์ค ๋ฐฉ์ด) ํ๋ค. (์ฆ, ์ฑํ
๋ํ ํ์คํ ๋ฆฌ์ ์
๋ ฅ์ฐฝ์ ์ ํ ๋๊ธฐ์ง ์๊ณ ๊ทธ๋๋ก ๋จ์์๋ค.) ๋จ, ์ ์ ๊ฐ ๊ทธ ์ํ์์ ํค๋ณด๋ "F5(์๋ก๊ณ ์นจ)"(Hard Navi)๋ฅผ ๋๋ ค๋ฒ๋ฆฌ๋ฉด Next.js๋ "/user/setting URL ํ๊ฒฝ์ ๋งค์นญ๋๋ @chat ํ์ธ ๋ฅผ ์ฐพ์ ์ ์์"์ด๋ ํ์ ์ ๋ด๋ฆฌ๊ณ ํฐ์ ธ๋ฒ๋ฆฌ๋ฏ๋ก, ์ด ์ถฉ๋์ ๋ฐฉ์ดํ default.tsx๋ฅผ ๋น ๊นกํต์ผ๋ก ํ๋ ์์ฑํด๋ฌ์ผ ์๋ฒฝํด์ง๋ค.
Q2. ์ํธ ๋ฆฌ๋๊ฐ ์ง๋ฌธํ๋ค. "์ธ์คํ๊ทธ๋จ ๊ณต์ ํด๋ฆญ์ ๋๋นํด ๊ฐ๋ก์ฑ๊ธฐ(Intercepting) ๋ผ์ฐํธ๋ฅผ ํด๋ ํธ๋ฆฌ์ ๋ค ์ธํ ํ์ด์. ๊ทธ๋ฐ๋ฐ ์ฐ๋ฆฌ ๋ชจ๋ฐ์ผ ์ฑ์ด๋ ์น์ฌ์ดํธ ํ์ ์์ X (๋ซ๊ธฐ) ๋ฒํผ์ ๋๋ ์ ๋๋, ๋ชจ๋ฌ ์ปดํฌ๋ํธ ๋ด๋ถ์์ ํจ์์ ์ผ๋ก ์ด๋ค ๋ช ๋ น์ด๋ฅผ ๋ ๋ ค์ผ ๋ฐฐ๊ฒฝ ํผ๋๋ฅผ ๋ถ์์ง ์๊ณ ๋ชจ๋ฌ ๊ป๋ฐ๊ธฐ๋ง ์ฐ์ํ๊ฒ ์ฒ ์(๋ซ๊ธฐ)์ํฌ ์ ์์๊น์?"
โ
์ ๋ต: Next.js๊ฐ ์ ๊ณตํ๋ router.back()์ ์ฌ์ฉํ๋ค.
๐ก ์์ธ ํด์ค: ํ์
(๊ฐ๋ก์ฑ ์ฌ๋กฏ) ์ปดํฌ๋ํธ๋ ์ฌ์ค์ ๊ธฐ์กด ํผ๋ ์์ URL ๋ ์ด์ด๊ฐ ํ ๊บผํ ๋ฎ์ธ(Push) ์ํ์ด๋ฏ๋ก, '๋ซ๊ธฐ' ๋ฒํผ์ onClick ์ด๋ฒคํธ ํธ๋ค๋ฌ์ useRouter() ํ
์์ ๊บผ๋ธ ๋ฅ์คํธ ์ ์ฉ const router = useRouter(); router.back() ์ ๋ฌ์์ฃผ๋ ์๊ฐ, ํ์คํ ๋ฆฌ๊ฐ ํ ๊ฐ ๋น ์ง๋ฉด์ ๊ณจ๋ฑ
์ด ์ฌ๋กฏ(@modal)์ ๋งคํ๋ ๋ชจ๋ฌ UI ๋ทฐํฌํธ๋ง ์ฆ๋ฐํ๊ณ ํ์ธต๋ถ ํผ๋๋ ์จ์ ํ ์ ์ง๋๋ ์ต์๊ธ ์ ๋๋ฉ์ด์
๋์์ ์ฐ์ถํ ์ ์๋ค.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ์ ๋ง ๋ง๋ฒ ๊ฐ์ ๋ชจ๋ฌ์ ์ธ๊ณ๋ฅผ ๊ฒฝํํ์ด! ์์ ์ ๋ชจ๋ฌ ํ๋ ๋์ฐ๋ ค๋ฉด URL์ ํฌ๊ธฐํ๊ณ ๊ทธ๋ฅ ์ํ๊ฐ์ผ๋ก๋ง ์ต์ง๋ก ๋น๋ณ๋๋ฐ, ์ด์ ๋ ์ธ์คํ๊ทธ๋จ์ฒ๋ผ URL๋ ๋ฐ๋๋ฉด์ ๋ฐฐ๊ฒฝ์ ์ ์ง๋๋ ๊ณ ๊ธ ๊ธฐ์ ์ ์ธ ์ ์๊ฒ ๋์ด.
๐ก ์ค๋์ ๊ตํ: "URL์ ๋ฐ๊พธ๋ฉด์๋ ๋ฐฐ๊ฒฝ์ ์งํค๊ณ ์ถ๋ค๋ฉด? ํ์ฌ ๋ผ์ฐํธ์์ ๋ชฉ์ ์ง๋ฅผ ๋์์ฑ๊ณ (@ํด๋ + (..)ํด๋), ๊ทธ ์๋ฆฌ์ ๋ชจ๋ฌ์ ์น์."
๋จ์ํ ๊ธฐ๋ฅ์ ๋ง๋๋ ๊ฑธ ๋์ด, ์ด๋ป๊ฒ ํ๋ฉด ์ฌ์ฉ์๊ฐ ๋ ํธ์ํ๊ฒ ์ ๋ณด๋ฅผ ํ์ํ ์ ์์์ง ๋ผ์ฐํ ๋ ๋ฒจ์์ ๊ณ ๋ฏผํ๋ ์๋์ด์ ์์ผ๋ฅผ ์กฐ๊ธ์ ์ฟ๋ณธ ๊ฒ ๊ฐ์. ์ค๋ ๊ธฐ๋ถ ์ต๊ณ ๋ค! ํด๊ทผ๊ธธ์ ๋ง์๋ ์นํจ ํ ๋ง๋ฆฌ ์ฌ ๋ค๊ณ ๊ฐ์ ์ ํ ์ถํ ํํฐ๋ผ๋ ํด์ผ์ง. ๋ด์ผ์ ๋ '์ฌ์ฉ์ ์นํ์ ์ธ' ์์ฒ ์ด๋ก ๋์์ค๊ฒ ์ด! ๐ฃ