[Next.js + TS] 4. 라우팅과 레이아웃
3편에서 홈에 샘플 카드를 붙였습니다.
이번 편은 URL로 화면을 나누고, 공통 내비는layout에 둡니다.
목표:/memos목록 ·/memos/[id]상세 · 상단 링크.
이 편에서 할 일
- 샘플 데이터를 공용 파일로 분리하기
/memos목록 페이지 만들기/memos/[id]상세 페이지 만들기MemoCard에 상세 링크 달기layout.tsx에 간단한 내비 넣기
App Router 핵심은 다시 이겁니다.
폴더 = URL
page.tsx= 화면layout.tsx= 공통 틀
왜 홈에만 두면 부족한가
지금은 / 하나에 목록이 다 있습니다.
실제 앱에서는 보통 이렇게 나눕니다.
| URL | 역할 |
|---|---|
/ |
소개/홈 |
/memos |
메모 목록 |
/memos/1 |
id가 1인 메모 상세 |
React Router로 직접 구성하던 일을, Next에서는 폴더를 만드는 것으로 해결합니다.
1) 샘플 데이터 공용화
목록과 상세가 같은 데이터를 봐야 합니다.app/page.tsx 안에만 두면 상세에서 못 씁니다.
경로: lib/memos.ts
import type { Memo } from "@/types/memo";
export const memos: Memo[] = [
{
id: "1",
title: "Next.js 시작",
content: "App Router로 프로젝트 만들었다.",
createdAt: "2026-07-29",
},
{
id: "2",
title: "TypeScript props",
content: "MemoCard에 타입을 붙였다.",
createdAt: "2026-07-29",
},
];
export function getMemoById(id: string): Memo | undefined {
return memos.find((memo) => memo.id === id);
}
memos: 전체 목록getMemoById: id로 하나 찾기
아직 DB/API는 없습니다.
라우팅 연습용 가짜 데이터로 충분합니다. (6편에서 fetch로 바꿉니다.)
2) 목록 페이지: /memos
폴더와 파일을 만듭니다.
app/
memos/
page.tsx
app/memos/page.tsx:
import MemoCard from "@/components/MemoCard";
import { memos } from "@/lib/memos";
export default function MemosPage() {
return (
<main>
<h1>메모 목록</h1>
<section>
{memos.map((memo) => (
<MemoCard key={memo.id} memo={memo} />
))}
</section>
</main>
);
}
브라우저에서 http://localhost:3000/memos 를 열면 목록이 보여야 합니다.
memos라는 폴더 이름이 곧 URL 경로입니다.
3) 상세 페이지: /memos/[id]
동적 구간은 대괄호 폴더로 만듭니다.
app/
memos/
page.tsx
[id]/
page.tsx
/memos/1→params.id === "1"/memos/2→params.id === "2"
app/memos/[id]/page.tsx:
import { notFound } from "next/navigation";
import { getMemoById } from "@/lib/memos";
type MemoDetailPageProps = {
params: Promise<{ id: string }>;
};
export default async function MemoDetailPage({ params }: MemoDetailPageProps) {
const { id } = await params;
const memo = getMemoById(id);
if (!memo) {
notFound();
}
return (
<main>
<h1>{memo.title}</h1>
<p>{memo.content}</p>
<time dateTime={memo.createdAt}>{memo.createdAt}</time>
</main>
);
}
포인트:
| 코드 | 의미 |
|---|---|
[id] |
URL의 가변 구간 |
params |
그 구간 값이 들어옴 |
await params |
최근 Next에서는 params가 Promise인 경우가 많음 |
notFound() |
없는 id면 404 |
http://localhost:3000/memos/1 이 열리고,/memos/999 는 404가 나면 성공입니다.
버전마다
params가 동기 객체인 템플릿도 있습니다.
타입이Promise라고 나오면await하면 되고, 아니면params.id를 그대로 쓰면 됩니다.
4) 카드에서 상세로 이동: Link
<a href>도 되지만, Next에서는 Link를 씁니다.
클라이언트 쪽 이동을 더 부드럽게 처리합니다.
components/MemoCard.tsx를 수정합니다.
import Link from "next/link";
import type { Memo } from "@/types/memo";
type MemoCardProps = {
memo: Memo;
};
export default function MemoCard({ memo }: MemoCardProps) {
return (
<article>
<h2>
<Link href={`/memos/${memo.id}`}>{memo.title}</Link>
</h2>
<p>{memo.content}</p>
<time dateTime={memo.createdAt}>{memo.createdAt}</time>
</article>
);
}
목록에서 제목을 누르면 상세로 가야 합니다.
5) 홈은 소개 페이지로
목록은 /memos로 옮겼으니, 홈은 가볍게 둡니다.
app/page.tsx:
import Link from "next/link";
export default function HomePage() {
return (
<main>
<h1>Memo App</h1>
<p>Next.js + TypeScript 시리즈 실습 프로젝트</p>
<p>
<Link href="/memos">메모 목록 보기</Link>
</p>
</main>
);
}
6) 공통 내비: layout.tsx
모든 페이지 위에 링크를 두고 싶다면 app/layout.tsx에 넣습니다.
children 위에 내비만 추가하면 됩니다. (폰트 설정 등은 기존 것 유지)
import Link from "next/link";
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Memo App",
description: "Next.js + TypeScript 실습",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<body>
<header>
<nav>
<Link href="/">홈</Link>
{" | "}
<Link href="/memos">메모</Link>
</nav>
</header>
{children}
</body>
</html>
);
}
이미 Geist 폰트 등이 있다면 내비만 추가해도 됩니다.
중요한 건 위치입니다.
layout
├─ header/nav ← 모든 페이지 공통
└─ children ← page.tsx 내용
/, /memos, /memos/1 어디로 가든 위 링크가 보여야 합니다.
지금 폴더 구조
app/
layout.tsx → 공통 내비
page.tsx → /
memos/
page.tsx → /memos
[id]/
page.tsx → /memos/:id
components/
MemoCard.tsx
lib/
memos.ts
types/
memo.ts
자주 하는 실수
1. app/memos.tsx 파일만 만듦
→ 목록은 app/memos/page.tsx 여야 /memos가 됩니다.
2. [id]를 일반 폴더명 id로 만듦
→ 대괄호가 있어야 동적 라우트입니다.
3. 상세에서 목록 데이터를 또 하드코딩
→ lib/memos.ts로 한곳에서 관리하세요.
4. Link에 href="#"
→ 반드시 /memos/${memo.id}처럼 실제 경로를 넣습니다.
이 편에서 가져갈 것
- App Router는 폴더로 URL을 만든다
[id]는 동적 세그먼트다- 목록/상세가 같은 데이터를 쓰면
lib로 분리한다 - 페이지 이동은
Link - 모든 페이지 공통 UI는
layout.tsx
다음 편 예고
[Next.js + TS] 5. Server Component vs Client Component
- 기본이 서버 컴포넌트인 이유
- 언제
'use client'가 필요한지 - 클릭 같은 인터랙션은 어디에 두는지
지금은 “읽기 전용 페이지” 중심입니다.
다음 편에서 서버/클라이언트 경계를 나눕니다.
시리즈 목차
- 왜 Next.js + TypeScript인가
- 프로젝트 만들고 폴더 구조 이해하기
- TypeScript로 컴포넌트 타입 잡기
- 라우팅과 레이아웃 ← 지금
- Server Component vs Client Component
- 데이터 가져오기
- 폼과 서버 액션 입문
- 정리 + 다음 학습 로드맵
'frontend > NextJs' 카테고리의 다른 글
| # [Next.js + TS] 6. 데이터 가져오기 (0) | 2026.07.29 |
|---|---|
| # [Next.js + TS] 5. Server Component vs Client Component (0) | 2026.07.29 |
| # [Next.js + TS] 3. TypeScript로 컴포넌트 타입 잡기 (0) | 2026.07.29 |
| # [Next.js + TS] 2. 프로젝트 만들고 폴더 구조 이해하기 (0) | 2026.07.29 |
| [Next.js + TS] 1. 왜 Next.js + TypeScript인가 (0) | 2026.07.29 |
