본문으로 건너뛰기
서버 & .htaccess

워드프레스 .htaccess 리다이렉트 설정: 301을 제대로 거는 법

순위를 잃지 않고 URL을 옮기는 방법. 어떤 워드프레스 .htaccess 리다이렉트 지시어를 쓸지, 규칙을 어디에 둘지, 접속이 막히지 않게 검증하는 방법을 정리했습니다.

게시됨

URL을 옮기면서 예전 주소의 순위를 그대로 가져가고 싶으신가요. 그러려면 .htaccess에 301이 필요합니다. 단 워드프레스가 지우지 않는 위치에 두고, 알맞은 지시어로 작성하고, 워드프레스가 실제로 내보내는 정확한 URL을 가리켜야 합니다. 이 셋 중 하나라도 틀리면 리다이렉트 연쇄나 무한 루프, 혹은 wp-admin 접근까지 막아버리는 500이 돌아옵니다.

선택, 위치, 검증 순으로 살펴봅니다.

어떤 지시어인가: Redirect, RedirectMatch, RewriteRule

관련된 Apache 모듈은 둘이며, 서로 바꿔 쓸 수 없습니다.

Redirect(mod_alias) — 알고 있는 경로 하나를 목적지 하나로. 통하는 가장 단순한 방법입니다.

Redirect 301 /old-page/ https://example.com/new-page/

알아둘 동작이 둘 있습니다. 정확한 문자열이 아니라 경로 접두사로 매칭하므로 /old-page//old-page/anything/도 잡아내고 목적지에 /anything/을 덧붙입니다. 그리고 쿼리 문자열은 자동으로 넘겨줍니다.

RedirectMatch(mod_alias) — 정규식 하나로 여러 URL을 덮습니다. 섹션 전체를 옮길 때 쓰는 방식입니다.

RedirectMatch 301 ^/blog/(.*)$ https://example.com/articles/$1

캡처 그룹 (.*)가 목적지의 $1이 되므로 /blog/hello-world//articles/hello-world/에 도착합니다. 맨 앞 슬래시에 주의하세요. mod_alias 패턴은 URL 경로 전체와 매칭됩니다.

RewriteRule(mod_rewrite) — 리다이렉트가 호스트명, 프로토콜, 쿼리 문자열, 사용자 에이전트 같은 조건에 따라 달라질 때 필요합니다. mod_alias에는 이런 것을 검사할 수단이 없습니다.

RewriteEngine On
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]

디렉터리 단위 .htaccess에서는 패턴의 맨 앞 슬래시가 제거됩니다. 여기서는 ^(.*)$인데 mod_alias 예제에서는 ^/blog/인 이유가 그것입니다. 이 둘을 헷갈리는 것이 붙여넣은 규칙이 아무 반응도 하지 않는 가장 흔한 원인입니다.

기본은 mod_alias로 가세요. RewriteRule은 조건 검사가 필요할 때만 꺼내십시오. 정규식이 줄면 틀릴 여지도 줄어듭니다.

규칙을 두어야 할 자리

직접 만든 리다이렉트는 # BEGIN WordPress에 둡니다. 그 안이 아닙니다.

# --- custom redirects ---
Redirect 301 /old-page/ https://example.com/new-page/
RedirectMatch 301 ^/blog/(.*)$ https://example.com/articles/$1

# BEGIN WordPress
# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
# dynamically generated, and should only be modified via WordPress filters.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

서로 독립적인 이유가 둘입니다.

  1. 워드프레스는 자기 블록을 다시 씁니다. 설정 → 고유주소를 저장하면 flush_rewrite_rules()가 호출되어 마커 사이의 내용을 전부 버리고 새로 생성합니다. 안쪽에 넣은 규칙은 다음에 누군가 그 화면을 건드리는 순간 사라집니다. 몇 달 뒤일 수도 있고, 그 두 사건을 연결해 생각하는 사람은 아무도 없습니다.
  2. 승패를 가르는 건 순서입니다. 워드프레스 블록은 파일도 디렉터리도 아닌 모든 요청을 index.php로 보내는 포괄 규칙으로 끝납니다. 그 뒤에 놓인 RewriteRule은 절대 실행되지 않습니다.

