다국어 페이지를 만들 때 대부분 포스트를 언어별로 복제하거나 무거운 플러그인부터 찾는다. 포스트는 원본 1개만 두고 서버 라우팅과 번역 팩으로 언어별 URL을 열어주는 구조를 실제 적용 사례로 정리했다.
다국어 지원, 다들 포스트부터 늘리려는 이유
워드프레스에서 여러 언어로 콘텐츠를 열 때 흔히 떠올리는 방법은 세 가지다. 언어별로 포스트를 따로 발행하거나, 멀티사이트(설치 하나로 여러 사이트를 운영하는 기능)로 쪼개거나, WPML 같은 다국어 플러그인을 설치하는 것이다.
포스트 복제는 가장 손쉬워 보이지만 원본 글 하나를 고칠 때마다 언어 수만큼 똑같은 수정을 반복해야 한다. 6개 언어를 지원하면 문구 하나 바꾸는 데 6번 손이 간다.
멀티사이트나 다국어 플러그인은 관리 구조는 나아지지만 그만큼 무거워진다. 멀티사이트는 사이트마다 별도 데이터베이스 테이블을 두고, 플러그인은 번역 UI와 데이터 구조를 새로 얹어 종속성이 남는다.
원본은 하나, 언어별 URL은 서버가 열어준다
실제로 적용한 방식은 포스트를 언어마다 만들지 않는 것이다. 원본 콘텐츠는 1개만 두고, ▲URL 설계 ▲서버 라우팅 ▲번역 팩 오버레이 ▲SEO 신호 ▲클라이언트 전환까지 다섯 구성 요소로 같은 글을 여러 언어 주소에서 열리게 만든다.
URL 설계는 단순하다. 기준 언어인 한국어는 프리픽스 없이 그대로 두고, 다른 언어는 짧은 국제 언어 코드를 앞에 붙인다. 원본이 tali.kr/slug/ 라면 일본어판은 tali.kr/ja/slug/, 스페인어판은 tali.kr/es/slug/ 식이다.
- 기준 언어는 프리픽스 없이 유지해 기존 URL과 색인을 그대로 보존한다
- 각 언어는 국제 표준 언어 코드 하나만 짧게 붙인다
- 슬러그가 한글처럼 아스키(ASCII, 영문 알파벳과 숫자로만 이뤄진 기본 문자 집합) 밖 문자면 영어 슬러그로 바꾸고 기존 주소는 301로 넘긴다
이 주소를 실제로 열어주는 건 워드프레스의 리라이트(rewrite, 들어온 요청 주소를 다른 규칙으로 다시 해석하는 기능) 규칙이다. 공식 문서에 따르면 add_rewrite_rule()은 URL 패턴을 쿼리 변수로 바꿔준다. 개념 수준으로 줄이면 이렇게 등록한다.
add_rewrite_rule(
'^(ja|es|pt|id|zh)/([^/]+)/?$',
'index.php?route_lang=$matches[1]&name=$matches[2]',
'top'
);
add_filter('query_vars', function ($vars) {
$vars[] = 'route_lang';
return $vars;
});
이 규칙은 워드프레스가 항상 켜두고 실행하는 머스트유즈 플러그인(mu-plugin) 한 곳에만 등록해서 관리한다. 여러 파일에 흩어지면 언어 코드가 겹치거나 순서가 꼬여 특정 언어만 404가 나기 쉽다.
번역은 복제가 아니라 팩으로 입힌다
언어 코드가 잡히면 그 다음은 콘텐츠를 어떻게 보여줄지다. 원본 글의 로직과 수치는 그대로 두고, 텍스트만 언어별 번역 팩으로 갈아 끼우는 구조를 쓴다.
번역 팩은 성격이 다른 두 종류로 나눈다. 소개문이나 안내 문구처럼 고정된 서술은 정적 HTML 팩으로 두고, 점수나 순위처럼 로직에 쓰이는 값과 얽힌 라벨은 JSON(자바스크립트에서 널리 쓰는 데이터 표현 형식) 팩으로 분리한다.
이렇게 나누는 이유는 명확하다. 계산식이나 실제 수치는 언어와 무관하게 하나만 존재해야 하고, 화면에 붙는 문구만 언어별로 바뀌어야 한다. 팩을 섞어두면 번역자가 실수로 숫자를 건드리거나, 로직 수정자가 특정 언어의 문구를 깨뜨리는 사고가 난다.
| 구분 | 담당 범위 | 언어별로 바뀌는 것 | 전 언어가 공유하는 것 |
|---|---|---|---|
| 본문 번역 팩(정적 HTML) | 소개문, 안내 문구 | 텍스트 전체 | 카드 레이아웃, 이미지 배치 |
| 데이터 번역 팩(JSON) | 라벨, 버튼 문구 | 문구 텍스트만 | 점수 계산, 실제 수치 |
| 공용 로직(스크립트) | 화면 동작, 인터랙션 | 없음 | 인터랙션 전체 |
운영에서 가장 자주 걸리는 문제는 번역 팩 결측이다. 새 언어를 추가했는데 특정 문구의 팩만 빠지면 그 자리에 원본 언어 텍스트가 그대로 노출된다. 발행 전 팩 목록을 원본과 자동 대조하는 감사 단계를 두면 이 사고를 미리 잡아낸다.
구글이 언어별 페이지로 인식하게 만드는 법
주소를 열었다고 검색엔진이 자동으로 언어별 독립 페이지로 인식하는 건 아니다. 구글 서치 센트럴 문서는 콘텐츠가 실제로 번역돼 있으면 언어별 버전은 중복 콘텐츠가 아니라고 안내한다. 다만 그 사실을 hreflang(언어, 지역판 페이지의 관계를 알려주는 태그)으로 명시해야 한다.
hreflang은 페이지마다 자신을 포함한 모든 언어 버전을 서로 링크해야 한다. 한 언어가 다른 언어를 가리키면 그 반대쪽도 똑같이 되돌아 가리켜야 하고, 어떤 언어에도 안 맞는 방문자를 위한 x-default도 함께 넣는다.
<link rel="alternate" hreflang="ko" href="https://example.com/slug/" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/slug/" />
<link rel="alternate" hreflang="es" href="https://example.com/es/slug/" />
<link rel="alternate" hreflang="x-default" href="https://example.com/slug/" />
canonical(검색엔진에게 대표 주소를 알려주는 태그)은 언어마다 자기 자신을 가리키게 한다. 한국어판이 일본어판을 canonical로 잡으면 일본어 페이지가 검색 결과에서 사라진다. 언어별 사이트맵을 제출해두면 색인 속도도 빨라진다.
리로드 없이 언어를 넘기는 절충안
언어 버튼을 누를 때마다 페이지를 통째로 새로고침하면 스크롤 위치와 화면 상태가 날아간다. 그래서 클릭하면 화면 텍스트만 즉시 바꾸고, 주소창은 history.replaceState(브라우저 기록을 새로 남기지 않고 현재 주소만 바꾸는 기능)로 조용히 맞춰주는 절충안을 쓴다.
function switchLanguage(lang) {
document.querySelectorAll('[data-i18n]').forEach(function (el) {
var key = el.getAttribute('data-i18n');
el.textContent = translations[lang][key] || el.textContent;
});
var base = location.pathname.replace(/^\/(ja|es|pt|id|zh)/, '');
var newPath = lang === 'ko' ? base : '/' + lang + base;
history.replaceState(null, '', newPath || '/');
}
서버 라우팅으로 처음 들어온 언어별 주소는 검색엔진과 새로고침 양쪽에서 정확히 동작하고, 이후 화면 안에서 언어를 바꾸는 동작은 리로드 없이 매끄럽게 처리된다. 검색엔진에는 정확한 주소, 사용자에게는 끊김 없는 전환을 동시에 준 구조다.
실제로 적용한 뒤 게임, 심리테스트 페이지 수백 개에 6개 언어 프리픽스를 붙였는데 포스트 복제는 0건이다. 원본 1곳만 고치면 언어 전체에 반영되고, 캐시도 URL 단위로 그대로 걸린다.
자주 묻는 질문 FAQ
Q1) 워드프레스 멀티사이트보다 이 방식이 항상 나은가
아니다. 언어마다 운영진과 도메인이 완전히 다르면 멀티사이트가 더 맞다. 콘텐츠가 같고 언어만 다르면 라우팅 방식이 유리하다.
Q2) 언어가 늘어나면 서버 부담이 커지나
라우팅 자체는 정규표현식 매칭 한 번이라 부담이 거의 없다. 번역 팩도 텍스트 파일이라 캐시만 잘 걸리면 언어가 늘어도 응답 속도 영향은 적다.
Q3) 기존에 이미 발행한 한글 글도 이 구조로 옮길 수 있나
가능하다. 기준 언어 주소는 그대로 두고 라우팅 규칙과 번역 팩만 추가하면 되므로, 기존 글의 URL과 색인을 건드리지 않는다.