_redirects: 정적 사이트에서 리디렉션 규칙 관리하기

_redirects: 정적 사이트에서 리디렉션 규칙 관리하기

블로그 글의 주소를 바꾸거나 문서 사이트의 폴더 구조를 정리하고 나면 꼭 이런 일이 생기는데요. 예전에 SNS나 다른 블로그에 공유된 옛 주소로 들어온 사람들이 모두 404 페이지를 보게 됩니다. 😅 게다가 검색 엔진이 옛 주소에 쌓아 둔 신호도 새 주소로 넘어가지 못합니다.

이럴 때 필요한 게 리디렉션(redirect) 규칙입니다. 서버를 직접 운영한다면 서버 코드에서 처리하면 되지만, 정적 사이트는 이야기가 다른데요. 이 글에서는 정적 사이트에서 리디렉션 규칙을 어디에 어떻게 적는지, 호스팅 플랫폼별 문법과 흔히 빠지는 함정을 직접 실행해 본 결과와 함께 정리해 보겠습니다.

리디렉션 규칙은 어디에 둘까?

리디렉션을 거는 방법은 크게 세 가지입니다.

방법동작검색 엔진
서버 측 리디렉션서버가 301, 308 같은 상태 코드로 응답가장 확실함
메타 리프레시HTML의 <meta http-equiv="refresh">로 이동즉시 이동이면 영구 리디렉션으로 취급
JavaScript 리디렉션location.href를 바꿔 이동렌더링에 실패하면 놓칠 수 있음

Google의 리디렉션 문서는 가능하면 서버 측 영구 리디렉션을 쓰라고 권합니다. 메타 리프레시(meta refresh)는 0초로 즉시 이동하면 영구 리디렉션으로, 지연이 있으면 임시 리디렉션으로 해석하고, JavaScript 리디렉션은 다른 방법을 쓸 수 없을 때만 쓰라고 합니다.

문제는 정적 사이트에는 응답을 만드는 서버 코드가 없다는 점인데요. 그래서 대부분의 정적 호스팅 플랫폼은 리디렉션 규칙을 적는 설정 파일을 따로 제공합니다. 플랫폼이 이 파일을 읽어서 정적 파일보다 먼저 리디렉션 응답을 보내 주는 거죠. Cloudflare와 Netlify는 _redirects 파일을, Vercel은 vercel.json을 씁니다.

서버 코드와 정적 파일을 섞어 쓰는 앱이라도 이 파일이 필요할 수 있습니다. 사전 렌더링된 페이지는 서버 코드를 거치지 않고 정적 파일로 바로 응답되기 때문에, 서버 미들웨어에 넣은 리디렉션이 적용되지 않을 수 있거든요. 이 구조는 SPA, SSG, SSR 비교 글과 트레일링 슬래시 글에서 다뤘습니다.

_redirects 파일 문법

Cloudflare Pages와 Cloudflare Workers의 정적 자산, 그리고 Netlify는 모두 같은 형식의 _redirects 파일을 씁니다. 빌드 결과물 폴더의 최상위에 확장자 없이 _redirects라는 이름으로 두면 됩니다. 보통은 프레임워크의 public/ 폴더에 넣어 두면 빌드할 때 그대로 복사됩니다.

한 줄에 규칙 하나씩, 옛 주소와 새 주소, 상태 코드를 공백으로 구분해 적습니다. #으로 시작하는 줄은 주석입니다.

public/_redirects
# 글 주소를 바꿨을 때: 영구 이동
/old-post/ /new-post/ 301

