flush_rewrite_rules() 함수에 대해

플러그인/테마 개발시 flush_rewrite_rules()라는 함수를 심심찮게 쓴다. 안 하면 사이트에서 404 에러가 날 수 있어 항상 코드에 집어 넣는 나름 중요한 역할을 하는 녀석이다. 그런데 이 함수가 하는 역할이 정확히 무엇인지 알고 있는가? 이 포스트에서 이 함수에 대해 좀 더 정확히 알아보고 기록해본다.

주소 다시쓰기는 고유주소(permalink)와 관련이 깊다. 각 포스트의 주소를 설정할 때 사이트 관리자는 원하는 형태로 주소의 구조를 설정할 수 있다.

예를 들어 포스트의 이름(슬러그)이 ‘sample-post’라면, ‘https://blog.changwoo.pe.kr/sample-post/’로 URL이 되든지, 아니면 전혀 다른 방법으로 경로를 지정 가능하다. 아무래도 https://blog.changwoo.pe.kr/index.php?p=123 같이 물음표 뒤에 쿼리 문자열을 붙이는 것보다는 훨씬 직관적인 주소를 가지게 된다.

그런데 index.php 같은 PHP 스크립트 파일은 서버에 실제 존재하기 때문에 동작하지만, ‘/sample-post/’ 같은 경로는 서버에 동작하지 않는다. URL 다시쓰기가 일어나기 때문이다.

다시쓰기는 두 가지 요소가 맞물려 동작한다. 워드프레스가 설치된 디렉토리에 숨겨져 있는 .htaccess 파일과 워드프레스의 옵션 테이블에 기록된 다시쓰기 설정 기록이다. 포스트에서는 편의를 위해 아파치 서버를 기준으로 설명하므로 .htaccess 파일만을 이야기한다. 엔진엑스는 .htaccess를 사용하지 않고 별도의 설정을 사용한다. 그러나 엔진엑스 또한 유사한 규칙을 내부에 적용한다.

.htaccess 동작

워드프레스가 설치된 .htaccess 파일에서는 공통적으로 다음과 같은 코드가 발견된다. (싱글 사이트 기준)

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress

# BEGIN WordPress ~ # END WordPress 사이에는 코드를 붙이지 않는 것이 좋다. 워드프레스 코드는 .htaccess 파일을 직접 편집하는 기능이 있다. 이 때 이 영역을 인식해서 다른 부분은 건드리지 않고 이 영역만 새롭게 편집하도록 동작한다. 아무튼 이 코드의 뜻은 이렇다.

  1. 다시쓰기 기능을 켠다.
  2. 현재 디렉토리를 기준으로 다시쓰기를 진행한다.
  3. index.php 파일이 그대로 오면 그대로 둔다.
  4. 그러나 요청받은 경로가 파일이나 디렉토리로 존재하지 않는다면, 그 경로는 무시하고 index.php에서 응답을 처리하도록 한다.

Rewrite Rules 동작

옵션 테이블에는 ‘rewrite_rules’라는 다소 크기가 큰 배열값이 존재한다. 아래는 그 배열의 일부를 발췌한 것이다.

Array
 (
     [^wp-json/?$] => index.php?rest_route=/
     [^wp-json/(.*)?] => index.php?rest_route=/$matches[1]
     ....
     [([^/]+)(?:/([0-9]+))?/?$] => index.php?name=$matches[1]&page=$matches[2]
     ....
)Code language: PHP (php)

배열의 키는 URL 패턴 정규식, 값은 해당 정규식이 매칭되었을 때 URL을 어떻게 풀어 쓸지를 기술한 것이다. 예를 들어 ‘https://blog.changwoo.pe.kr/sample-post/’라는 URL이 입력되면 blog.changwoo.pe.kr 이라는 서버의 /sample-post/ 라는 경로로 응답을 요청한다. 이 때 워드프레스는 rewrite_rules 규칙에 따라 이 경로가 저 배열의 어떤 패턴과 일치하는지 대조한다.

이 때 /sample-post/는 위 예에서 세번째에 있는 ‘([^/]+)(?:/([0-9]+))?/?$’과 매칭된다. 그래서 ‘/sample-post/’는 사실상 ‘index.php?name=sample-post&page=’ 와 동일한 결과가 나온다. 실제로 이런 주소를 넣으면 워드프레스가 /sample-post/로 다시 리다이렉트를 시키기는 하지만 말이다.

한편 WP 클래스의 parse_request() 메소드는 요청을 받았을 때 주어진 경로를 분석해 어떤 응답을 주어야 할지 결정하는 역할을 한다. 분석을 하는 것은 WP::parse_request() 이고, 자료는 옵션 테이블에 기록된 rewrite_rules 에서 가져온다.

name이라는 파라미터는 포스트의 이름을 찾을 때 사용한다. 그러므로 포스트 이름이 ‘saple-post’인 단일 포스트를 쿼리할 것이고, 단일 포스트이므로 singlular 타입의 템플릿에서 출력을 담당할 것이다. 이 일련의 과정을 WP 클래스에서 미리 준비한다.

다시쓰기 규칙은 코드에 고정되어 있지 않고 데이터베이스에서 수정이 가능하므로 동적으로 여러 규칙을 추가할 수 있다. 이 때 이 규칙을 업데이트하는 역할이 바로 flush_rewrite_rules() 함수의 역할이다.

Hard flush vs Soft flush

flush_rewrite_rules() 함수는 불리언 인수를 선택적으로 넣을 수 있다. 이것이 true (기본값)이면 하드 플러시(hard flush)가 일어나며, false이면 소프트 플러시(soft flush)가 일어난다.

