웹 프로그래밍

라우팅(Routing)

2025-12-08

라우팅

라우팅(routing)은 웹사이트의 여러 페이지 사이를 이동하는 것으로 각각의 경로를 라우트(route)라고 부르며, 웹사이트에서 링크를 누르면 다른 페이지로 이동하는 과정을 의미합니다.

App Router

Next.js 라우팅은 폴더 구조 기반으로 라우트를 구성하며, 각 라우트가 트리(tree) 형태로 구성되어 있습니다. 그리고 이렇게 트리에 존재하는 각 노드(node)들은 URL 경로의 각 세그먼트에 매핑됩니다.

  • https://www.example.com/dashboard/analytics
    • URL 경로: URL에서 도메인(https://www.example.com) 다음에 나오는 부분(dashboard/analytics)이 되며, 이는 세그먼트들의 조합
    • URL 세그먼트: URL 경로의 일부, 슬래시(/)로 구분(dashboard, analytics)되며 계층구조로 형성

App Router의 주요 특징

  • Next.js >= 13 버전의 권장 라우터
  • app 폴더 기반으로 리액트 (서버) 컴포넌트 기반으로 구성

Remark. 아래 폴더 구조를 참고해서 URL을 예측하고, URL을 보고 폴더 구조를 작성할 수 있어야 함

app/
  page.tsx      # /
  about/
    page.tsx    # /about
  pokemon/
    page.tsx    # /pokemon

App Router - 파일명 규칙

파일 이름 설명 역할
layout 공통 UI 레이아웃
page 개별 UI 페이지
loading 로딩 UI 로딩 화면
not-found 404 UI 404 페이지
error 전역 오류 UI 에러 화면
route 서버 API 엔드포인트 API
template 재사용 가능한 UI 템플릿
default 라우트의 대체 UI 대체 UI

App Router - 컴포넌트 계층 구조

  • 중첩된 세그먼트는 상위 세그먼트 컴포넌트 내에 중첩
  • 각 레벨별로 에러 처리, 로딩 상태 관리 가능
  • app/layout.tsx 파일에 레이아웃 컴포넌트를 적용하면 모든 페이지에 적용
<Layout>
  <Template>
    <ErrorBoundary fallback={<Error />}>
      <Suspense fallback={<Loading />}>
        <ErrorBoundary fallback={<NotFound />}>
          <Page />
        </ErrorBoundary>
      </Suspense>
    </ErrorBoundary>
  </Template>
</Layout>

App Router 예제

// app/about/page.tsx
export default function About() {
  return (
    <div>
      <h1>About 페이지</h1>
      <p>이 페이지는 Next.js의 정적 페이지 예시입니다.</p>
    </div>
  );
}

Dynamic Segments

웹 개발에서는 /pokemon/1, /pokemon/2와 같이 동적으로 바뀌는 경로(파라미터)가 흔히 필요합니다. Next.js는 이러한 동적 라우트를 Dynamic Segment(동적 세그먼트)라는 개념으로 쉽게 처리할 수 있게 해줍니다.

동적 세그먼트는 폴더명이나 파일명을 [param] (대괄호)로 지정합니다. 대괄호 안의 이름은 곧 파라미터 이름이자 props로 전달되는 키(key) 이름입니다.

동적 세그먼트 기본 구조

// app/pokemon/[pokemonId]/page.tsx

interface PokemonPageProps {
  params: { pokemonId: string };
}

const PokemonPage = ({ params }: PokemonPageProps) => {
  return <div>Pokemon ID: {params.pokemonId}</div>;
};

export default PokemonPage;

동적 세그먼트 작동 방식

실제로 /pokemon/1에 접속하면 { pokemonId: '1' } 이 props로 들어오고, /pokemon/2로 접속하면 { pokemonId: '2' }가 들어오게 됩니다.

  • app/pokemon/[pokemonId]/page.tsx \(\to\) /pokemon/1 \(\to\) { pokemonId: '1' }
  • app/pokemon/[pokemonId]/page.tsx \(\to\) /pokemon/2 \(\to\) { pokemonId: '2' }

한정된 동적 세그먼트

(만약) 동적 라우트가 한정된 값만 가질 경우(블로그 글 1~10개 등)에는 generateStaticParams 함수를 활용해, 빌드 시점에 모든 정적 경로를 미리 만들어둘 수 있습니다. 아래와 같은 코드는 포켓몬 상세 페이지가 미리 정적으로 생성되어, 데이터베이스에 따로 접속하지 않고도 빠르게 화면을 보여줄 수 있습니다.

// app/pokemon/[pokemonId]/page.tsx
export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/pokemons');
  const pokemons = await res.json();
  return pokemons.map((pokemon) => ({
    pokemonId: pokemon.id,
  }));
}

