문서
App Router 사용하기
리다이렉트

리다이렉트

Next.js에서 리다이렉트를 처리하는 방법에는 여러 가지가 있습니다. 이 페이지에서는 사용 가능한 각 옵션, 사용 사례, 그리고 많은 수의 리다이렉트를 관리하는 방법에 대해 살펴보겠습니다.

API목적위치상태 코드
redirect데이터 변경 또는 이벤트 후 사용자 리다이렉트서버 컴포넌트, 서버 액션, 라우트 핸들러307 (임시) 또는 303 (서버 액션)
permanentRedirect데이터 변경 또는 이벤트 후 사용자 리다이렉트서버 컴포넌트, 서버 액션, 라우트 핸들러308 (영구)
useRouter클라이언트 측 경로 이동 처리클라이언트 컴포넌트의 이벤트 핸들러N/A
next.config.js에서의 redirects경로를 기반으로 들어오는 요청 리다이렉트next.config.js 파일307 (임시) 또는 308 (영구)
NextResponse.redirect조건에 따라 들어오는 요청 리다이렉트미들웨어모든 상태 코드

redirect 함수

redirect 함수를 사용하면 사용자를 다른 URL로 리다이렉트할 수 있습니다. 서버 컴포넌트, 라우트 핸들러, 그리고 서버 액션에서 redirect를 호출할 수 있습니다.

redirect는 주로 데이터 변경이나 이벤트 후에 사용됩니다. 예를 들어, 게시물을 생성한 후:

app/actions.tsx
"use server";
 
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
 
export async function createPost(id: string) {
  try {
    // 데이터베이스 호출
  } catch (error) {
    // 오류 처리
  }
 
  revalidatePath("/posts"); // 캐시된 게시물 업데이트
  redirect(`/post/${id}`); // 새 게시물 페이지로 이동
}
app/actions.js
"use server";
 
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
 
export async function createPost(id) {
  try {
    // 데이터베이스 호출
  } catch (error) {
    // 오류 처리
  }
 
  revalidatePath("/posts"); // 캐시된 게시물 업데이트
  redirect(`/post/${id}`); // 새 게시물 페이지로 이동
}

알아두면 좋은 점:

  • redirect는 기본적으로 307 (임시 리다이렉트) 상태 코드를 반환합니다. 서버 액션에서 사용될 때는 303 (See Other)을 반환하며, 이는 일반적으로 POST 요청의 결과로 성공 페이지로 리다이렉트할 때 사용됩니다.
  • redirect는 내부적으로 오류를 발생시키므로 try/catch 블록 외부에서 호출해야 합니다.
  • redirect는 렌더링 과정 중에 클라이언트 컴포넌트에서 호출할 수 있지만 이벤트 핸들러에서는 사용할 수 없습니다. 대신 useRouter 훅(hook)을 사용할 수 있습니다.
  • redirect는 절대 URL도 허용하며 외부 링크로 리다이렉트하는 데 사용할 수 있습니다.
  • 렌더링 과정 전에 리다이렉트하려면 next.config.js 또는 미들웨어를 사용하세요.

자세한 정보는 redirect API 참조를 참조하세요.

permanentRedirect 함수

permanentRedirect 함수를 사용하면 사용자를 다른 URL로 영구적으로 리다이렉트할 수 있습니다. 서버 컴포넌트, 라우트 핸들러, 그리고 서버 액션에서 permanentRedirect를 호출할 수 있습니다.

permanentRedirect는 주로 엔티티의 정식 URL을 변경하는 데이터 변경이나 이벤트 후에 사용됩니다. 예를 들어, 사용자가 사용자 이름을 변경한 후 프로필 URL을 업데이트할 때:

app/actions.ts
"use server";
 
import { permanentRedirect } from "next/navigation";
import { revalidateTag } from "next/cache";
 
export async function updateUsername(username: string, formData: FormData) {
  try {
    // 데이터베이스 호출
  } catch (error) {
    // 오류 처리
  }
 
  revalidateTag("username"); // 사용자 이름에 대한 모든 참조 업데이트
  permanentRedirect(`/profile/${username}`); // 새 사용자 프로필로 이동
}
app/actions.js
"use server";
 
import { permanentRedirect } from "next/navigation";
import { revalidateTag } from "next/cache";
 
export async function updateUsername(username, formData) {
  try {
    // 데이터베이스 호출
  } catch (error) {
    // 오류 처리
  }
 
  revalidateTag("username"); // 사용자 이름에 대한 모든 참조 업데이트
  permanentRedirect(`/profile/${username}`); // 새 사용자 프로필로 이동
}