우선 워드프레스에서 가장 손쉽게 플러시를 일으키는 방법을 언급하고 싶다. 가장 간단한 방법은 관리자 페이지 설정 > 고유주소 (permalink) /wp-admin/options-permalink.php 로 접속하는 것이다. 거기서 굳이 ‘저장’ 버튼을 누르지 않아도 된다. 그냥 접속하는 것만으로 플러시가 일어난다. 소스 코드를 열어 보라. 헤더가 나오기 전에 먼저 flush_rewrite_rules()를 호출하고 있는 것을 확인할 수 있다.

소프트 플러시는 데이터베이스를 업데이트하는 것만으로 끝난다. 옵션 테이블의 ‘rewrite_rules’ 레코드에 다시 쓰고 작업을 마친다. 반면 하드 플러시는 데이터베이스 업데이트와 더불어 .htaccess 를 편집한다. 아까 Rewrite Rules 동작에서 언급한 부분을 .htaccess 파일에 집어넣는 과정까지 더한다.

다시 말해, 다시 쓰기는 웹서버의 .htaccess에서 미리 고정시킨 설정과 워드프레스 데이터베이스에서 지정한 동적인 설정이 같이 맞물려야 제대로 동작한다. 가끔 워드프레스 호스팅을 바꾸고 메인 페이지나 관리자는 접속되는데 메뉴를 눌러 나오는 다른 페이지는 404 웹 서버 에러가 떠서 당황하는 경우가 있을 것이다. 99.9% .htaccess 파일 백업이 누락된 경우다. 이 때는 아까 말한 것 처럼 관리자 페이지 설정 > 고유주소에 한 번 들어가 주는 것만으로 해결될 수 있다. .htaccess 파일을 워드프레스가 재생성하기 때문이다.

Hard flush의 진짜 의미

사실 대부분의 경우 하드 플러시는 앞서의 예처럼 .htaccess가 누락된 조금 극단적인 경우가 아닌 이상 굳이 할 이유가 없어 보인다. 어차피 코어가 적는 .htaccess는 위에서 적은 것처럼 거의 동일하다. 그리고 세밀한 다시쓰기는 그냥 데이터베이스 레코드로 저장되어 있다. 그래서 한 번 제대로 .htaccess 파일을 작성하면 끝이다. 왜 굳이 하드 플러시가 기본값인가? 굳이 하드 플러시를 써야 할 이유가 있을까? 안정성 때문일까? 어쩌다 .htaccess 파일이 삭제될 수도 있으니까?

이 포스트를 작성하려는 진짜 의도를 이제 풀어나가고자 한다. 우리는 지금까지 다시쓰기로 풀어낸 결과를 index.php로만 국한시켜 보고 있었다. 그러나 꼭 다시쓰기가 index.php로만 귀결되라는 법이 있을까?

이런 예를 들어 보자. 어떤 웹사이트를 제작하면서 어떤 REST API를 작업하고 외부로 공개하고자 한다. 이 때 경로는 /api/v1/foo/bar 처럼 좀 더 엔드포인트를 쓰고 싶다.

아마도 보통은 REST API 같은 요청은 /wp-admin/admin-ajax.php에서 JSON 기반으로 통신하고 싶을 확률이 높다. 그런데, 이렇게 하면 /wp-admin/admin-ajax.php 라는 경로는 앞서 말한 API 엔드포인트로는 쓰기 부적합하다.

이것을 add_rewrite_rules()를 이용해 처리할 수 있다. /api/v1/foo/bar 같은 URL이 들어오는 경우라면 /wp-admin/admin-ajax.php?action=api-v1&foo=bar 같은 식으로 다시 쓰기를 지정할 수 있겠다. 개발자가 직접 .htaccess 파일을 편집해도 되지만, add_rewrite_rules()를 써서 처리하는 것이 더 안정적인 방법일 것이다.

명심해야 할 것은 소프트 플러시로 DB에 기록되는 것은 index.php를 위시한 규칙 밖에 적용되지 않는다는 사실이다. index.php이외의 경로로 다시쓰기 규칙을 등록하면, 이것은 소프트 플러시로는 해결되지 않는다. 그러므로 위 예와 같은 다시쓰기는 반드사 하드 플러시를 통해 .htaccess 파일로 규칙을 흘려 보내야 한다. 그러면 코어가 생성하는 # BEGIN WordPress ~ # END WordPress 블록 내부에 적용한 다시쓰기가 기록된다.

가령 아래처럼 소스코드를 작성하면,

<?php
add_action( 'init', function() {
	add_rewrite_rule(
		'api/v1/foo/bar',
		'wp-admin/admin-ajax.php?action=api-v1&foo=bar',
		'top'
	);
	flush_rewrite_rules();
});

.htaccess는 이렇게 기록된다. 중간에 RewriteRule로 API 엔드포인트에 대한 다시쓰기 규칙이 설정된다.

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteRule ^api/v1/foo/bar /wp-admin/admin-ajax.php?action=api-v1&foo=bar [QSA,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>Code language: HTML, XML (xml)

간단한 다시 쓰기라면 add_rewrite_rule()과 flush_rewrite_rules()로 관리하는 것이 더 좋을 수 있다. 경로 변경에 따른 보정이 flush_rewrite_rules() 함수 호출에 대해 자동으로 대응이 된다는 장점이 있으니까. 그러나 서버 플랫폼에 종속적인 면이 있므로 고려해야 한다. 예를 들어 엔진엑스는 .htaccess 파일을 사용하지 않으니까 이런 규칙은 무용지물이 된다.

댓글 남기기