솔직히 짚어야 할 복잡한 지점이 하나 있습니다. mod_alias와 mod_rewrite는 실제로 파일에 적힌 순서대로 실행되지 않습니다. Apache는 URL 변환 단계에서 mod_alias를 처리하고, 디렉터리 단위 mod_rewrite는 그보다 뒤인 fixup 단계에서 처리합니다. 그래서 파일상 아래에 있는 Redirect 줄이 위에 있는 RewriteRule을 이길 수 있습니다. 겹치는 경로에서 둘 다 필요해진다면, 그 경로에서는 한쪽 모듈만 골라 그 안에서 해결하세요. mod_alias와 mod_rewrite의 싸움을 디버깅하는 데 한 시간을 쓸 가치는 없습니다.

끝의 슬래시

워드프레스 자체의 정규화 리다이렉트는 대부분의 고유주소에 끝 슬래시를 붙입니다. 그래서 이렇게 쓰면,

Redirect 301 /old-page/ https://example.com/new-page

이동이 두 번 일어납니다. 먼저 내가 건 301이 슬래시 없는 URL로 보내고, 다음에 워드프레스 자체의 301이 슬래시를 붙입니다. 연쇄되어도 순위 신호는 전달되지만 크롤링 예산을 낭비하고 방문자마다 왕복이 한 번 늘어납니다.

먼저 목적지를 브라우저에서 열고, 주소창에 최종적으로 자리 잡은 URL을 그대로 복사해 사용하세요. 고유주소 구조가 .html로 끝나거나 끝 슬래시가 없다면 거기에 맞추면 됩니다. 여기에 모두에게 통하는 정답은 없고, 오직 워드프레스가 내보내는 것에 맞추는 원칙만 있습니다.

스스로 잠기지 않으면서 검증하기

.htaccess는 요청이 올 때마다 읽힙니다. 구문 오류는 wp-admin을 포함한 사이트 전체에 500을 반환하므로, 복구 경로는 필요해지기 전에 마련되어 있어야 합니다.

먼저 파일을 백업하세요. SSH에서는 이렇게 합니다.

cp /path/to/wordpress/.htaccess /path/to/wordpress/.htaccess.bak

사이트가 500을 내면 망가진 파일 위에 사본을 덮어쓰기만 해도 즉시 돌아옵니다. 다른 창에 SFTP 세션을 미리 열어 두세요. 사이트가 죽은 상태에서 처음부터 로그인하려 할 때 패닉이 시작됩니다. 참고로 apachectl configtest.htaccess파싱하지 않습니다. 사이트가 죽어 있어도 설정이 정상이라고 보고합니다.

브라우저 말고 curl로 검증하세요. 브라우저는 301 응답을 강하게 캐시하며 태연히 어제 결과를 보여줍니다.

curl -sI https://example.com/old-page/ | head -n 5

두 가지를 읽습니다. 상태 줄이 301인지, 그리고 Location: 헤더가 정확한 최종 URL인지. 연쇄 전체를 보려면 따라가면 됩니다.

curl -sIL https://example.com/old-page/ | grep -E '^(HTTP|Location)'

HTTP/1.1 301 줄이 둘 이상이면 연쇄를 만든 것입니다. 다섯 개를 넘기면 루프를 만든 것이고, curl이 멈춰서 알려줍니다.

확신이 서기 전까지는 302를 쓰세요. 302는 같은 방식으로 캐시되지 않으므로 실수를 몇 초 만에 되돌릴 수 있습니다. curl이 의도한 목적지를 보여주면 그때 301로 바꾸세요. 비용은 없고, 이 글의 어떤 습관보다도 많은 사이트를 구했습니다.

규칙이 아예 아무 일도 하지 않는 것 같다면, 애초에 Apache가 그 파일을 읽고 있는지부터 확인하세요. 디렉터리에 AllowOverride None이 걸려 있으면 Apache는 .htaccess를 완전히 무시하고, nginx는 아예 보지도 않습니다. 정규식을 파고들기 전에 curl -IServer: 헤더를 확인하세요. 사용 중인 Apache 버전과 설치 경로에 맞는 구문으로 블록을 구성하려면 워드프레스 .htaccess 생성기를 이용하세요.

아예 .htaccess를 쓰지 말아야 할 때

