본문으로 건너뛰기
설정

직렬화 데이터를 깨뜨리지 않고 WordPress 데이터베이스 URL 일괄 변경하기

위젯, 메뉴, 테마 설정이 초기화되지 않게 WordPress 사이트 주소를 데이터베이스에서 바꾸는 방법. 직렬화 데이터를 그대로 지켜주는 WP-CLI 명령을 씁니다.

게시됨

WordPress 데이터베이스 전체에 UPDATE ... SET column = REPLACE(column, 'oldurl', 'newurl') 같은 단순 쿼리를 돌리지 마세요. 글자는 정확히 바뀌지만 그와 동시에 설정이 망가집니다. WordPress는 배열과 객체를 PHP 직렬화 문자열로 저장하고, 이 형식은 포함된 모든 문자열의 바이트 길이를 함께 기록하기 때문입니다. 단순 SQL 치환은 글자만 고치고 기록된 길이는 건드리지 않아서, 그 값은 읽을 수 없는 상태가 됩니다.

대신 직렬화를 이해하는 도구를 쓰세요. 명령줄에서는 wp search-replace입니다. 데이터베이스를 먼저 백업하고, dry run으로 확인한 다음 실제로 실행합니다.

실제로 무엇이 깨지는가

PHP 직렬화 문자열은 이렇게 생겼습니다.

s:18:"http://example.com"

여기서 18은 뒤따르는 글자의 바이트 길이입니다. 세어 보세요. http://example.com은 정확히 18자입니다.

이제 저장된 테마 설정을 하나 떠올려 봅시다.

a:1:{s:4:"logo";s:18:"http://example.com";}

http://https://로 단순 SQL 치환하면 이렇게 되어야 할 것 같습니다.

a:1:{s:4:"logo";s:19:"https://example.com";}

그런데 그렇게 되지 않습니다. 실제로는 이렇게 됩니다.

a:1:{s:4:"logo";s:18:"https://example.com";}

문자열은 이제 19바이트인데 여전히 18이라고 주장하고 있습니다. PHP는 선언된 길이를 읽고 18바이트 앞으로 이동한 뒤, 있어야 할 자리에서 닫는 따옴표와 세미콜론을 찾지 못하고 포기합니다. 배열 전체가 역직렬화에 실패합니다.

왜 아무 소리 없이 실패하는가

이 부분이 이 버그를 비싸게 만듭니다. WordPress는 오류를 띄우고 멈추지 않습니다. 옵션을 읽을 때 값을 역직렬화 헬퍼에 통과시키는데, 잘못된 데이터에 대해 PHP가 false를 반환하면 WordPress는 옵션이 애초에 없었던 것처럼 동작하는 값을 코드에 넘깁니다. 그러면 플러그인이나 테마가 기본값으로 되돌아갑니다.

그래서 증상은 오류 화면이 아닙니다. 증상은 이렇습니다.

  • 사이드바에서 위젯이 사라짐
  • 사용자 정의하기 설정이 테마 기본값으로 초기화됨
  • 페이지 빌더의 레이아웃 데이터가 빈 값으로 돌아와 페이지가 텅 비어 보임
  • 플러그인 설정 화면이 방금 설치한 것처럼 보임
  • 메뉴가 지정된 위치를 잃어버림

관리자 화면 어디에도 문제가 있다는 표시가 없습니다. 행은 여전히 데이터베이스에 있고, 겉보기에 멀쩡한 글자로 가득 차 있습니다. 어긋난 것은 길이 접두사 한두 바이트뿐입니다. 역직렬화가 실패하면 PHP가 알림을 내보내기는 하지만, 오류 표시가 꺼져 있는 운영 사이트에서는 아무도 그걸 보지 못합니다. WP_DEBUG를 켜고 WP_DEBUG_LOG를 활성화한 뒤 WP_DEBUG_DISPLAY를 false로 두면, 그 알림이 읽을 수 있는 로그 파일에 쌓입니다.

피해가 고르게 퍼지는 것도 아닙니다. 단순한 스칼라 값, 즉 siteurl, home, 일반 글 본문, 자기 컬럼에 그대로 들어 있는 URL 같은 것들은 무식한 치환에도 멀쩡히 살아남습니다. 깨지는 건 직렬화된 값뿐입니다. 잘못된 치환이 처음에는 잘 된 것처럼 보이는 이유가 여기 있습니다.

무엇이든 손대기 전에 wp-config.php부터 확인

wp-config.phpWP_HOME이나 WP_SITEURL을 정의하고 있다면, 그 상수가 데이터베이스 값을 이깁니다. wp_options를 아무리 바꿔도 전혀 반영되지 않은 것처럼 보입니다. 데이터베이스가 문제라고 결론 내리기 전에 이런 줄이 있는지 먼저 찾아보세요.

define( 'WP_HOME', 'http://example.com' );
define( 'WP_SITEURL', 'http://example.com' );