동적 세그먼트 확장(a)

동적 세그먼트는 [...param]처럼 대괄호 안에 ...(점 세 개)를 붙이면, 해당 부분 뒤의 모든 세그먼트를 배열로 받아 처리할 수 있습니다.

  • app/pokemon/[...info]/page.tsx \(\to\) /pokemon/1 \(\to\) { info: ['1'] }
  • app/pokemon/[...info]/page.tsx \(\to\) /pokemon/1/overview \(\to\) { info: ['1', 'overview'] }
  • app/pokemon/[...info]/page.tsx \(\to\) /pokemon/1/overview/detail \(\to\) { info: ['1','overview','detail'] }

동적 세그먼트 확장(b)

이중 대괄호([[...param]])를 쓰면, 해당 파라미터가 없어도(=생략되어도) 매칭됩니다. 아래는 이중 대괄호를 사용한 예시입니다.

  • app/pokemon/[[...info]]/page.tsx \(\to\) /pokemon \(\to\) {}
  • app/pokemon/[[...info]]/page.tsx \(\to\) /pokemon/1 \(\to\) { info: ['1'] }
  • app/pokemon/[[...info]]/page.tsx \(\to\) /pokemon/1/overview \(\to\) { info: ['1', 'overview'] }
  • app/pokemon/[[...info]]/page.tsx \(\to\) /pokemon/1/overview/detail \(\to\) { info: ['1','overview','detail'] }

타입 정의

타입스크립트라면, 아래처럼 각 라우트 세그먼트별로 params의 타입을 명확히 정의해두면 좋습니다.

// app/pokemon/[pokemonId]/page.tsx

interface PageProps {
  params: {
    pokemonId: string;
  };
}

const Page = ({ params }: PageProps) => {
  return <div>Pokemon ID: {params.pokemonId}</div>;
};

export default Page;
  • app/pokemon/[pokemonId]/page.tsx \(\to\) { pokemonId: string }
  • app/pokemon/[...info]/page.tsx \(\to\) { info: string[] }
  • app/pokemon/[[...info]]/page.tsx \(\to\) { info?: string[] }
  • app/pokemon/[type]/[pokemonId]/page.tsx \(\to\) { type: string; pokemonId: string }

Dynamic Routing 예제(a)

// app/pokemon/[pokemonId]/page.tsx

interface PageProps {
  params: {
    pokemonId: string;
  };
}

export default function PokemonDetailPage({ params }: PageProps) {
  // params.pokemonId로 동적으로 값 받기
  return (
    <div>
      <h1>포켓몬 상세보기</h1>
      <p>포켓몬 ID: {params.pokemonId}</p>
    </div>
  );
}

Dynamic Routing 예제(b)

여러 값을 다이나믹하게 받으려면 [...info]를 활용합니다.

// app/pokemon/[...info]/page.tsx

interface PageProps {
  params: {
    info: string[];
  };
}

