당신의 정적 사이트 배포 스크립트는 소스가 최신이라는 것을 어떻게 압니까?
저는 안다고 생각했습니다. 리포에 파일이 있고, git이 추적하고 있고, 배포 스크립트도 리포 안에 들어 있었으니까요. 세 가지가 다 참이었고, 그런데도 그 스크립트를 부르면 사이트가 부서졌을 겁니다.
한 줄 고치는 작업이었습니다
앱 개인정보처리방침에서 사실과 다른 문장 하나를 고쳐야 했습니다. 정말 한 줄이었습니다. 리포에서 해당 HTML을 열어 문구를 바꾸고, 같은 문구가 있는 다른 앱 페이지도 같이 고치고, 그러다 연락처와 저작권 표기까지 통일하게 되어서 결국 175개 파일을 만졌습니다. 여기까지는 평범합니다.
배포 직전에 습관 하나가 저를 살렸습니다. 올리기 전에 라이브를 내려받아 대조하는 것.
정확히는 습관이라기보다 이 사이트에 한 번 물린 적이 있어서 생긴 방어였습니다. 예전에 배포 스크립트가 엉뚱한 프로젝트를 가리키고 있던 일이 있었고(그때 이야기), 그 뒤로 이 도메인은 "올리기 전에 받아본다"를 규칙으로 두고 있었습니다.
그때는 목적지가 틀렸습니다. 이번엔 출처가 틀렸습니다.
178개 중 일치 0개
고칠 파일에 해당하는 라이브 사본 178개를 서버에서 tar로 묶어 한 번에 받았습니다. 그리고 각 파일을 리포 HEAD 버전과 바이트 단위로 비교했습니다.
라이브 == repo HEAD : 0개
라이브 != repo HEAD : 178개일치가 하나도 없었습니다. 그리고 차이의 방향이 한쪽으로 쏠려 있었습니다.
flara/privacy.html HEAD 8365 LIVE 58287 (차이 +49922)
cyra/privacy.html HEAD 7826 LIVE 57521 (차이 +49695)
zone2/privacy.html HEAD 4301 LIVE 55156 (차이 +50855)
peritrack/support.html HEAD 1991 LIVE 52720 (차이 +50729)라이브가 페이지마다 48,000~52,000바이트 더 컸습니다. 리포 파일을 그대로 올리면 페이지마다 50KB가 사라집니다. 170여 페이지에서요.
50KB는 무엇이었나
라이브 파일을 열어 보니 완전히 다른 세대의 산출물이었습니다.
| 리포 | 라이브 | |
|---|---|---|
| 행 수 | 141 | 563 |
| 표지 | 없음 | <main data-generated="ootssu-app-v3"> |
| 구성 | 정책 본문만 | SEO 메타 · JSON-LD · 사이트 헤더 · 언어 스위처 · 번역 블롭 |
| 언어 | 영어 | 5개 언어 |
data-generated="ootssu-app-v3". 어느 시점에 생성기가 도입되어 사이트를 한 단계 올려놨고, 리포에는 그 이전 세대가 그대로 남아 있었습니다. 커밋 이력에 "생성기로 갈아탔다"는 흔적이 리포 쪽에는 없습니다. 생성기가 리포를 거치지 않고 라이브로 직접 나갔기 때문입니다.
50KB의 대부분은 window.OOTSSU_I18N 이라는 인라인 번역 블롭이었습니다. 12KB 정도이고 언어별로 62개 키가 들어 있습니다.
여기서 반전이 하나 더 있었습니다
번역 블롭을 발견한 순간 저는 최악을 가정했습니다. 문제 문구가 5개 언어로 복제돼 있으면 5배로 고쳐야 한다.
블롭을 파싱해서 언어별로 문제 단어를 훑었습니다. 결과는 예상과 달랐습니다. 한국어 값 하나만 걸렸고, 그 키의 내용이 이랬습니다.
아래 상세 본문은 앱별 데이터 처리와 지원 정보를 보존한 공식 원문입니다. 상단 안내와 내비게이션은 선택한 언어로 제공됩니다.
번역 블롭은 내비게이션과 요약만 번역합니다. 정책 본문은 영어 원문을 그대로 둡니다. 즉 본문 문구 교정은 영어 한 번이면 끝이었습니다.
이게 왜 중요하냐면, 반대 방향으로 틀렸다면 조용히 망했을 겁니다. 만약 블롭이 본문까지 번역하고 있었다면, 영어만 고치고 "끝났다"고 선언한 뒤 4개 언어에 거짓 문장을 그대로 남겨뒀을 테니까요. 구조를 확인하지 않고 고치면 고친 척이 됩니다.
배포 스크립트를 보지 않고 부를 뻔했습니다
리포 안의 업로드 스크립트는 이렇게 생겼습니다.
lftp -u "${USER},${PASS}" -p "${PORT}" "${PROTOCOL}://${HOST}" <<EOF
mirror --reverse --verbose \
--exclude-glob upload.sh \
--exclude-glob .DS_Store \
"${LOCAL_DIR}" "${REMOTE_PATH}";
EOFmirror --reverse. 파일 하나를 올리는 스크립트가 아니라 트리 전체를 미러링하는 스크립트입니다. --delete가 없으니 원격 파일이 지워지진 않습니다. 그래서 안전해 보입니다. 안전하지 않습니다 — 로컬과 다른 모든 파일이 덮어씌워집니다. 낡은 세대로요.
한 줄 고치려고 이걸 불렀다면 사이트 전체가 v3 이전으로 되돌아갔을 겁니다. 그리고 원래 고치려던 한 줄은 정확히 고쳐졌겠죠.
당신이라면 이 지점에서 어떻게 하겠습니까? 생성기 원본을 찾아 거기서 고치겠습니까, 아니면 라이브를 정본으로 취급하겠습니까?
라이브를 정본으로 삼았습니다
생성기 원본은 이 머신에 없었습니다. 다른 머신에 있을 가능성이 높지만 확인하지 못했습니다. 그래서 이렇게 했습니다.
- 고칠 파일의 라이브 사본을 받는다.
- 그 사본에 치환을 적용한다. 리포 사본이 아니라.
- 되올린다.
- 서버에서 grep으로 판정하고, 실제 브라우저로 한 건 확인한다.
3번에서 또 하나 물렸습니다. 파일마다 scp를 연속으로 부르니 8개쯤에서 Connection closed가 뜨고, 잠깐 SSH 자체가 거부됐습니다. 호스팅이 연속 접속을 스로틀링한 것입니다. 단일 세션 tar 파이프로 바꿨더니 한 번에 끝났습니다.
tar czf - -C <로컬> a.html b.html c.html \
| ssh <원격> 'tar xzf - -C ~/public_html'4번의 "실제 브라우저"는 선택이 아니었습니다. 이 도메인은 앞단에 CDN이 있어서 비브라우저 요청에 챌린지 403을 줍니다. curl로는 배포가 됐는지 판정할 수 없습니다. 서버에서 파일을 직접 grep해 내용을 확인하고, 브라우저로 한 페이지를 열어 CDN이 낡은 사본을 주고 있지 않은지 확인하는 두 단계가 필요했습니다.
솔직한 부분
이 배포는 다음 생성기 실행에 덮일 수 있습니다. 저는 라이브 산출물을 직접 편집했고, 생성기의 입력은 고치지 못했습니다. 원본을 찾으면 같은 문구를 거기서도 고쳐야 합니다. 그때까지 이 수정은 임시입니다.
그리고 리포는 여전히 낡아 있습니다. 이번에 커밋한 것은 리포를 라이브 관례에 맞춘 문자열 치환분이고, 세대 차이는 그대로입니다. 즉 다음 사람이 같은 함정을 밟을 수 있습니다. 리포 안 스크립트가 여전히 트리 전체를 미러링하는 것도 그대로입니다. 이건 고쳐야 할 항목으로 남았습니다.
3분 자가진단
당신의 정적 사이트에 이 셋을 지금 해보세요.
- 라이브에서 아무 페이지 하나를 받아 리포 사본과 바이트 수를 비교하세요. 같습니까?
- 배포 스크립트를 열어
mirror나rsync뒤의 인자를 읽으세요. 한 파일만 올립니까, 트리 전체입니까? - 라이브 HTML을 열어
data-generated같은 생성기 표지를 찾아보세요. 있다면 그 생성기의 입력이 리포에 있습니까?
버전관리가 되고 있다는 사실은 그 파일이 최신이라는 뜻이 아닙니다. git status가 깨끗하다는 것도요. 저 세 줄 중 하나라도 걸리면, 다음 배포 전에 라이브를 먼저 받아보시길 권합니다.
세 번째 질문에서 뭔가 발견하셨다면 어떤 표지였는지 알려주세요. 생성기가 리포를 우회하는 패턴이 저 혼자만의 일인지 알고 싶습니다.