# [Next.js + TS] 4. 라우팅과 레이아웃

[Next.js + TS] 4. 라우팅과 레이아웃

3편에서 홈에 샘플 카드를 붙였습니다.
이번 편은 URL로 화면을 나누고, 공통 내비는 layout에 둡니다.
목표: /memos 목록 · /memos/[id] 상세 · 상단 링크.


이 편에서 할 일

  1. 샘플 데이터를 공용 파일로 분리하기
  2. /memos 목록 페이지 만들기
  3. /memos/[id] 상세 페이지 만들기
  4. MemoCard에 상세 링크 달기
  5. 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/1params.id === "1"
  • /memos/2params.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. Linkhref="#"
→ 반드시 /memos/${memo.id}처럼 실제 경로를 넣습니다.


이 편에서 가져갈 것

  • App Router는 폴더로 URL을 만든다
  • [id]는 동적 세그먼트다
  • 목록/상세가 같은 데이터를 쓰면 lib로 분리한다
  • 페이지 이동은 Link
  • 모든 페이지 공통 UI는 layout.tsx

다음 편 예고

[Next.js + TS] 5. Server Component vs Client Component

  • 기본이 서버 컴포넌트인 이유
  • 언제 'use client'가 필요한지
  • 클릭 같은 인터랙션은 어디에 두는지

지금은 “읽기 전용 페이지” 중심입니다.
다음 편에서 서버/클라이언트 경계를 나눕니다.


시리즈 목차

  1. 왜 Next.js + TypeScript인가
  2. 프로젝트 만들고 폴더 구조 이해하기
  3. TypeScript로 컴포넌트 타입 잡기
  4. 라우팅과 레이아웃 ← 지금
  5. Server Component vs Client Component
  6. 데이터 가져오기
  7. 폼과 서버 액션 입문
  8. 정리 + 다음 학습 로드맵