툴과 개발 환경12 분 읽기

배포하려던 리포가 라이브보다 한 세대 낡아 있었습니다

정책 문구 한 줄을 고치려고 정적 사이트 배포 스크립트를 부르려던 순간이었습니다. 라이브 파일 178개를 먼저 내려받아 대조했더니 일치가 0개였고, 라이브가 페이지마다 50KB 더 컸습니다. 그대로 올렸으면 170여 페이지에서 그만큼을 지웠을 겁니다.

#deployment#verification#gotchas#reality-check
왼쪽: 리포의 141행 영어 스켈레톤. 오른쪽: 라이브의 563행 다국어 산출물. 리포를 올리면 오른쪽이 왼쪽으로 덮인다.
버전관리가 되고 있다는 사실은 그 파일이 최신이라는 뜻이 아닙니다.
왼쪽은 리포의 141행 영어 스켈레톤, 오른쪽은 라이브의 563행 다국어 산출물. 리포를 올리면 오른쪽이 왼쪽으로 덮인다

당신의 정적 사이트 배포 스크립트는 소스가 최신이라는 것을 어떻게 압니까?

저는 안다고 생각했습니다. 리포에 파일이 있고, 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}";
EOF

mirror --reverse. 파일 하나를 올리는 스크립트가 아니라 트리 전체를 미러링하는 스크립트입니다. --delete가 없으니 원격 파일이 지워지진 않습니다. 그래서 안전해 보입니다. 안전하지 않습니다 — 로컬과 다른 모든 파일이 덮어씌워집니다. 낡은 세대로요.

한 줄 고치려고 이걸 불렀다면 사이트 전체가 v3 이전으로 되돌아갔을 겁니다. 그리고 원래 고치려던 한 줄은 정확히 고쳐졌겠죠.

당신이라면 이 지점에서 어떻게 하겠습니까? 생성기 원본을 찾아 거기서 고치겠습니까, 아니면 라이브를 정본으로 취급하겠습니까?

라이브를 정본으로 삼았습니다

생성기 원본은 이 머신에 없었습니다. 다른 머신에 있을 가능성이 높지만 확인하지 못했습니다. 그래서 이렇게 했습니다.

  1. 고칠 파일의 라이브 사본을 받는다.
  2. 그 사본에 치환을 적용한다. 리포 사본이 아니라.
  3. 되올린다.
  4. 서버에서 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분 자가진단

당신의 정적 사이트에 이 셋을 지금 해보세요.

  1. 라이브에서 아무 페이지 하나를 받아 리포 사본과 바이트 수를 비교하세요. 같습니까?
  2. 배포 스크립트를 열어 mirrorrsync 뒤의 인자를 읽으세요. 한 파일만 올립니까, 트리 전체입니까?
  3. 라이브 HTML을 열어 data-generated 같은 생성기 표지를 찾아보세요. 있다면 그 생성기의 입력이 리포에 있습니까?

버전관리가 되고 있다는 사실은 그 파일이 최신이라는 뜻이 아닙니다. git status가 깨끗하다는 것도요. 저 세 줄 중 하나라도 걸리면, 다음 배포 전에 라이브를 먼저 받아보시길 권합니다.

세 번째 질문에서 뭔가 발견하셨다면 어떤 표지였는지 알려주세요. 생성기가 리포를 우회하는 패턴이 저 혼자만의 일인지 알고 싶습니다.

관련 글