편집자가 직접 관리해야 하는 소수의 리다이렉트라면, 규칙을 데이터베이스에 저장하는 리다이렉트 플러그인이 더 나은 도구입니다. 호스팅 이전에도 살아남고 SSH도 필요 없습니다. 대신 대가는 분명합니다. 리다이렉트를 응답하려고 PHP가 부팅되어야 하므로 Apache가 직접 답하는 것보다 느립니다.

.htaccess는 구조적이고 영구적인 이동에 쓰세요. 도메인 변경, 섹션 이름 변경, https나 www 강제 같은 경우입니다. 일회성 편집 리다이렉트에는 플러그인을 쓰십시오. 어느 URL을 어느 계층이 담당하는지 알고 있다면 둘을 함께 써도 괜찮습니다. 두 곳에 정의된 리다이렉트는 나쁜 오후를 기다리는 버그이기 때문입니다.

FAQ

질문

워드프레스 .htaccess 파일에서 직접 만든 리다이렉트는 어디에 넣어야 하나요?

# BEGIN WordPress 줄 위에 넣어야 하며, 마커 사이에는 절대 넣지 않습니다. 누군가 고유주소 설정 화면을 저장할 때마다 워드프레스가 마커 안쪽을 통째로 다시 생성하므로, 안쪽에 넣은 규칙은 아무 경고 없이 사라집니다. 순서 때문에도 위치가 중요합니다. 워드프레스 블록은 매칭되지 않은 요청을 index.php로 보내는 포괄 규칙으로 끝나기 때문입니다.

301에는 Redirect, RedirectMatch, RewriteRule 중 무엇을 써야 하나요?

경로가 하나로 특정되면 Redirect, 패턴 하나로 여러 URL을 덮어야 하면 RedirectMatch, 호스트명이나 프로토콜, 쿼리 문자열 같은 조건에 따라 달라지면 RewriteRule을 씁니다. Redirect와 RedirectMatch는 mod_alias 소속이고 더 단순합니다. RewriteRule은 mod_rewrite 소속이며 조건을 검사할 수 있는 유일한 지시어입니다.

.htaccess 리다이렉트가 무한 루프를 만드는 이유는 무엇인가요?

대개 목적지 주소가 방문자를 보낸 그 규칙에 다시 매칭되어 규칙이 끝없이 발동하기 때문입니다. 또 하나 흔한 원인은 로드 밸런서나 CDN 뒤에서 https를 강제하는 경우입니다. 방문자는 이미 https를 쓰고 있어도 서버는 모든 요청을 평문 http로 인식합니다. 이때는 X-Forwarded-Proto 헤더를 검사해야 합니다.

워드프레스 리다이렉트에서 끝의 슬래시가 중요한가요?

중요합니다. 워드프레스의 정규화 리다이렉트가 대부분의 고유주소에 끝 슬래시를 붙이기 때문에, 슬래시 없는 URL로 301을 걸면 한 번이면 될 이동이 두 번이 됩니다. 연쇄된 리다이렉트도 순위 신호는 전달하지만 크롤링 예산을 낭비하고 방문자를 느리게 만듭니다. 슬래시를 포함해 워드프레스가 실제로 반환하는 최종 URL과 정확히 일치시키세요.

사이트를 망가뜨리지 않고 .htaccess 리다이렉트를 검증하려면 어떻게 하나요?

브라우저를 믿기 전에 헤더만 받아오는 옵션을 붙인 curl을 옛 URL에 실행해 상태 코드와 Location 헤더를 읽으세요. 브라우저는 301 응답을 강하게 캐시해서 오래된 결과를 보여줍니다. 그리고 반드시 정상 동작하는 파일의 사본을 먼저 만들어 두세요. .htaccess의 구문 오류는 wp-admin을 포함한 모든 페이지에 500을 반환합니다.

리다이렉트를 추가했더니 사이트 전체가 500 오류를 냅니다. 왜 그런가요?

.htaccess에 잘못된 지시어가 하나만 있어도 해당 디렉터리 아래의 모든 URL이 관리자 화면까지 함께 멈춥니다. 흔한 원인은 닫히지 않은 IfModule 블록, 빠뜨린 RewriteEngine On 줄, 그리고 Apache 2.2와 2.4의 접근 제어 구문이 한 파일에 섞인 경우입니다. 마지막에 추가한 블록을 지우고 새로고침해 확인하세요.