Next.js 14에서 SEO를 위한 리다이렉트 최적화: 301 vs 308 vs 307
Next.js 14에서 SEO를 위한 리다이렉트 최적화: 301 vs 308 vs 307
SEO 개선 작업 중 구글봇이 여전히 예전 URL을 크롤링하는 현상을 발견했습니다. 기존 raffles URL이 크롤러(구글봇)에게 계속 인덱싱되며 검색 결과에 남아 있었기 때문입니다. 이를 해결하기 위해 영구 리다이렉트(301 또는 308) 방식을 점검하고 적용하는 과정을 정리합니다.
📌 1. 기존 리다이렉트 코드 (307 적용)
Next.js에서 제공하는 redirect() 함수는 서버 컴포넌트, 라우트 핸들러, 서버 액션에서 사용하여 사용자를 다른 URL로 리디렉트하는 기능을 제공합니다.
- 기본적으로 307 Temporary Redirect가 적용됩니다.
- 스트리밍 컨텍스트에서 사용되면 클라이언트 측에서
<meta>태그를 통해 리디렉트됩니다. - 서버 액션에서 사용되면 303 리다이렉트가 발생합니다.
- 위의 상황이 아니라면 기본적으로 307 리다이렉트가 적용됩니다.
import { notFound, redirect } from "next/navigation"; import { getProductDetailById } from "@/lib/api/product"; export default async function Page({ params }: { params: { id: number } }) { if (isNaN(Number(params.id))) { return notFound(); } const product = await getProductDetailById(params.id); if (!product || product?.error) return notFound(); if (product.categorySlug === "ticket") redirect(`/ticket/${product.permalink}`); if (product.categorySlug === "collection") redirect(`/collection/${product.permalink}`); redirect(`/product/${product.permalink}`); return; }
🚨 문제점: 307은 임시 리다이렉트이므로 크롤러(구글봇)가 여전히 raffles URL을 탐색함
redirect()함수의 기본 동작이 307 Temporary Redirect이므로, 검색 엔진이 여전히 원래 URL을 유지합니다.- 결과적으로
rafflesURL이 여전히 검색 엔진에 남아 크롤링됨.
📌 2. 301 vs 307 비교: 왜 301을 사용해야 하는가?
| 상태 코드 | 메서드 유지 | 브라우저 캐시 | SEO 영향 | 사용 예시 | | -------------------------- | -------------------- | ------- | ------------------ | -------------- | | 301 Moved Permanently | ❌ (POST → GET 변경 가능) | ✅ | ✅ (새 URL로 인덱싱됨) | SEO 최적화, 영구 이동 | | 307 Temporary Redirect | ✅ (POST 유지됨) | ❌ | ❌ (크롤러가 원래 URL 유지) | 일시적 리다이렉트 필요 시 |
✅ 301은 크롤러가 새로운 URL을 공식 주소로 인식하며, 검색 엔진이 기존 URL을 제거하도록 유도합니다. ❌ 307은 일시적 리다이렉트이므로 크롤러가 기존 URL을 계속 크롤링합니다.
📌 3. next.config.js를 이용한 영구 리다이렉트 적용 방법 (정적 URL만 가능)
module.exports = { async redirects() { return [ { source: "/raffles/:id", destination: "/product/:id", permanent: true, // ✅ 영구 리다이렉트 적용 }, ]; }, };
✅ 정적인 URL에는 next.config.js를 사용하여 영구 리다이렉트 (308) 을 적용할 수 있지만, 동적 라우팅을 포함한 데이터 기반 리다이렉트는 불가능합니다.
✅ 앞서 301에 대해 설명했지만 왜 갑자기 308인지 아래 설명하겠습니다.
📌 4. permanentRedirect() 소개 (308 적용) 및 308의 등장 이유
Next.js 14에서는 permanentRedirect()를 사용하여 308을 적용할 수 있습니다.
왜 308이 등장했을까?
전통적인 301은 POST 요청을 GET으로 변경하는 문제가 있었습니다. 이 때문에 POST 요청을 유지하면서도 영구적인 리다이렉트를 지원하는 308이 등장하게 되었습니다. 즉 308이 조금 더 안전하며 업데이트 된 버전이라고 볼 수 있습니다. 때문에 Next는 308을 기본 지원합니다.
| 상태 코드 | 메서드 유지 | 브라우저 캐시 | SEO 영향 | Next.js 지원 |
| ---------------------------- | ---------------------- | --------- | ------------------- | --------------------------- |
| 301 Moved Permanently | ❌ (POST → GET 변경 가능) | ✅ | ✅ (새 URL로 인덱싱됨) | ❌ 직접 지원 없음 (API Route 필요) |
| 308 Permanent Redirect | ✅ (POST 유지됨) | ✅ | ✅ (301과 동일하게 처리됨) | ✅ permanentRedirect() 사용 |
✅ 308은 301과 동일한 SEO 영향을 가지며, Next.js에서 기본 지원됨. ❌ 301은 직접 지원되지 않으며, API Route를 이용해야 적용 가능.
📌 5. permanentRedirect() 적용 코드
import { notFound, permanentRedirect } from "next/navigation"; import { getProductDetailById } from "@/lib/api/product"; export default async function Page({ params }: { params: { id: number } }) { if (isNaN(Number(params.id))) { return notFound(); } const product = await getProductDetailById(params.id); if (!product || product?.error) return notFound(); if (product.categorySlug === "raffles") { permanentRedirect(`/product/${product.permalink}`); } if (product.categorySlug === "ticket") permanentRedirect(`/ticket/${product.permalink}`); if (product.categorySlug === "collection") permanentRedirect(`/collection/${product.permalink}`); permanentRedirect(`/product/${product.permalink}`); }
✅ 이제 raffles URL을 방문하는 사용자는 product, ticket, collection 경로로 영구적으로 리다이렉트됨
✅ 검색엔진(구글봇)은 기존 raffles URL을 제거하고 새로운 URL을 공식 주소로 인식
✅ Next.js의 공식적인 permanentRedirect()를 사용하여 유지보수도 용이
📌 6. 그럼에도 301을 사용해야 한다면? (API Route 활용)
Next.js에서는 permanentRedirect()가 308을 반환하며, 301을 직접 지정하는 기능이 없음. 따라서, 301을 적용하려면 API Route를 활용해야 함.
module.exports = { async redirects() { return [ { source: "/raffles/:id", destination: "/api/redirect?id=:id", permanent: true, }, ]; }, };
import { NextRequest, NextResponse } from "next/server"; import { getProductDetailById } from "@/lib/api/product"; export async function GET(req: NextRequest) { const id = req.nextUrl.searchParams.get("id"); const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || req.nextUrl.origin; if (!id || isNaN(Number(id))) { return NextResponse.json({ error: "Not Found" }, { status: 404 }); } const product = await getProductDetailById(Number(id)); if (!product || product?.error) { return NextResponse.json({ error: "Not Found" }, { status: 404 }); } let destination = `${baseUrl}/product/${product.permalink}`; if (product.categorySlug === "ticket") destination = `${baseUrl}/ticket/${product.permalink}`; if (product.categorySlug === "collection") destination = `${baseUrl}/collection/${product.permalink}`; return NextResponse.redirect(destination, 301); }
📌 7. 최종 정리: 301 vs 308 vs 307 비교
- ✅ 301: SEO 최적화에 유리하지만 Next.js에서 기본 지원되지 않아 API Route 필요.
- ✅ 308: Next.js 기본 지원, 301과 동일한 SEO 효과, POST 요청 유지.
- ❌ 307: 임시 리다이렉트, 크롤러가 원래 URL을 유지함.
🚀 결론:
이번 SEO 최적화 작업의 핵심은 기존 URL(raffles)이 더 이상 사용되지 않으며, 새로운 URL 구조(product, ticket, collection)로 완전히 변경되었기 때문입니다. 즉, 기존 URL이 계속 크롤링되면 안 되며, 검색 엔진이 새로운 URL만을 인덱싱하도록 유도해야 합니다.
이를 위해 임시 리다이렉트(307) 대신 영구 리다이렉트(308)를 적용해야 합니다.
307은 크롤러(구글봇)가 기존 URL을 유지하므로 SEO 최적화가 이루어지지 않습니다. 308은 301과 동일한 SEO 효과를 가지며, 기존 URL이 아니라 새로운 URL을 공식 URL로 인식되도록 유도합니다. 특히, 308은 POST 요청을 유지할 수 있어 더 안전한 영구 리다이렉트 방식입니다. Next.js는 301 대신 308을 기본적으로 지원하므로, permanentRedirect()를 사용하는 것이 가장 적절한 방법입니다. ✅ 이전 URL이 크롤링되지 않도록 하기 위해 영구 리다이렉트(308)를 사용해야 함 ✅ Next.js에서는 308을 기본 지원하므로 permanentRedirect()가 최적의 선택 ✅ SEO 최적화를 위해 검색 엔진이 새로운 URL만을 인덱싱하도록 유도
그럼에도 301을 구현하고자 한다면 API Route를 활용할 수 있습니다. 그러나 301은 Next.js에서 기본 지원되지 않으며, 직접 API Route를 작성해야 하는 점을 고려해야 합니다. 즉, 특별한 이유가 없다면 308을 사용하는 것이 더 쉽고 SEO 관점에서도 더 안전한 선택입니다.
🚀 일반 redirect()(307)를 사용하는 경우
| 상황 | redirect()(307) 사용 이유 |
|------|-----------------------------|
| 로그인 후 특정 페이지로 이동 | 로그인 상황은 동적으로 변경될 수 있으며, 영극 리다이렉트가 필요하지 않음 |
| 폼 제출 후 특정 페이지로 이동 | 사용자의 요청이 유지되어야 하며, 일시적인 리다이렉트가 적절 |
| A/B 테스트 또는 기본 플래그 적용 | 특정 조건에 따라 다른 페이지로 동적으로 이동해야 함 |
| 사이트 유지보수 또는 일시적 변경 상황 적용 | 유지보수가 끝나면 원래 URL을 다시 사용할 예정 |