알아두면 좋은 점:

  • permanentRedirect는 기본적으로 308 (영구 리다이렉트) 상태 코드를 반환합니다.
  • permanentRedirect는 절대 URL도 허용하며 외부 링크로 리다이렉트하는 데 사용할 수 있습니다.
  • 렌더링 과정 전에 리다이렉트하려면 next.config.js 또는 미들웨어를 사용하세요.

자세한 정보는 permanentRedirect API 참조를 참조하세요.

useRouter() 훅(Hook)

클라이언트 컴포넌트의 이벤트 핸들러 내에서 리다이렉트해야 하는 경우, useRouter 훅의 push 메서드를 사용할 수 있습니다. 예를 들어:

app/page.tsx
"use client";
 
import { useRouter } from "next/navigation";
 
export default function Page() {
  const router = useRouter();
 
  return (
    <button type="button" onClick={() => router.push("/dashboard")}>
      대시보드
    </button>
  );
}
app/page.js
"use client";
 
import { useRouter } from "next/navigation";
 
export default function Page() {
  const router = useRouter();
 
  return (
    <button type="button" onClick={() => router.push("/dashboard")}>
      대시보드
    </button>
  );
}

알아두면 좋은 점:

  • 프로그래밍 방식으로 사용자를 네비게이트할 필요가 없다면 <Link> 컴포넌트를 사용해야 합니다.

자세한 정보는 useRouter API 참조를 참조하세요.

next.config.jsredirects

next.config.js 파일의 redirects 옵션을 사용하면 들어오는 요청 경로를 다른 목적지 경로로 리다이렉트할 수 있습니다. 이는 페이지의 URL 구조를 변경하거나 미리 알려진 리다이렉트 목록이 있을 때 유용합니다.

redirects경로, 헤더, 쿠키, 쿼리 매칭을 지원하여 들어오는 요청에 따라 사용자를 리다이렉트할 수 있는 유연성을 제공합니다.

redirects를 사용하려면 next.config.js 파일에 옵션을 추가하세요:

next.config.js
module.exports = {
  async redirects() {
    return [
      // 기본 리다이렉트
      {
        source: "/about",
        destination: "/",
        permanent: true,
      },
      // 와일드카드 경로 매칭
      {
        source: "/blog/:slug",
        destination: "/news/:slug",
        permanent: true,
      },
    ];
  },
};

자세한 정보는 redirects API 참조를 참조하세요.

알아두면 좋은 점:

  • redirectspermanent 옵션을 통해 307 (임시 리다이렉트) 또는 308 (영구 리다이렉트) 상태 코드를 반환할 수 있습니다.
  • redirects는 플랫폼에 따라 제한이 있을 수 있습니다. 예를 들어, Vercel에서는 1,024개의 리다이렉트 제한이 있습니다. 많은 수의 리다이렉트(1000+)를 관리하려면 미들웨어를 사용하여 사용자 정의 솔루션을 만드는 것을 고려해보세요. 자세한 내용은 대규모 리다이렉트 관리를 참조하세요.
  • redirects는 미들웨어 전에 실행됩니다.

미들웨어의 NextResponse.redirect

미들웨어를 사용하면 요청이 완료되기 전에 코드를 실행할 수 있습니다. 그런 다음 들어오는 요청에 따라 NextResponse.redirect를 사용하여 다른 URL로 리다이렉트할 수 있습니다. 이는 조건(예: 인증, 세션 관리 등)에 따라 사용자를 리다이렉트하거나 대량의 리다이렉트가 있는 경우에 유용합니다.

예를 들어, 사용자가 인증되지 않은 경우 /login 페이지로 리다이렉트하려면:

middleware.ts
import { NextResponse, NextRequest } from "next/server";
import { authenticate } from "auth-provider";
 
export function middleware(request: NextRequest) {
  const isAuthenticated = authenticate(request);
 
  // 사용자가 인증된 경우 정상적으로 계속 진행
  if (isAuthenticated) {
    return NextResponse.next();
  }
 
  // 인증되지 않은 경우 로그인 페이지로 리다이렉트
  return NextResponse.redirect(new URL("/login", request.url));
}
 
export const config = {
  matcher: "/dashboard/:path*",
};
middleware.js
import { NextResponse } from "next/server";
import { authenticate } from "auth-provider";
 
