Intercepting Routes๋ ์ฌ์ฉ์์ ์ ๊ทผ ๋ฐฉ๋ฒ์ ๋ฐ๋ผ ๋ผ์ฐํ ์ ์ ์ดํ ์ ์๋ ๊ธฐ๋ฅ์ ๋๋ค.
์ด๋ฅผ ํตํด ๋ค์ํ ์๋๋ฆฌ์ค์์ ๋ผ์ฐํ ๋์์ ์ ์ฐํ๊ฒ ๊ด๋ฆฌํ ์ ์์ผ๋ฉฐ, ๊ฐ์ฅ ํํ ํ์ฉ ์ฌ๋ก๋ก๋ ์ธ์ฆ, ๊ถํ ๋ถ์ฌ, A/B ํ ์คํธ ๋ฑ์ด ์์ต๋๋ค.
์๋ฅผ ๋ค์ด, ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ์ง ์์ ์ํ๋ก ํน์ ํ์ด์ง์ ์ ๊ทผํ๋ ค ํ ๋ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋๋ ์ ํ๊ฑฐ๋, ํน์ ์กฐ๊ฑด์ ๋ฐ๋ผ ์ฌ์ฉ์์๊ฒ ๋ค๋ฅธ ๋ฒ์ ์ ํ์ด์ง๋ฅผ ์ ๊ณตํ๋ A/B ํ ์คํธ๋ฅผ ๊ตฌํํ ์ ์์ต๋๋ค.Next.js์์๋ ์ด ๊ธฐ๋ฅ์ middleware๋ฅผ ํตํด ๊ตฌํํ ์ ์์ต๋๋ค.
Next.js์ middleware๋ ์๋ฒ ์ฌ์ด๋์์ ์์ฒญ์ ๊ฐ๋ก์ฑ๊ณ , ์์ฒญ์ ์์ ํ๊ฑฐ๋ ์กฐ๊ฑด์ ๋ฐ๋ผ ๋ค๋ฅธ ํ์ด์ง๋ก ๋ฆฌ๋๋ ์ ํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
์ด๋ฅผ ํ์ฉํ์ฌ ์ฌ์ฉ์๊ฐ ์์ฒญํ ๊ฒฝ๋ก์ ๋ํด ํน์ ๋ก์ง์ ์ฒ๋ฆฌํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ ์ ํ ํ์ด์ง๋ฅผ ์ ๊ณตํ ์ ์์ต๋๋ค.
์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค๋ฅธ ๋ถ๋ถ์ ์๋ ๋ผ์ฐํธ๋ฅผ ํ์ฌ ๋ ์ด์์ ์์์ ๋ก๋ํด์,
์ฌ์ฉ์๊ฐ ๋ค๋ฅธ ์ปจํ ์คํธ๋ก ์ด๋ํ์ง ์๊ณ ๋ ๊ทธ ๋ผ์ฐํธ ๋ด์ฉ์ ๋ณผ ์ ์๊ฒ ํด ์ค๋ค.
๋ํ ์์๊ฐ ํผ๋ โ ์ฌ์ง ๋ชจ๋ฌ์ด๋ค.
/feed์์ ์ฌ์ง์ ํด๋ฆญํ๋ฉด, Next.js๊ฐ ์ค์ ๋ก๋ /photo/123 ๋ผ์ฐํธ๋ฅผ ์ฝ์ด์ค์ง๋ง,/feed ์ปจํ
์คํธ๋ฅผ ์ ์งํ๋ฉด์ /feed ์์ ์ฌ์ง ์์ธ ๋ชจ๋ฌ์ ๋์ฐ๋ ์์ผ๋ก ๋์ํ๋ค.๋ฐ๋๋ก
/photo/123์ ์
๋ ฅํ๊ฑฐ๋,/photo/123 ๋งํฌ๋ฅผ ๋ฐ๋ก ์ด๊ฑฐ๋,์ด๋ฒ์๋ ๋ชจ๋ฌ์ด ์๋๋ผ /photo/123 ์ ์ฒด ํ์ด์ง๊ฐ ๋ ๋๋ง๋๊ณ , ์ด๋๋ ๊ฐ๋ก์ฑ๊ธฐ(interception)๊ฐ ์ผ์ด๋์ง ์๋๋ค.
์ฆ, Interception Routes์ ๊ฐ๋ ์ ๋ค์ ์ ๋ฆฌํ๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
์ํํธ ๋ค๋น๊ฒ์ด์ (ํด๋ผ์ด์ธํธ ๋ด ์ด๋)์ผ ๋๋ง ๋ค๋ฅธ ๋ทฐ(๋ชจ๋ฌ/์ค๋ฒ๋ ์ด)๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ,
ํ๋ ๋ค๋น๊ฒ์ด์ (์ง์ URL, ์๋ก๊ณ ์นจ)์ผ ๋๋ ์๋ ๋ผ์ฐํธ ์ ์ฒด ํ์ด์ง๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ผ์ฐํ ํจํด
/login, /photo/[id], /products/[id])์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์๋ค./feed์์ ์ฌ์ง ๋ชจ๋ฌ์ ์ด์ด๋ ๋ค์ ํผ๋๊ฐ ์ ์ง๋๋ค./photo/[id], /login ๊ฐ์ ์ค์ ํ์ด์ง๊ฐ ์กด์ฌํ๊ธฐ ๋๋ฌธ์,๋ณธ๊ฒฉ์ ์ผ๋ก Intercepting Routes์ ๋ํด์ ์์๋ณด๊ธฐ ์ ์, ์ธ๊ทธ๋จผํธ(segment)์ ์ฌ๋กฏ(slot)์ ๊ฐ๋ ์ ์ ๋ฆฌํด ๋ณด์.
app ํด๋ ์๋์ ๊ฐ ํด๋๊ฐ ๋ผ์ฐํธ ์ธ๊ทธ๋จผํธ(route segment)์ด๊ณ , ์ด๊ฒ ๊ทธ๋๋ก URL ์ธ๊ทธ๋จผํธ๋ก ๋งคํ๋๋ค.app
โ page.tsx -> URL: /
โ feed
โ โ page.tsx -> URL: /feed
โ photo
โ [id]
โ page.tsx -> URL: /photo/:idfeed ํด๋ โก๏ธ feed ์ธ๊ทธ๋จผํธ โก๏ธ URL /feedphoto ํด๋ โก๏ธ photo ์ธ๊ทธ๋จผํธ โก๏ธ URL /photo[id] ํด๋ โก๏ธ ๋์ ์ธ๊ทธ๋จผํธ โก๏ธ URL /photo/123์์ 123 ๋ถ๋ถ
- ์ธ๊ทธ๋จผํธ = ๊ฒฝ๋ก(URL) ํ ์นธ์ ๋์๋๋ ํด๋ ์ด๋ฆ
- URL
/blog/[slug]๋ ์ธ๊ทธ๋จผํธ 3๊ฐ๋ก ๊ตฌ์ฑ๋จ
/(๋ฃจํธ)blog[slug](๋์ ์ธ๊ทธ๋จผํธ)- ์ฌ๊ธฐ์
app์ ์ธ๊ทธ๋จผํธ๊ฐ ์๋๊ณ ๊ทธ๋ฅ ๋ฃจํธ ๋๋ ํฐ๋ฆฌ์ด๋ค.- ๊ทธ๋์ "feed ์ธ๊ทธ๋จผํธ"๋ผ๊ณ ํ๋ฉด
app/feedํด๋์ URL/feed์กฐํฉ์ ํตํ์ด ๋งํ๋ ๊ฒ์ด๋ค.
@ํด๋๋ช
์ผ๋ก ํด๋๋ฅผ ๋ง๋ค๋ฉด ํ๋์ ์ฌ๋กฏ์ด ๋๊ณ , ๊ฐ์ ๋ ๋ฒจ์ layout.tsx๋ก props์ฒ๋ผ ์ ๋ฌ๋์ด ํจ๊ป ๋ ๋๋ง๋๋ค.app
โ layout.tsx
โ page.tsx // ๊ธฐ๋ณธ children
โ @analytics
โ โ page.tsx // analytics ์ฌ๋กฏ
โ @team
โ page.tsx // team ์ฌ๋กฏ@analytics โก๏ธ analytics ์ฌ๋กฏ@team โก๏ธ team ์ฌ๋กฏapp/layout.tsx๋ ์ด๋ฐ ์์ผ๋ก props๋ฅผ ๋ฐ๋๋ค.// app/layout.tsx
export default function RootLayout({
children,
analytics,
team,
}: {
children: React.ReactNode; // ๊ธฐ๋ณธ ๋ผ์ฐํธ
analytics: React.ReactNode; // @analytics ์ฌ๋กฏ
team: React.ReactNode; // @team ์ฌ๋กฏ
}) {
return (
<html>
<body>
<header>ํค๋</header>
<aside>{analytics}</aside>
<main>{children}</main>
<aside>{team}</aside>
</body>
</html>
);
}@analytics, @team)์ URL์ ์ ์ฐํ๋ค./@team/members๋ ์ค์ ๋ก /members๋ก ์ ๊ทผ๋๋ค.children + @modal ์ฌ๋กฏ)Intercepting Routes๋ ํด๋ ์ด๋ฆ ๊ท์น์ผ๋ก ์ ์ํ๋ค.
(.): ๊ฐ์ ๋ ๋ฒจ์ ์ธ๊ทธ๋จผํธ๋ฅผ ๊ฐ๋ก์ฑ(..): ํ ๋ ๋ฒจ ์์ ์ธ๊ทธ๋จผํธ๋ฅผ ๊ฐ๋ก์ฑ(..)(..): ๋ ๋ ๋ฒจ ์์ ์ธ๊ทธ๋จผํธ๋ฅผ ๊ฐ๋ก์ฑ(...): app ๋ฃจํธ(app) ๊ธฐ์ค์ผ๋ก ์ธ๊ทธ๋จผํธ๋ฅผ ๊ฐ๋ก์ฑ์ด ๊ท์น์ ํ์ผ ์์คํ
๋๋ ํฐ๋ฆฌ ๊ตฌ์กฐ๊ฐ ์๋๋ผ, ๋ผ์ฐํธ ์ธ๊ทธ๋จผํธ ๊ธฐ์ค์ด๋ค.
@modal, @auth ๊ฐ์ Parallel Routes์ slot ํด๋๋ ์ธ๊ทธ๋จผํธ๋ก ์ทจ๊ธ๋์ง ์๋๋ค.
๊ทธ๋์ ๋๋ ํฐ๋ฆฌ ๋ ๋ฒจ์ด ๋ ์นธ ์์ฌ๋, ์ธ๊ทธ๋จผํธ ๊ด์ ์์๋ ํ ๋จ๊ณ ์์ผ ์ ์๋ค.
๋ํ ์ด ๊ธฐ๋ฅ์ App Router(/app)๋ฅผ ์ํ ๋ผ์ฐํ
๋ฌธ์์๋ง ํฌํจ๋์ด ์์ด์, Pages Router(/pages)์์๋ ์ธ ์ ์๋ค.
Intercepting Routes ๋์์ ์ดํดํ ๋, Next.js์ ๋ค๋น๊ฒ์ด์ ํ์ ์ ๊ฐ์ด ๋ณด๋ ๊ฒ ์ข๋ค.
<Link href="/photo/123"> ํด๋ฆญ์ด๋, router.push('/photo/123') ํธ์ถ ๊ฐ์ ํด๋ผ์ด์ธํธ ๋ด ์ด๋์ผ ๋,/photo/123์ผ๋ก ๋ฐ๋ ์๋ ์๊ณ , ํน์ URL์ /feed๋ก ์ ์งํ๋ฉด์ ๋ด๋ถ์ ์ผ๋ก๋ง ๊ฐ๋ก์ฑ ์๋ ์๋ค./photo/123 ๋งํฌ๋ฅผ ํด๋ฆญํ๊ฑฐ๋, ์๋ก๊ณ ์นจ(F5)๊ณผ ๊ฐ์ ํ๋ ๋ค๋น๊ฒ์ด์
์ผ ๋,app/photo/[id]/page.tsx ๋ฑ)๋ฅผ ํต์งธ๋ก ๋ ๋๋งํ๋ค.
- ํผ๋์์ ์นด๋ ํด๋ฆญ โก๏ธ ๋ชจ๋ฌ (Intercepted)
- ์ฃผ์์ฐฝ์์ ์ง์
/photo/123์ ๋ ฅ โก๏ธ ์ ์ฒด ํ์ด์ง (Non-intercepted)
/feed + /photo/[id] ๋ชจ๋ฌapp
โโ feed
โ โโ page.tsx // ํผ๋ ๋ชฉ๋ก ํ์ด์ง
โ โโ (..)photo
โ โโ [id]
โ โโ page.tsx // ์ธํฐ์
ํธ๋ ์ฌ์ง ๋ชจ๋ฌ์ฉ ํ์ด์ง
โโ photo
โโ [id]
โโ page.tsx // ์ ์ฒด ์ฌ์ง ์์ธ ํ์ด์ง app/photo/[id]/page.tsx: ์๋์ ์ ์ฒด ์์ธ ํ์ด์งapp/feed/(..)photo/[id]/page.tsx: /feed ์ปจํ
์คํธ ์์์ ๊ฐ๋ก์ฑ์ ๋ณด์ฌ์ค ๋ฒ์ (๋ณดํต ๋ชจ๋ฌ UI)(..)photo: feed ์ธ๊ทธ๋จผํธ ์์์, ํ ๋จ๊ณ ์(..)๋ก ์ฌ๋ผ๊ฐ์(app), ๊ทธ ๋ถ๋ชจ ์๋์ ์๋ photo ์ธ๊ทธ๋จผํธ๋ฅผ ๊ฐ๋ก์ฑ๋ค.์์ ์ฝ๋
/feed/page.tsx: ์นด๋์์ /photo/[id]๋ก ์ด๋
// app/feed/page.tsx
import Link from "next/link";
const photos = [
{ id: "1", title: "์ฌ์ง 1" },
{ id: "2", title: "์ฌ์ง 2" },
];
export default function FeedPage() {
return (
<main>
<h1>ํผ๋</h1>
<ul>
{photos.map((p) => (
<li key={p.id}>
<Link href={`/photo/${p.id}`}>{p.title}</Link>
</li>
))}
</ul>
</main>
);
}
/photo/[id]/page.tsx: ์ ์ฒด ํ์ด์ง ๋ฒ์
// app/photo/[id]/page.tsx
interface Props {
params: { id: string };
}
export default function PhotoPage({ params }: Props) {
return (
<main>
<h1>์ฌ์ง ์ ์ฒด ํ์ด์ง</h1>
<p>ID: {params.id}</p>
{/* ์ง์ง๋ผ๋ฉด ์ฌ๊ธฐ์ fetch ํด์ ์ด๋ฏธ์ง/๋ฉํ๋ฐ์ดํฐ ๋ณด์ฌ์ค */}
</main>
);
}
/feed/(..)photo/[id]/page.tsx: ํผ๋ ์์ ์น๋ ๋ชจ๋ฌ ๋ฒ์
// app/feed/(..)photo/[id]/page.tsx
"use client";
import { useRouter } from "next/navigation";
interface Props {
params: { id: string };
}
export default function PhotoModalPage({ params }: Props) {
const router = useRouter();
const close = () => {
router.back(); // ๋ค๋ก ๊ฐ๊ธฐ = ๋ชจ๋ฌ ๋ซ๊ธฐ
};
return (
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div style={{ background: "white", padding: 24 }}>
<button onClick={close}>๋ซ๊ธฐ</button>
<h1>์ฌ์ง ๋ชจ๋ฌ (ID: {params.id})</h1>
</div>
</div>
);
}
/photo/[id]๋ก ์ด๋ํ์ง๋งapp/feed/(..)photo/[id]/page.tsx๋ฅผ ์ฌ์ฉํด์ /feed ๋ ์ด์์ ์์ ๋ชจ๋ฌ์ ๋ ๋๋งํ๋ค./photo/[id]๋ก ๋ค์ด๊ฐ๊ฑฐ๋ ์๋ก๊ณ ์นจํ๋ฉด,app/photo/[id]/page.tsx ์ ์ฒด ํ์ด์ง๊ฐ ๋ ๋๋ง๋๋ค.@modal ์ฌ๋กฏ๊ณผ ์กฐํฉํด์ ๋ ๊น๋ํ๊ฒ ๋ง๋๋ ํจํด์ ๋ง์ด ์ด๋ค./login ๋ชจ๋ฌ (Parallel Routes + Intercepting)/login ์ ์ ํ์ด์ง ๋ง๋ค๊ธฐ
// app/login/page.tsx
import { Login } from "@/app/ui/login";
export default function Page() {
return <Login />;
}
@auth ์ฌ๋กฏ + default ๋ง๋ค๊ธฐ
// app/@auth/(.)login/page.tsx
import { Modal } from "@/app/ui/modal";
import { Login } from "@/app/ui/login";
export default function Page() {
return (
<Modal>
<Login />
</Modal>
);
}
@auth/(.)login์ธ ์ ์ด ํต์ฌ์ด๋ค.(.)login์ ๊ฐ์ ๋ ๋ฒจ์ login ์ธ๊ทธ๋จผํธ๋ฅผ ๊ฐ๋ก์ฑ๋ค๋ ๋ป์ด๋ค.@auth๋ ์ฌ๋กฏ(slot)์ด๋ผ ์ธ๊ทธ๋จผํธ ๋ ๋ฒจ ๊ณ์ฐ์ ํฌํจ๋์ง ์๋๋ค.๋ ์ด์์์์ @auth ์ฌ๋กฏ ๋ ๋๋ง
// app/layout.tsx
import Link from "next/link";
export default function RootLayout({
auth,
children,
}: {
auth: React.ReactNode;
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
<nav>
<Link href="/login">๋ก๊ทธ์ธ</Link>
</nav>
{/* ๋ชจ๋ฌ ์ฌ๋กฏ */}
{auth}
{/* ์ค์ ํ์ด์ง */}
{children}
</body>
</html>
);
}
/์์ "๋ก๊ทธ์ธ" ๋ฒํผ์ ํด๋ฆญํ๋ฉด(์ํํธ ๋ค๋น๊ฒ์ด์
),/login์ผ๋ก ์ด๋ + @auth/(.)login/page.tsx๊ฐ ๊ฐ๋ก์ฑ์ <Modal><Login /></Modal> ๋ ๋/login, ํ๋ฉด์ ํ์ฌ ํ์ด์ง + ๋ก๊ทธ์ธ ๋ชจ๋ฌ/login์ ์ฃผ์์ฐฝ์ ์ง์ ์
๋ ฅํ๊ฑฐ๋ ์๋ก๊ณ ์นจํ๋ฉด(ํ๋ ๋ค๋น๊ฒ์ด์
),app/login/page.tsx์ ์ ์ฒด ๋ก๊ทธ์ธ ํ์ด์ง๊ฐ ๋ ๋๋๋ค.router.back()์ ํธ์ถํ๋ฉด,@auth์์ null์ ๋ฐ๋ณตํ๋ ๋ผ์ฐํธ(์: @auth/page.tsx๋ @auth/[...catchAll]/page.tsx)๋ฅผ ๋ง๋ค์ด์ ๋ชจ๋ฌ์ ๋ช
์์ ์ผ๋ก ๋ซ์์ผ ํ๋ค.