export default function PokemonInfoPage({ params }: PageProps) {
  // params.info: 경로의 세그먼트들이 배열로 들어옴
  return (
    <div>
      <h1>포켓몬 정보</h1>
      <ul>
        {params.info.map((segment, idx) => (
          <li key={idx}>{segment}</li>
        ))}
      </ul>
    </div>
  );
}

Dynamic Routing 예제(c)

// app/pokemon/[[...info]]/page.tsx

interface PageProps {
  params: {
    info?: string[];
  };
}

export default function PokemonOptionalInfoPage({ params }: PageProps) {
  return (
    <div>
      <h1>포켓몬 선택 정보</h1>
      {params.info ? (
        <ul>
          {params.info.map((segment, idx) => (
            <li key={idx}>{segment}</li>
          ))}
        </ul>
      ) : (
        <p>파라미터가 없습니다.</p>
      )}
    </div>
  );
}

라우트 이동이 필요하다면 다음과 같이 useRouter 훅을 사용할 수 있습니다. 하지만 라우터로 이동시킬 경우, 웹사이트에서 버튼/링크 위에 마우스를 올려도 목적지 경로가 보이지 않고, 새 탭 열기(macOS command+클릭 등)도 불가능합니다. 따라서, 대부분의 경우 <Link> 컴포넌트를 사용하는 것이 권장됩니다.

'use client';
import { useRouter } from 'next/navigation';
export default function Page() {
  const router = useRouter();
  return (
    <button
      type="button"
      onClick={() => router.push('/home')}
    >
      Home
    </button>
  );
}
import Link from 'next/link';
export default function Page() {
  return (
    <Link href="/home">
      <button type="button">
        Home
      </button>
    </Link>
  );
}
import Link from 'next/link';

export default function LinkExample() {
  return (
    <div>
      <h2>Next.js Link 예제</h2>
      <ul>
        <li>
          <Link href="/about">About 페이지로 이동</Link>
        </li>
        <li>
          <Link href="/pokemon">
            <button type="button">
              포켓몬 목록으로 이동 (버튼 모양)
            </button>
          </Link>
        </li>
        <li>
          <Link href="https://nextjs.org" target="_blank" rel="noopener noreferrer">
            공식 Next.js 사이트 (새 탭)
          </Link>
        </li>
        <li>
          <Link href="/docs" prefetch={false}>
            프리페치 해제 예시
          </Link>
        </li>
      </ul>
    </div>
  );
}

// prefetch 옵션은 페이지를 프리페치(prefetch)할지 여부를 결정합니다. 프리페치는 페이지를 미리 로드하는 것을 의미합니다.

redirect

서버 컴포넌트에서 조건에 따라 다른 경로로 리디렉션(redirect)하고 싶을 때는 redirect 함수를 사용하면 됩니다.

import { redirect, RedirectType } from 'next/navigation';
import { getSession } from '@/lib/authManager';
export default async function Page() {
  const session = await getSession();
  if (!session) {
    redirect('/', RedirectType.replace);
  }
}

redirect 사용시 주의 사항

redirect 함수 정보는 다음과 같습니다.

  • 기본적으로 307(Temporary Redirect) HTTP 상태코드를 반환
  • POST 결과로 성공 페이지로 이동할 땐 303(see other) 코드를 반환
  • 내부적으로 error를 throw하기 때문에, 반드시 try/catch 밖에서 호출해야 정상 리디렉션됨, try/catch로 감싸면 안 됨
// 잘못된 예시
try {
  redirect('/');
} catch (e) {
  // ...
}

redirect 제한 사항

  • 클라이언트 컴포넌트는 렌더링 과정에서 호출할 수 있지만, 이벤트 핸들러 내에서는 사용할 수 없음(이럴 때는 useRouter 훅을 사용)
  • redirect는 절대경로(URL)도 지원하므로 외부 페이지로 이동할 수도 있음
  • 렌더링 전에 미리 리디렉션이 필요하다면 next.config 파일이나 미들웨어(middleware)를 이용해야 함

참고문헌