라우팅·레이아웃·동적 세그먼트
라우팅·레이아웃·동적 세그먼트
요약: 폴더가 URL이 되는 규칙을 익히고, 동적 주소와 중첩 레이아웃으로 화면 구조를 잡는다.
1. 왜 이 주제가 필요한가
웹 앱은 URL이 곧 정보 구조다. /posts/hello가 글이고 /about이 소개라면, 폴더를 그렇게 나누면 된다.
App Router는 설정 파일에 라우트를 나열하지 않는다. app/ 아래 폴더 이름이 경로가 된다. 레이아웃은 부모 폴더에서 자식으로 중첩된다.
주소와 UI 껍질을 같이 설계하면 페이지마다 헤더를 복사하지 않아도 된다.
2. 한 줄 규칙
page.tsx가 있는 폴더만 공개 URL이다. 대괄호 폴더([slug])는 값이 바뀌는 구간이다.
레이아웃은 하위 이동 시 리마운트되지 않는다. 공통 셸은 layout.tsx에 둔다.
3. 예제
파일과 URL
| 파일 | URL |
|---|---|
app/page.tsx |
/ |
app/posts/page.tsx |
/posts |
app/posts/[slug]/page.tsx |
/posts/hello |
app/docs/[...slug]/page.tsx |
/docs/a, /docs/a/b |
app/shop/[[...slug]]/page.tsx |
/shop와 /shop/a 모두 |
[slug]는 한 칸. [...slug]는 나머지 칸 전부(캐치올). [[...slug]]는 그 칸이 없어도 된다(선택 캐치올).
동적 세그먼트
Next.js 15부터 params는 Promise다. 서버 페이지에서 await로 읽는다.
// app/posts/[slug]/page.tsx
import {notFound} from 'next/navigation';
type Props = {
params: Promise<{slug: string}>;
};
const POSTS: Record<string, string> = {
hello: '첫 글',
world: '둘째 글',
};
export default async function PostPage({params}: Props) {
const {slug} = await params;
const title = POSTS[slug];
if (!title) notFound();
return (
<article>
<h1>{title}</h1>
<p>slug: {slug}</p>
</article>
);
}
빌드 때 HTML을 미리 만들려면 generateStaticParams를 같이 쓴다. 정적 사이트 생성(SSG, 빌드 시 HTML을 고정하는 방식)과 충돌하지 않는다. await params는 Next API일 뿐, 페이지를 클라이언트 렌더로 바꾸지 않는다.
export function generateStaticParams() {
return [{slug: 'hello'}, {slug: 'world'}];
}
중첩 레이아웃
// app/posts/layout.tsx
import type {ReactNode} from 'react';
import Link from 'next/link';
export default function PostsLayout({children}: {children: ReactNode}) {
return (
<div>
<aside>
<nav>
<Link href="/posts/hello/">hello</Link>
<Link href="/posts/world/">world</Link>
</nav>
</aside>
<section>{children}</section>
</div>
);
}
/posts/hello에서 /posts/world로 이동해도 이 레이아웃은 다시 마운트되지 않는다. 사이드바 스크롤·필터 상태를 유지하기 쉽다.
루트 layout.tsx는 <html>/<body>를 가진다. 하위 레이아웃은 그 안의 껍질만 추가한다.
라우트 그룹
폴더 이름을 (marketing)처럼 괄호로 감싸면 URL에 안 들어간다. 레이아웃만 나누고 싶을 때 쓴다.
app/(site)/page.tsx → /
app/(site)/about/page.tsx → /about
app/(manager)/manager/page.tsx → /manager
(site)와 (manager)에 서로 다른 layout.tsx를 둘 수 있다. 주소는 폴더 이름에서 괄호 그룹을 뺀 나머지다.
Link와 이동
import Link from 'next/link';
export function PostLink() {
return <Link href="/posts/hello/">글 보기</Link>;
}
내부 이동은 <a> 대신 next/link의 Link를 쓴다. 미리 코드를 받아 전환이 빠르다.
trailingSlash: true면 href도 /posts/hello/처럼 맞춘다. 슬래시가 어긋나면 같은 페이지가 두 URL로 열린다.
버튼을 눌러 이동할 때는 Client Component에서 라우터를 쓴다.
'use client';
import {useRouter} from 'next/navigation';
export function GoPostsButton() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push('/posts/')}>
목록
</button>
);
}
next/navigation이다. Pages Router의 next/router가 아니다.
병렬·인터셉트
@folder 슬롯은 한 레이아웃에 여러 page를 나란히 넣을 때 쓴다. 대시보드 다중 패널이 전형적인 경우다.
(.)photo 같은 인터셉트 라우트는 목록 위에 모달을 얹고, 새로고침하면 전용 페이지로 열 때 쓴다.
입문 앱은 동적 세그먼트와 중첩 레이아웃만으로 충분한 경우가 많다. 필요할 때 문서에서 추가한다.
4. 흔한 실수
| 실수 | 결과 | 대안 |
|---|---|---|
params.slug를 동기로 읽는다 |
Next 15에서 타입·런타임 경고 | const {slug} = await params |
레이아웃에서 {children}을 뺀다 |
하위 페이지가 안 보인다 | 레이아웃은 껍질만, 본문은 children |
| 라우트 그룹을 URL에 기대한다 | (site)가 주소에 없다 |
괄호는 URL에서 빠진다 |
<a href="/posts">만 쓴다 |
풀 새로고침 | 내부는 Link |
| trailing slash 혼용 | 중복 URL, 깨진 링크 | 설정과 href를 일치 |
정리
라우팅은 폴더 이름이고, 공개 여부는 page.tsx다. 바뀌는 칸은 [slug], 공통 셸은 layout.tsx다.
URL을 먼저 그리고 폴더를 만든다. 레이아웃은 “이 구간에서 무엇이 유지돼야 하는가”로 나눈다.
연습
/posts/[slug]페이지와generateStaticParams두 개(hello,world)를 만든다.(marketing)/(app)그룹으로 공개 사이트와 로그인 후 화면의 레이아웃을 나눈다.- 내부 링크를 모두
Link로 바꾸고,trailingSlash정책과href를 맞춘다.
댓글
0아직 댓글이 없습니다. 첫 댓글을 남겨보세요.