# 폴더째 옮겼을 때: 스플랫
/docs/v1/* /docs/v2/:splat 301

# 경로 구조를 바꿨을 때: 플레이스홀더
/blog/:year/:slug/ /:slug/ 301

# 상태 코드를 생략하면
/temp /new-post/

이 파일을 Cloudflare Workers 정적 자산으로 올리고 wrangler dev(4.45.0)로 띄워서 요청해 봤습니다. 상태 코드와 리디렉션 목적지만 찍도록 curl에 -w 옵션을 줬습니다.

curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' localhost:8790/old-post/
결과
/old-post/                 301 http://localhost:8790/new-post/
/docs/v1/install/          301 http://localhost:8790/docs/v2/install/
/blog/2024/new-post/       301 http://localhost:8790/new-post/
/temp                      302 http://localhost:8790/new-post/

마지막 줄을 눈여겨보세요. 상태 코드를 생략하면 Cloudflare는 302 임시 리디렉션을 보냅니다. 반면 Netlify는 같은 줄에 301을 기본값으로 씁니다. 같은 파일이 플랫폼에 따라 다르게 동작하는 셈이라, 상태 코드는 항상 직접 적는 게 안전합니다. 주소를 옮겼다면 영구 이동인 301이나 308을 써야 검색 엔진이 새 주소를 대표 주소로 받아들입니다. 상태 코드의 차이는 HTTP 상태 코드 안내서를 참고하세요.

스플랫과 플레이스홀더

규칙을 하나씩 적기 어려울 때는 패턴을 씁니다.

스플랫(splat)은 *로 나머지 경로를 통째로 받아서 목적지의 :splat 자리에 넣습니다. 위 예에서 /docs/v1/install/이 /docs/v2/install/로 간 게 스플랫 덕분인데요. 폴더 아래 문서가 수십 개여도 규칙 한 줄이면 됩니다. Cloudflare에서는 규칙 하나에 스플랫을 하나만 쓸 수 있습니다.

플레이스홀더(placeholder)는 :year, :slug처럼 이름을 붙여 경로 조각 하나를 받습니다. 스플랫과 달리 /를 넘지 않고 정확히 한 조각만 받기 때문에, 날짜가 들어 있던 옛 주소 구조를 단순하게 바꿀 때 좋습니다. 위 예의 /blog/2024/new-post/가 /new-post/로 간 것처럼, 받은 값 중 필요한 것만 골라 쓸 수도 있습니다.

쿼리 스트링과 끝 슬래시

리디렉션 규칙에서 자주 놓치는 부분이 두 가지 있습니다.

우선 쿼리 스트링은 그대로 따라갑니다. 광고 추적용 파라미터가 붙은 옛 주소로 요청해 봤습니다.

결과
/old-post/?ref=x           301 http://localhost:8790/new-post/?ref=x

규칙에 쿼리 스트링을 적지 않았는데도 목적지에 ?ref=x가 붙었습니다. 반대로 Cloudflare는 쿼리 스트링으로 규칙을 고르는 기능은 지원하지 않아서, /page?id=1과 /page?id=2를 서로 다른 곳으로 보낼 수는 없습니다.

다음은 끝 슬래시입니다. 규칙은 /old-post/로 적었는데 슬래시 없이, 또는 대소문자를 바꿔서 요청해 봤습니다.

결과
/old-post                  404
/Old-Post/                 404

둘 다 규칙에 걸리지 않고 404가 났습니다. Cloudflare는 규칙의 주소와 정확히 같은 경로만 리디렉션하기 때문입니다. 옛 주소가 두 형태로 공유됐을 가능성이 있다면 규칙을 두 줄 다 적어 두는 게 안전합니다. 참고로 Netlify는 규칙을 비교하기 전에 끝 슬래시를 정리해서, 슬래시가 있든 없든 같은 규칙에 걸립니다. 그래서 Netlify에서 슬래시만 붙이거나 떼는 규칙을 적으면 무한 리디렉션이 생기니 주의하세요.

규칙 순서와 우선순위

같은 경로에 규칙이 여러 개 걸리면 위에 있는 규칙이 이깁니다. 그래서 구체적인 규칙을 위에, 스플랫 같은 넓은 규칙을 아래에 둡니다.

public/_redirects
# 특정 문서만 다른 곳으로
/docs/v1/legacy/ /archive/legacy/ 301

# 나머지는 v2로
/docs/v1/* /docs/v2/:splat 301

순서를 거꾸로 적으면 /docs/v1/legacy/도 스플랫 규칙에 먼저 걸려서 /docs/v2/legacy/로 가 버립니다.

정적 파일과의 우선순위는 플랫폼마다 다릅니다. Cloudflare는 요청한 경로에 파일이 있든 없든 _redirects 규칙을 먼저 따릅니다. 반면 Netlify는 기본적으로 그 경로에 파일이 있으면 규칙을 건너뛰는데, 이를 섀도잉(shadowing)이라고 부릅니다. 파일이 있어도 리디렉션하려면 상태 코드 뒤에 !를 붙여 301!처럼 적어야 합니다.

또 하나 기억할 점은 이 파일이 정적 자산에만 적용된다는 겁니다. Cloudflare 문서에 따르면 Pages Functions나 Worker 코드가 응답하는 요청에는 _redirects가 적용되지 않습니다. 서버 코드가 처리하는 경로라면 그 코드 안에서 리디렉션해야 합니다.

마지막으로 개수 제한이 있습니다. Cloudflare는 파일 하나에 고정 규칙 2,000개와 스플랫이나 플레이스홀더를 쓴 동적 규칙 100개까지, 규칙 한 줄은 1,000자까지 허용합니다. 이보다 많다면 Cloudflare 대시보드의 Bulk Redirects를 씁니다.

Vercel: vercel.json

Vercel은 _redirects 파일 대신 vercel.json의 redirects 배열에 규칙을 적습니다.

vercel.json
{
  "redirects": [
    { "source": "/old-post/", "destination": "/new-post/" },
    { "source": "/docs/v1/:path*", "destination": "/docs/v2/:path*" },
    { "source": "/temp", "destination": "/new-post/", "permanent": false }
  ]
}

permanent를 생략하면 기본값이 true라서 308이, false로 두면 307이 나갑니다. Cloudflare와 기본값이 반대인 셈이죠. 301처럼 다른 코드가 필요하면 permanent 대신 statusCode를 지정합니다. 경로 패턴은 :path*처럼 이름 뒤에 *를 붙여 나머지 경로를 받고, has와 missing으로 헤더나 쿠키 조건을 걸 수도 있습니다.

프레임워크 설정으로 관리하기

플랫폼 파일을 직접 쓰는 대신 프레임워크 설정에 리디렉션을 적을 수도 있습니다. 예를 들어 Astro는 설정 파일의 redirects에 규칙을 적습니다.

astro.config.mjs
import { defineConfig } from "astro/config";

export default defineConfig({
  redirects: {
    "/old-post/": "/new-post/",
  },
});

그런데 여기에는 함정이 있습니다. 어댑터 없이 정적 사이트로 빌드하면 Astro는 HTTP 리디렉션을 만들 수 없어서, 옛 주소에 메타 리프레시 태그가 든 HTML 파일을 대신 만듭니다. 상태 코드도 지정할 수 없습니다. 반대로 Netlify나 Vercel 같은 어댑터를 쓰면 Astro가 이 설정을 플랫폼 형식(_redirects 등)으로 바꿔 주어, 진짜 HTTP 리디렉션이 됩니다. 이때 기본 상태 코드는 GET 요청이 301, 그 밖의 메서드는 308입니다.

Next.js도 next.config.js의 redirects()로 규칙을 적으며, permanent: true면 308, false면 307을 씁니다. 다만 프레임워크 설정은 결국 플랫폼이 이해하는 형태로 바뀌어야 동작하니, 배포한 뒤에는 꼭 curl로 실제 상태 코드를 확인해 보세요.

마치며

정적 사이트의 리디렉션은 서버 코드가 아니라 플랫폼 설정 파일의 몫입니다. 주소를 바꿀 일이 생기면 다음을 확인해 보세요.

  • 상태 코드를 직접 적었는가(주소를 옮겼다면 301이나 308)
  • 끝 슬래시가 다른 형태로도 들어올 수 있다면 두 형태 모두 적었는가
  • 구체적인 규칙이 넓은 규칙보다 위에 있는가
  • 리디렉션되는 옛 주소를 사이트맵에서 뺐는가

주소를 바꾸는 순간 함께 규칙을 추가하는 습관을 들이면, 몇 달 뒤 Search Console의 404 목록을 뒤지며 빠진 규칙을 찾는 일을 줄일 수 있습니다.

더 자세한 문법은 Cloudflare의 Redirects 문서를 참고하세요.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord