Technote

개발 워크플로우 실무

연재 WP-CLI 자동화 레시피 8부 중 5부

멱등한 시드 스크립트 — 두 번 돌려도 사본이 생기지 않게

시드는 한 번만 도는 스크립트가 아닙니다. 조회를 잘못 쓰면 검사는 통과하고 삽입은 실행되어, 워드프레스가 조용히 -2 를 붙인 사본을 만듭니다.

시드 스크립트는 페이지 · 텀 · 설정 같은 기본 구조를 코드로 만들어 두는 스크립트입니다. 그런데 시드는 한 번만 도는 스크립트가 아닙니다. 개발하면서 열 번, 스테이징에서 한 번, 새 환경을 세울 때 또 한 번 돕니다. 그때마다 결과가 같아야 합니다 — 그것이 멱등입니다.

구조는 단순합니다. 만들기 전에 찾고, 있으면 건너뜁니다. 함정은 전부 “찾는” 쪽에 있습니다.

⚠ post_status => any 는 draft 를 놓친다

가장 흔한 형태부터 봅니다. 슬러그로 페이지가 있는지 확인하는 코드입니다.

// 이 조회는 초안 페이지를 찾지 못한다
$found = get_posts( [
    'name'        => 'pricing',
    'post_type'   => 'page',
    'post_status' => 'any',
] );

이름으로 하는 조회는 단일 글 조회 경로로 잡혀 공개 상태만 봅니다. 그래서 초안으로 만들어 둔 페이지가 있어도 결과는 0건입니다.

증상이 고약합니다. “없으니 만들자” 로 이어져 삽입이 실행되고, 워드프레스는 슬러그 충돌을 피하려고 조용히 -2 를 붙입니다. 에러도 경고도 없고, 스크립트 로그에는 “생성 완료” 가 찍힙니다. 나중에 /pricing/ 이 /pricing-2/ 로 넘어가는 것을 보고서야 알게 됩니다.

해결은 상태를 전부 명시하는 것입니다.

'post_status' => [ 'publish', 'future', 'draft', 'pending', 'private', 'trash' ],

목록에서 빠뜨리기 쉬운 둘이 특히 중요합니다. future 를 빼면 예약된 글마다 사본이 생기고, trash 를 빼면 운영자가 일부러 버린 글을 재실행이 되살립니다. 휴지통에 있다는 것은 “없다” 가 아니라 “치웠다” 는 뜻입니다.

조회 설계 — 위 세 줄이면 재실행이 사본을 만들지 않는다

안정 키를 심고, 저장된 결과를 되읽는다

슬러그 조회에는 근본적인 약점이 있습니다. 슬러그가 한 번이라도 밀리면 다음 실행부터는 영영 못 찾습니다. 그래서 1차 조회는 우리가 직접 심은 키로 합니다.

update_post_meta( $id, '_seed_key', 'pricing:ko' );

이 키는 제목이 바뀌어도, 슬러그가 밀려도, 상태가 바뀌어도 그대로입니다. 슬러그 조회는 2차 안전망으로만 둡니다.

그리고 삽입 직후에 실제로 저장된 슬러그를 되읽습니다. 요청한 것과 다르면 경고를 냅니다 — 조용한 -2 를 소리나게 만드는 유일한 방법입니다.

if ( get_post_field( 'post_name', $id ) !== $slug ) {
    WP_CLI::warning( "슬러그가 밀렸다: {$slug}" );
}

kses — 성공 로그와 함께 삽화가 사라진다

앞 회차에서 예고한 문제입니다. WP-CLI 는 사용자 0 으로 돌고, 그 상태에는 unfiltered_html 권한이 없습니다. 그래서 저장 경로의 kses 필터가 허용 태그 목록에 없는 태그를 걷어냅니다.

본문에 인라인 <svg> 삽화가 들어 있다면 그림만 통째로 사라진 채 저장됩니다. 삽입 자체는 성공하고, 반환값도 정상 ID 이고, 로그도 “생성 완료” 입니다. 화면을 열어 보기 전까지 아무도 모릅니다.

저장소가 소유한 신뢰 가능한 콘텐츠에 한해 필터를 내립니다.

kses_remove_filters();

사용자 입력에는 절대 쓰지 않습니다. 이 한 줄이 정당한 경우는 “우리가 저장소에 커밋해 둔 마크업을 그대로 넣는다” 뿐입니다.

날짜 — 갱신에서 GMT 가 남는다

글에는 로컬 날짜(post_date)와 GMT 날짜(post_date_gmt) 두 컬럼이 있습니다. 삽입에서는 로컬 날짜만 줘도 코어가 GMT 를 유도해 채웁니다. 갱신에서는 그렇지 않습니다 — 넘기지 않으면 옛 GMT 값이 그대로 남습니다.

이것이 왜 문제인가 하면, 예약 발행을 실제로 집행하는 크론은 GMT 컬럼을 기준으로 시각을 판단하기 때문입니다. 로컬 날짜만 고쳐 놓으면 목록에는 새 날짜가 보이는데 발행은 옛 시각에 일어납니다. 그래서 삽입에서도 습관적으로 명시하는 편이 낫습니다.

'post_date'     => $date,
'post_date_gmt' => get_gmt_from_date( $date ),

이 크론이 왜 정시에 안 도는지는 7회차에서 따로 다룹니다.

시드는 지우지 않는다

마지막 원칙입니다. 시드는 만들거나 건너뛸 뿐이고, 삭제는 별도 인자나 별도 명령으로 분리합니다. 공유 데이터베이스에서 파괴적 동작이 기본값이 되는 순간, 그 스크립트는 아무도 안심하고 돌릴 수 없게 됩니다.

같은 원칙이 적용된 실제 절차는 작업 과정에 공개돼 있고, 관련 글은 개발 워크플로우 아카이브에 있습니다.

다음 회차

스크립트가 인자를 셋 넘게 받기 시작하면 승격할 때입니다. 다음 회차는 플러그인에 자체 WP-CLI 명령을 붙이는 방법이고, 핵심은 log · warning · error 를 구별해서 쓰는 일입니다.

이 주제의 다른 글

노하우 목록으로

개발 워크플로우 심화

배포 전 디자인 QA 체크리스트

배포 후에 발견하는 디자인 문제의 대부분은 배포 전에 순서대로 확인하면 잡힙니다. 규칙 · 상태 · 실기기의 세 단계로 정리했습니다.

디자이너 3분 읽기

개발 워크플로우 심화

되돌릴 계획 없이 배포하지 않습니다

롤백은 버튼 하나가 아니라 코드와 데이터베이스 두 갈래이고, 되돌리는 순서가 있습니다. 그 순서를 배포 전에 적어 두는 것까지가 준비입니다.

디자이너 5분 읽기

₩270,000 · 신청하기