export function middleware(request) {
  const isAuthenticated = authenticate(request);
 
  // 사용자가 인증된 경우 정상적으로 계속 진행
  if (isAuthenticated) {
    return NextResponse.next();
  }
 
  // 인증되지 않은 경우 로그인 페이지로 리다이렉트
  return NextResponse.redirect(new URL("/login", request.url));
}
 
export const config = {
  matcher: "/dashboard/:path*",
};

알아두면 좋은 점:

  • 미들웨어는 next.config.jsredirects 후에 그리고 렌더링 전에 실행됩니다.

자세한 정보는 미들웨어 문서를 참조하세요.

대규모 리다이렉트 관리 (고급)

많은 수의 리다이렉트(1000+)를 관리하려면 미들웨어를 사용하여 사용자 정의 솔루션을 만드는 것을 고려해볼 수 있습니다. 이를 통해 애플리케이션을 다시 배포하지 않고도 프로그래밍 방식으로 리다이렉트를 처리할 수 있습니다.

이를 위해 다음 사항을 고려해야 합니다:

  1. 리다이렉트 맵 생성 및 저장.
  2. 데이터 조회 성능 최적화.

Next.js 예제: 아래 권장 사항의 구현에 대해서는 블룸 필터를 사용한 미들웨어 예제를 참조하세요.

1. 리다이렉트 맵 생성 및 저장

리다이렉트 맵은 데이터베이스(일반적으로 키-값 저장소) 또는 JSON 파일에 저장할 수 있는 리다이렉트 목록입니다.

다음과 같은 데이터 구조를 고려해보세요:

{
  "/old": {
    "destination": "/new",
    "permanent": true
  },
  "/blog/post-old": {
    "destination": "/blog/post-new",
    "permanent": true
  }
}

미들웨어에서 Vercel의 Edge Config 또는 Redis와 같은 데이터베이스에서 읽고 들어오는 요청에 따라 사용자를 리다이렉트할 수 있습니다:

middleware.ts
import { NextResponse, NextRequest } from "next/server";
import { get } from "@vercel/edge-config";
 
type RedirectEntry = {
  destination: string;
  permanent: boolean;
};
 
export async function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname;
  const redirectData = await get(pathname);
 
  if (redirectData && typeof redirectData === "string") {
    const redirectEntry: RedirectEntry = JSON.parse(redirectData);
    const statusCode = redirectEntry.permanent ? 308 : 307;
    return NextResponse.redirect(redirectEntry.destination, statusCode);
  }
 
  // 리다이렉트를 찾지 못한 경우, 리다이렉트하지 않고 계속 진행
  return NextResponse.next();
}
middleware.js
import { NextResponse } from "next/server";
import { get } from "@vercel/edge-config";
 
export async function middleware(request) {
  const pathname = request.nextUrl.pathname;
  const redirectData = await get(pathname);
 
  if (redirectData) {
    const redirectEntry = JSON.parse(redirectData);
    const statusCode = redirectEntry.permanent ? 308 : 307;
    return NextResponse.redirect(redirectEntry.destination, statusCode);
  }
 
  // 리다이렉트를 찾지 못한 경우, 리다이렉트하지 않고 계속 진행
  return NextResponse.next();
}

2. 데이터 조회 성능 최적화

모든 들어오는 요청에 대해 대규모 데이터셋을 읽는 것은 느리고 비용이 많이 들 수 있습니다. 데이터 조회 성능을 최적화하는 두 가지 방법이 있습니다:

  • Vercel Edge Config 또는 Redis와 같이 빠른 읽기에 최적화된 데이터베이스를 사용합니다.
  • 블룸 필터와 같은 데이터 조회 전략을 사용하여 더 큰 리다이렉트 파일이나 데이터베이스를 읽기 전에 리다이렉트가 존재하는지 효율적으로 확인합니다.

이전 예제를 고려하여, 생성된 블룸 필터 파일을 미들웨어로 가져온 다음, 들어오는 요청 경로가 블룸 필터에 존재하는지 확인할 수 있습니다.

존재한다면, 요청을 라우트 핸들러로 전달하여 실제 파일을 확인하고 사용자를 적절한 URL로 리다이렉트합니다. 이렇게 하면 모든 들어오는 요청을 느리게 만들 수 있는 큰 리다이렉트 파일을 미들웨어로 가져오는 것을 피할 수 있습니다.

middleware.ts
import { NextResponse, NextRequest } from "next/server";
import { ScalableBloomFilter } from "bloom-filters";
import GeneratedBloomFilter from "./redirects/bloom-filter.json";
 
type RedirectEntry = {
  destination: string;
  permanent: boolean;
};
 
