앱스토어 등록 URL을 끊지 않고 WordPress를 정적 사이트로 옮기기
스토어 등록 24건이 WordPress 페이지를 개인정보처리방침 URL로 쓰고 있었다. LiteSpeed에서 등록 URL을 모두 200으로 유지한 채 5분 만에 전환한 기록.
WordPress를 정적 사이트로 옮기는 글은 많지만, 거의 다루지 않는 문제가 하나 있습니다. 그 URL이 앱스토어에 등록돼 있는 경우입니다. 저희 사이트는 App Store와 Google Play 등록 24건이 WordPress 고정 페이지를 개인정보처리방침 URL로 쓰고 있었고, 8건은 WordPress의 "지원" 페이지를 가리키고 있었습니다. 이 중 하나라도 404가 나면 심사에서 반려될 수 있고, 스토어에서 "지원"을 누른 사용자는 오류 화면을 보게 됩니다.
이 글은 Hostinger 공유 호스팅(웹서버 LiteSpeed, 앞단에 Hostinger CDN)에서 tlaboratory.net을 WordPress에서 Python으로 만든 정적 사이트로 옮기면서, 등록된 URL을 하나도 끊지 않고 전환한 기록입니다. 수치는 모두 2026년 11월 8일 운영 서버에서 직접 잰 값입니다.
옮겨야 했던 것
| 항목 | 개수 | 조건 |
|---|---|---|
| WordPress 글 (2021년 강좌, 한글 슬러그) | 67 | 새 URL로 301 |
| WordPress 고정 페이지 | 19 | 그중 13개가 앱 지원 페이지 |
| 이 도메인의 스토어 등록 URL | 16종 | 계속 200을 돌려줄 것 |
| WordPress 개인정보 페이지를 쓰는 스토어 등록 | 24 | 주소는 그대로, 내용만 교체 |
| 같은 문서 루트의 앱 JSON API | 8개 폴더 | 절대 영향을 주지 않을 것 |
조심해야 했던 이유는 마지막 줄입니다. 같은 public_html에는 설치된 앱 16개가 시작할 때마다 호출하는 광고 설정 API도 있었습니다. 루트 .htaccess에 문법 오류가 하나라도 있으면 블로그만이 아니라 도메인 전체가 500이 됩니다.
결정: 리다이렉트하지 말고 "그 자리에서" 보여 주기
옛 URL마다 두 가지 처리 중 하나를 정했습니다.
| 옛 URL 종류 | 처리 | 이유 |
|---|---|---|
| App Store Connect / Play Console에 등록됨 | 내부 리라이트, 200 | 주소 문자열이 영구히 유효. 심사 봇이 리다이렉트에서 걸릴 여지가 없음 |
| WordPress 글·카테고리·피드 | 새 URL로 301 | 일반적인 SEO 이전 |
| 어디에도 등록되지 않은 고정 페이지 | 해당 앱의 새 지원 페이지로 301 | 검색 유입만 처리 |
내부 리라이트는 요청 주소를 바꾸지 않고 다른 파일을 응답합니다.
# 스토어 등록 URL → 같은 주소에서 새 페이지를 200으로
RewriteRule ^개인정보\-처리방침/?$ privacy/index.html [L]
RewriteRule ^technical\-support\-of\-the\-「로또\-당첨정보」/?$ app/kr_lottery/support/index.html [L,NC]
RewriteRule ^HorseRace/support\.html/?$ app/horserace/support/index.html [L]
새로 등록하는 앱은 /app/<id>/support/, /app/<id>/privacy/ 같은 정리된 주소를 쓰고, 기존 등록은 앱마다 다음 업데이트 때 하나씩 바꾸면 됩니다. 모든 앱을 한꺼번에 업데이트할 필요가 없습니다.
LiteSpeed는 한글 URL을 어떻게 비교할까
옛 URL 절반에는 한글이나 일본어가 들어 있었고, 스토어마다 저장된 인코딩도 달랐습니다. %E3%80%8C(대문자)로 등록된 곳도 있고 %e3%80%8c(소문자)도 있었습니다. 어떤 등록은 US%20Lottery%20Infomation처럼 공백과 대문자로 돼 있었는데, WordPress의 실제 슬러그는 us-lottery-infomation이었습니다.
추측하지 않고 운영 서버의 격리 폴더에서 실측했습니다. 스크립트가 /_rwtest_20261108/을 만들고, 그 폴더 안에서만 동작하는 .htaccess를 올린 뒤 요청 15개를 보냅니다. 그리고 finally에서 폴더를 반드시 통째로 지웁니다. 루트 .htaccess는 건드리지 않습니다.
OK 200 GET 한글 소문자 인코딩
OK 200 GET 한글 대문자 인코딩
OK 200 GET 한글 슬래시 없음
OK 200 GET US 대문자+%20 (스토어 등록형)
OK 200 GET US 소문자+하이픈 (WP 슬러그)
OK 200 GET 일본어 「宝くじ当選情報」
OK 301 GET 301 한글 글 주소
...
결과: 15/15 통과
알게 된 것:
- 패턴은 디코딩된 경로와 비교됩니다.
.htaccess에 UTF-8 한글을 그대로 쓴 규칙이 모든 인코딩 형태에 매칭됐습니다. 비교용으로 퍼센트 인코딩으로 쓴 규칙(^%EB%A7%81...)은 한 번도 매칭되지 않았습니다. [NC]로 대소문자는 해결되지만 공백과 하이픈 차이는 해결되지 않습니다. 등록 주소에 공백이 있는 자리에는[\s-]를 생성하도록 했습니다.- 규칙을 생성할 때는 이스케이프가 겹치는 것을 조심하세요. LiteSpeed 자체는
\,\s, 공백이 든 패턴을 따옴표로 감싼 형태를 모두 받아들입니다(나중에 세 가지 모두 200으로 실측). 문제는 생성 스크립트가\\(이중 이스케이프)를 출력한 것이었고, 그대로 배포했다면 iOS 지원 URL이 소리 없이 깨졌을 겁니다. 이스케이프가 몇 겹이 되어도 깨지지 않는\s로 바꿨습니다.
이어서 생성한 루트 .htaccess 전체(규칙 207개)를 격리 폴더용으로 바꿔 한 번 더 올려서, 운영 루트에 넣기 전에 문법 오류가 없음을 확인했습니다.
전환 전에 빠지기 쉬운 함정
1. 파일을 올리는 순간 그 주소의 내용이 바뀝니다. WordPress는 /privacy/를 프론트 컨트롤러로 응답하고 있었습니다. 정적 빌드의 privacy/index.html을 올리는 순간, .htaccess를 바꾸기 전이라도 그 주소는 새 페이지를 보여 주기 시작합니다. 그래서 개인정보처리방침 문안은 "전환 전"이 아니라 "첫 업로드 전"에 확정해야 합니다.
2. 내가 만들지 않은 파일. 배포 스크립트는 자신이 올린 파일 목록(매니페스트)을 갖고 있습니다. dry-run에서 서버의 robots.txt가 우리가 올린 파일이 아니라는 것을 감지했습니다. 그 안에는 다음(Daum) 웹마스터 인증 줄이 있어서, 덮어썼다면 인증이 풀렸을 겁니다. 스크립트는 이런 경우 멈추고, 명시적으로 허용했을 때만 원본을 backup/에 내려받은 뒤 덮어씁니다.
빌드 파일 240개 중 변경 240개
서버에 이미 있는(우리가 올리지 않은) 파일: robots.txt
중단: 덮어쓰려면 --overwrite-foreign 를 명시하세요
전환 타임라인
| 시각 (JST) | 작업 | 결과 |
|---|---|---|
| 23:01 | 사전 점검: 앱용 URL, API 스냅샷 95건, 빌드 검사 | 8/8, 차이 0, 오류 0 |
| 23:02 | 정적 파일 240개 업로드 | robots.txt 백업 |
| 23:06 | 루트 .htaccess 교체 (기존본은 로컬·서버 양쪽에 보관) |
— |
| 23:06 | 앱과 직결된 URL 8개 빠른 점검 | 8/8 |
| 23:07 | 전체 URL 점검: 스토어 URL 17개, 301 표본, 새 페이지, 404 | 53/53 |
| 23:09 | API 회귀 비교 | 실질 차이 0 |
WordPress 파일은 서버에 그대로 남겨 뒀습니다. 새 .htaccess가 index.php로 아무것도 보내지 않을 뿐이라, 롤백은 이전 .htaccess를 되돌리는 명령 한 줄이면 됩니다.
API 비교에서 차이가 4건 나왔지만 모두 전환과는 무관했습니다. 2건은 의도적으로 바꾼 지원 페이지, 1건은 일시적인 타임아웃이었습니다. 나머지 1건은 경마 배당 순위였는데, 배당액이 같은 행의 순서가 요청마다 바뀌는 것이었습니다. 898행의 내용은 동일했고, ORDER BY에 동률을 가르는 기준이 없어서 생긴 일입니다.
전환 후: CDN 캐시
사이트는 Hostinger CDN 뒤에 있습니다. hPanel에서 캐시를 비우기 전까지 일부 URL은 x-hcdn-cache-status: HIT와 함께 옛 WordPress 내용을 돌려줬습니다. 점검 스크립트는 캐시 우회 쿼리를 붙여 요청했기 때문에 통과했던 것입니다. 검증은 쿼리를 붙인 요청과 붙이지 않은 요청 양쪽으로 하세요. 붙이지 않은 결과는 서버가 아니라 CDN을 보고 있는 것입니다.
체크리스트
- 작업 전에 스토어에 등록된 URL을 모두 찾고, URL마다 "그 자리에서 보여 주기 / 301"을 정한다
- 리라이트 동작은 운영 서버의 일회용 하위 폴더에서 실측한다. 생성한 파일 전체도 시험한다
- 변경 전에 모든 API 응답을 저장하고 변경 후 비교한다
- 배포 스크립트에 "내가 만들지 않은 파일은 덮어쓰지 않는다" 제약을 둔다
- 옛 시스템 파일은 한동안 남겨 두고, 롤백은 명령 한 줄로 만든다
- CDN 캐시를 비운 뒤, 캐시 우회 쿼리를 붙인 요청과 붙이지 않은 요청 양쪽으로 다시 확인한다
다음 글에서는 같은 날 밤에 한 나머지 작업을 다룹니다. 설치된 어떤 버전의 앱도 멈추지 않게 하면서 레거시 PHP API 폴더 8개를 /api/v1/ 아래로 옮긴 과정입니다.
자주 묻는 질문
스토어에 등록한 지원 URL을 301로 리다이렉트해도 되나요?
이번에는 피했습니다. 옛 URL은 내부 리라이트로 새 페이지를 돌려주므로 등록된 주소 그대로 200이 나옵니다. 심사나 외부 링크에서는 리다이렉트가 보이지 않고, 스토어 등록 변경은 앱마다 다음 업데이트 때 천천히 하면 됩니다.
LiteSpeed의 RewriteRule은 인코딩된 URL과 디코딩된 URL 중 무엇과 비교하나요?
디코딩된 경로입니다. 한글·일본어를 UTF-8 그대로 쓴 규칙이 소문자·대문자 퍼센트 인코딩과 슬래시 없는 요청에 모두 매칭됐습니다. 퍼센트 인코딩으로 쓴 규칙은 한 번도 매칭되지 않았습니다.
실제 전환에는 얼마나 걸렸나요?
약 5분입니다. 정적 파일 업로드, 루트 .htaccess 교체, 앱용 URL 빠른 점검, 전체 URL과 API 회귀 점검 순서로 진행했습니다.