훅은 이름만 맞으면 되는 것이 아닙니다. 요청 하나가 처리되는 동안 워드프레스는 정해진 순서로 세상을 조립하고, 우리 코드는 그 조립이 어디까지 진행된 시점에 끼어드느냐에 따라 전혀 다른 것을 보게 됩니다.
요청 한 번의 조립 순서
대략 이 순서로 진행됩니다. 앞의 단계에서는 뒤의 단계가 만들 것이 아직 없습니다.
plugins_loaded 는 모든 플러그인 파일이 읽힌 직후입니다. 여기서부터 다른 플러그인의 함수와 클래스를 안전하게 부를 수 있습니다. after_setup_theme 는 테마가 자기 기능을 선언하는 자리이고, init 은 등록의 자리입니다 — 포스트 타입 · 택소노미 · 리라이트 · 세션 · 번역이 전부 여기서 확정됩니다.
wp_loaded 는 등록이 모두 끝난 뒤라 “다 갖춰진 상태” 를 전제할 수 있는 첫 지점이고, template_redirect 부터는 쿼리가 이미 실행돼 is_singular() 같은 조건 판정이 의미를 갖습니다.
init 이전에 하면 안 되는 것
번역이 가장 조용하게 실패합니다. load_plugin_textdomain() 을 init 보다 이르게 부르면 로케일이 아직 확정되지 않은 상태라 언어팩이 붙지 않고, 그 뒤의 __() 호출이 전부 원문을 그대로 돌려줍니다. 에러는 없고 화면만 영어입니다 — 그래서 원인을 .mo 파일 경로나 파일명에서 찾게 됩니다. 실제로는 코드가 아니라 시점이 틀린 것입니다.
같은 이유로 클래스 상수나 프로퍼티 기본값에 __() 를 넣을 수 없습니다. PHP 상수 표현식은 함수 호출을 담을 수 없고, 담을 수 있다 해도 그 시점은 파일 로드 시점이라 init 보다 이릅니다. 라벨 표는 상수 배열이 아니라 메서드로 만듭니다.
// ✗ 파일 로드 시점 — 로케일이 아직 없다 (그리고 상수는 함수 호출을 담지 못한다)
const LABELS = [ 'speed' => __( '속도', 'wper' ) ];
// ✓ 호출 시점에 번역한다
public static function labels() {
return [ 'speed' => __( '속도', 'wper' ) ];
}
같은 init 안에서도 순서가 있습니다
init 하나에 등록이 몰리므로, 그 안의 우선순위가 실제로 결과를 바꾸는 경우가 있습니다. 리라이트 규칙은 permastruct 가 등록된 순서대로 쌓이기 때문입니다.
이 저장소에서 실제로 겪은 사례가 그렇습니다. 아카이브 슬러그가 tools 인 커스텀 포스트 타입이 만드는 첨부 파일 규칙이, 같은 경로 아래 중첩된 택소노미 아카이브 /tools/type/{slug}/ 보다 먼저 등록되어 그 주소를 삼켰습니다. 규칙 자체는 양쪽 다 정상이라 리라이트를 다시 흘려도 낫지 않습니다 — 문제는 존재가 아니라 순서였고, 택소노미 등록을 init 우선순위 9(포스트 타입은 10)로 내리는 것이 해결이었습니다.
이런 종류의 사고는 화면이 200을 돌려주면서 조용히 틀리기 때문에 발견이 늦습니다. 점검 습관은 개발 워크플로우 아카이브에 정리돼 있고, 운영 중인 사이트에서 이런 지점을 한 번에 훑고 싶다면 최적화 지원 사업의 진단에 포함돼 있습니다.
다음 회차
시점이 맞았는데도 내 값이 화면에 안 나오는 경우가 있습니다. 누군가 나보다 뒤에서 같은 값을 다시 바꾸고 있기 때문입니다 — 다음 회차는 우선순위 경쟁과 그 한계입니다.