// 생성된 JSON 파일에서 블룸 필터 초기화
const bloomFilter = ScalableBloomFilter.fromJSON(GeneratedBloomFilter as any);
 
export async function middleware(request: NextRequest) {
  // 들어오는 요청의 경로 가져오기
  const pathname = request.nextUrl.pathname;
 
  // 경로가 블룸 필터에 있는지 확인
  if (bloomFilter.has(pathname)) {
    // 경로를 라우트 핸들러로 전달
    const api = new URL(
      `/api/redirects?pathname=${encodeURIComponent(request.nextUrl.pathname)}`,
      request.nextUrl.origin
    );
 
    try {
      // 라우트 핸들러에서 리다이렉트 데이터 가져오기
      const redirectData = await fetch(api);
 
      if (redirectData.ok) {
        const redirectEntry: RedirectEntry | undefined =
          await redirectData.json();
 
        if (redirectEntry) {
          // 상태 코드 결정
          const statusCode = redirectEntry.permanent ? 308 : 307;
 
          // 목적지로 리다이렉트
          return NextResponse.redirect(redirectEntry.destination, statusCode);
        }
      }
    } catch (error) {
      console.error(error);
    }
  }
 
  // 리다이렉트를 찾지 못한 경우, 리다이렉트하지 않고 요청 계속 진행
  return NextResponse.next();
}
middleware.js
import { NextResponse } from "next/server";
import { ScalableBloomFilter } from "bloom-filters";
import GeneratedBloomFilter from "./redirects/bloom-filter.json";
 
// 생성된 JSON 파일에서 블룸 필터 초기화
const bloomFilter = ScalableBloomFilter.fromJSON(GeneratedBloomFilter);
 
export async function middleware(request) {
  // 들어오는 요청의 경로 가져오기
  const pathname = request.nextUrl.pathname;
 
  // 경로가 블룸 필터에 있는지 확인
  if (bloomFilter.has(pathname)) {
    // 경로를 라우트 핸들러로 전달
    const api = new URL(
      `/api/redirects?pathname=${encodeURIComponent(request.nextUrl.pathname)}`,
      request.nextUrl.origin
    );
 
    try {
      // 라우트 핸들러에서 리다이렉트 데이터 가져오기
      const redirectData = await fetch(api);
 
      if (redirectData.ok) {
        const redirectEntry = await redirectData.json();
 
        if (redirectEntry) {
          // 상태 코드 결정
          const statusCode = redirectEntry.permanent ? 308 : 307;
 
          // 목적지로 리다이렉트
          return NextResponse.redirect(redirectEntry.destination, statusCode);
        }
      }
    } catch (error) {
      console.error(error);
    }
  }
 
  // 리다이렉트를 찾지 못한 경우, 리다이렉트하지 않고 요청 계속 진행
  return NextResponse.next();
}

그런 다음 라우트 핸들러에서:

app/redirects/route.ts
import { NextRequest, NextResponse } from "next/server";
import redirects from "@/app/redirects/redirects.json";
 
type RedirectEntry = {
  destination: string;
  permanent: boolean;
};
 
export function GET(request: NextRequest) {
  const pathname = request.nextUrl.searchParams.get("pathname");
  if (!pathname) {
    return new Response("Bad Request", { status: 400 });
  }
 
  // redirects.json 파일에서 리다이렉트 항목 가져오기
  const redirect = (redirects as Record<string, RedirectEntry>)[pathname];
 
  // 블룸 필터 거짓 양성 처리
  if (!redirect) {
    return new Response("No redirect", { status: 400 });
  }
 
  // 리다이렉트 항목 반환
  return NextResponse.json(redirect);
}
app/redirects/route.js
import { NextResponse } from "next/server";
import redirects from "@/app/redirects/redirects.json";
 
export function GET(request) {
  const pathname = request.nextUrl.searchParams.get("pathname");
  if (!pathname) {
    return new Response("Bad Request", { status: 400 });
  }
 
  // redirects.json 파일에서 리다이렉트 항목 가져오기
  const redirect = redirects[pathname];
 
  // 블룸 필터 거짓 양성 처리
  if (!redirect) {
    return new Response("No redirect", { status: 400 });
  }
 
  // 리다이렉트 항목 반환
  return NextResponse.json(redirect);
}

알아두면 좋은 점:

  • 블룸 필터를 생성하려면 bloom-filters와 같은 라이브러리를 사용할 수 있습니다.
  • 악의적인 요청을 방지하기 위해 라우트 핸들러로 보내는 요청을 검증해야 합니다.