새 URL로 고치거나, 아예 지우고 데이터베이스가 주도하게 두면 됩니다.

백업, 진심으로 하세요

직렬화가 깨지면 되돌리기가 늘 쉬운 것은 아닙니다. 길이 접두사와 내용이 어긋나는 순간, 깨진 값만 보고 원래 값을 복원할 수는 없습니다. 원본 문자열을 모르면 올바른 길이가 얼마였는지 알 방법이 없기 때문입니다. 백업만이 유일한 실행 취소입니다.

wp db export backup-before-replace.sql

WP-CLI를 쓸 수 없다면 mysqldump로 하세요.

mysqldump -u USER -p DBNAME > backup-before-replace.sql

가능하면 서버 밖에 보관하세요. 웹 루트에 놓인 백업은 복구 지점이자 보안 문제입니다.

안전한 명령

WP-CLI의 search-replace는 값을 하나씩 역직렬화하고, 해독된 구조를 순회하며 그 안에서 치환한 뒤, 올바른 길이로 다시 직렬화합니다. 먼저 dry run입니다.

wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid

보고서를 읽으세요. 테이블과 컬럼별로 몇 건이 치환될지 알려줍니다. 테이블 목록이 이상하거나, 처음 보는 테이블에 수천 건이 잡힌다면 멈추고 확인한 뒤에 실제 실행으로 넘어가세요.

그다음 실행합니다.

wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only

플래그에 대해 솔직하게 짚어보면 이렇습니다.

  • --all-tables는 WordPress가 직접 등록하지 않은 테이블까지 포함합니다. 플러그인 데이터가 흔히 그런 곳에 있습니다. 이 옵션이 없으면 일부 플러그인 테이블은 통째로 건너뜁니다.
  • --skip-columns=guid는 중요합니다. guid는 살아 있는 URL이 아니라 피드 리더용 영구 식별자입니다. 이걸 바꾸면 피드 구독자에게 전체 보관 글이 새 글로 보일 수 있습니다.
  • 끝의 슬래시는 빼고, 건드리고 싶지 않은 프로토콜 변형도 포함하지 말고 검색하세요. example.com만 덩그러니 치환하면 이메일 주소와 본문 속 언급까지 전부 걸립니다.

명령줄을 쓸 수 없다면, 직렬화를 인식하는 마이그레이션 플러그인이 관리자 화면에서 같은 해독 작업을 해 줍니다. 찾아야 할 동작은 “직렬화 데이터를 처리한다”고 명시되어 있는지 여부입니다. 그렇게 적혀 있지 않은 도구라면 처리하지 않는다고 보면 됩니다.

어떤 테이블과 컬럼이 관련되는가

테이블컬럼직렬화 위험도
wp_optionsoption_value높음 — 위젯, 테마 설정, 트랜지언트, 대부분의 플러그인 설정
wp_postmetameta_value높음 — 페이지 빌더 레이아웃, 사용자 정의 필드 배열
wp_usermetameta_value높음 — 권한, 화면 및 알림판 환경설정
wp_termmetameta_value보통 — 플러그인에 따라 다름
wp_postspost_content, guid본문은 낮음, 다만 숏코드 속성과 블록 마크업에 URL이 들어 있음
wp_commentscomment_content, comment_author_url낮음
wp_linkslink_url, link_image낮음, 대개 쓰지 않음

멀티사이트라면 wp_blogs, wp_site, wp_sitemeta와 사이트별 wp_2_options, wp_3_options 등을 추가하세요. 멀티사이트 URL 변경에는 검색 치환 외에 별도의 규칙이 더 있으니, 아예 다른 작업으로 다루는 편이 낫습니다.

search-replace로도 완전히 해결되지 않는 경우

한계를 솔직하게 적어둡니다.

데이터베이스 안에 저장된 JSON. 일부 페이지 빌더는 레이아웃 데이터를 JSON으로 저장하면서 슬래시를 이스케이프합니다. 그래서 URL이 https://example.com이 아니라 https:\/\/example.com 형태로 들어 있습니다. 일반 형태로 검색하면 이건 놓칩니다. 이스케이프된 문자열에 대해 한 번 더 돌려야 할 수 있습니다. 복사본에서 먼저 테스트하세요.

Base64로 인코딩된 값. 플러그인이 설정을 인코딩해서 저장한다면, 어떤 텍스트 기반 치환도 그 URL을 볼 수 없습니다. 그런 설정은 대개 해당 플러그인 화면에서 다시 입력해야 합니다.

파일에 하드코딩된 URL. functions.php, 자식 테마 템플릿, 하드코딩된 에셋 경로에 적힌 것은 데이터베이스에 없으므로 전혀 바뀌지 않습니다. 테마 디렉터리는 따로 grep 하세요.

캐시된 출력과 생성된 결과물. 오브젝트 캐시, 페이지 캐시, CDN 사본은 치환이 제대로 끝난 뒤에도 예전 URL을 계속 내보냅니다. 치환이 실패했다고 단정하기 전에 캐시를 비우고 다시 확인하세요.

이미 잘못된 쿼리를 돌렸다면

백업을 복원하세요. 그게 답입니다. 대부분 통하지도 않는 복구 절차를 늘어놓기보다 이렇게 분명히 말하는 편이 낫습니다.

백업이 없다면 되살릴 수 있는 길은 좁습니다. 애초에 직렬화되지 않았던 값은 멀쩡하고, 깨진 직렬화 값은 손으로 재구성하거나 해당 설정 화면을 다시 저장해서 초기화하는 수밖에 없습니다. 위젯 영역을 다시 저장하고, 메뉴 위치를 다시 지정하고, 플러그인 설정을 다시 입력하면 깨진 값 위에 올바르게 직렬화된 새 값이 덮어써집니다. 번거롭지만 통합니다. 그리고 손상된 행을 그대로 남겨 다음 사람을 헷갈리게 하는 것보다는 낫습니다.

무엇이든 실행하기 전에 올바른 구문을 만들어 보고 싶다면 WordPress 검색 치환 SQL 도구로 생성해 보세요. 어떤 컬럼이 단순 SQL로 처리해도 안전하고 어떤 컬럼이 직렬화를 인식하는 처리를 요구하는지 보여주므로, 명령을 확정하기 전에 그 경계를 눈으로 확인할 수 있습니다.

FAQ

질문

단순 SQL REPLACE 문이 왜 WordPress 사이트를 망가뜨리나요?

WordPress는 배열과 객체를 PHP 직렬화 문자열로 저장하는데, 이 형식은 안에 들어 있는 모든 문자열의 바이트 길이를 함께 기록합니다. 단순 REPLACE는 글자만 바꾸고 예전 길이 숫자는 그대로 남겨둡니다. 그러면 PHP가 값을 역직렬화하지 못하고, WordPress는 기본값으로 되돌아가며, 설정은 틀린 값이 아니라 아예 빈 값처럼 보입니다.

데이터베이스에서 WordPress 사이트 URL을 바꾸는 가장 안전한 방법은 무엇인가요?

먼저 데이터베이스 전체를 백업한 뒤, WP-CLI의 wp search-replace 명령을 dry run으로 한 번 돌려보고 실제로 실행합니다. WP-CLI는 값을 하나씩 역직렬화해서 해독된 구조 안에서 치환한 뒤, 길이를 다시 계산해 직렬화합니다. 명령줄을 쓸 수 없다면 직렬화를 인식하는 마이그레이션 플러그인이 관리자 화면에서 같은 일을 해 줍니다.

예전 URL은 어느 테이블에 남아 있나요?

주로 wp_options, wp_posts, wp_postmeta, wp_usermeta, wp_termmeta, wp_comments입니다. 직렬화 위험은 wp_options와 모든 meta_value 컬럼에 몰려 있습니다. 위젯, 테마 설정, 트랜지언트, 페이지 빌더 데이터가 전부 배열로 저장되기 때문입니다. 멀티사이트라면 wp_blogs, wp_site, wp_sitemeta에도 도메인이 들어 있습니다.

wp-config.php에 URL이 정의되어 있어도 데이터베이스만 바꾸면 되나요?

아닙니다. wp-config.php가 WP_HOME이나 WP_SITEURL을 정의하고 있으면, 그 상수가 wp_options의 siteurl과 home 값을 덮어씁니다. 상수를 고치거나 지우기 전까지는 데이터베이스를 바꿔도 화면에는 아무 변화가 없습니다. 실패한 마이그레이션의 상당수가 이 한 줄 때문이니 wp-config.php부터 확인하세요.

guid 컬럼도 같이 바꿔야 하나요?

아닙니다. guid는 동작하는 URL이 아니라 피드 리더용 영구 식별자입니다. 이걸 다시 쓰면 피드 구독자에게 예전 글이 전부 새 글로 보일 수 있습니다. WP-CLI를 돌릴 때 skip-columns에 guid를 넣으세요. 예외는 공개 피드나 구독자가 아예 없던 사이트 정도입니다.

잘못 치환한 값이 깨졌는지 어떻게 알 수 있나요?

깨진 직렬화 값은 데이터베이스에서는 멀쩡해 보이지만 WordPress에서는 빈 값으로 돌아옵니다. 위젯이 사라지고, 테마 사용자 정의 설정이 초기화되고, 플러그인 옵션이 오류 메시지 하나 없이 기본값으로 돌아갑니다. WP_DEBUG와 WP_DEBUG_LOG를 켜면 역직렬화 알림이 로그에 남습니다. 선언된 문자열 길이와 실제 바이트 수를 비교하면 바로 확인됩니다.