조합 홈페이지 호스팅과 캐시
「배포됐다」 다음에 사람이 여는 그 주소가 바뀌었는가
도메인 회수와 사이트 직접 관리와 도메인과 네임서버를 잇는 글입니다. 협동조합과 의료사협과 교육기관의 홈페이지를 정적 호스팅으로 옮기고 운영하면서 넘어진 자리를 모았습니다. 조직 이름과 도메인 실물과 업체 이름은 지웠습니다.
요지
정적 호스팅에서는 「배포에 성공했다」와 「사람이 여는 주소가 바뀌었다」가 다른 질문입니다. 그 사이에 커스텀 도메인 연결, 엣지 캐시, 남의 서버가 끼어 있기 때문입니다. 판정은 사람이 실제로 여는 주소에서, 배포한 파일에만 있는 문자열로 합니다. 조직을 증명하는 정보는 푸터가 아니라 첫 화면에 두고, 떠나는 업체의 사이트에서는 관리자 계정부터 확보합니다.
1. Git 연동으로 시작한다
클라우드플레어 페이지에서 깃허브 저장소와 연동된 프로젝트는 API로 만들 수 없었습니다. API는 「내부 오류」처럼 보이는 응답을 돌려줬는데, 실제 원인은 깃허브 쪽에 연동 앱이 설치되지 않은 것이었습니다. 그 앱 설치는 대시보드에서 사람이 한 번 눌러야 합니다.
- 막혔다고 파일을 직접 올리는 방식으로 프로젝트를 만들면 나중에 Git 연동으로 바꿀 수 없습니다. 프로젝트를 지우고 다시 만들어야 하고, 그 전에 도메인부터 떼야 삭제가 됩니다. 막히면 우회하지 말고 사람의 클릭 한 번을 받습니다.
- 연동 앱을 조직 계정에 설치할 때 기본값은 「모든 저장소」이고, 지금과 앞으로의 모든 저장소에 읽기와 쓰기 권한을 줍니다. 남의 조직 계정이라면 그 홈페이지 저장소 하나로 좁힙니다.
- 루트 도메인은 그 도메인의 DNS 존이 클라우드플레어에 있어야 붙습니다. 지금 주소가 루트인 사이트를 옮기려면 네임서버 이전을 피할 수 없으니, 계획의 앞쪽에 둡니다.
- 서브도메인은 바깥 DNS로도 붙지만, 페이지 프로젝트에 도메인을 먼저 등록하지 않고 DNS만 넣으면 522가 납니다. 루트만 등록하고 www를 빼먹어도 www는 522가 나므로 www도 따로 등록합니다.
2. 커스텀 도메인이 DNS보다 먼저다
페이지에 커스텀 도메인이 「활성」으로 붙어 있는 동안에는 DNS를 다른 서버로 바꿔도 엣지가 계속 페이지로 보냅니다. 라우팅은 커스텀 도메인의 활성 상태가 먼저이고 DNS는 그다음이기 때문입니다. 한 의료사협 사이트에서 원래 서버로 되돌리는 연습을 두 번 했는데 두 번의 결과가 반대로 나왔습니다. 현장 기록에 따르면 원인은 조작이 아니라 관찰한 시각이었습니다. DNS가 페이지를 가리키지 않은 채 1분에서 2분이 지나면 클라우드플레어가 그 도메인을 스스로 비활성으로 내리는데, 첫 연습은 그 뒤를 봤고 둘째 연습은 그 전을 봤습니다.
- 페이지 도메인이 붙은 호스트의 서버를 바꿀 때는 DNS와 커스텀 도메인 해제를 한 쌍으로 다룹니다.
- 다시 페이지로 돌아올 때 도메인을 새로 등록하면 「이미 추가됨」만 돌아오고 살아나지 않습니다. 등록이 아니라 재검증을 호출해야 1초쯤 뒤에 활성이 됩니다. 이 절을 빠뜨린 탓에 그 사이트는 31초에서 65초 동안 522로 죽었습니다.
- 도메인을 다른 계정의 프로젝트로 옮길 때는 옛 프로젝트에서 떼고, 새 프로젝트에 붙이고, DNS 레코드를 새 프로젝트 주소로 직접 고치고, 재검증을 호출합니다. 그 이름에 옛 레코드가 남아 있으면 클라우드플레어가 덮어 쓰지 않기 때문에 셋째 단계를 빼면 대기 상태에 머뭅니다.
- 상태는 대기에서 오류를 잠깐 스쳐 활성으로 갑니다. 한 번 읽고 단정하지 말고 몇 초 간격으로 읽은 최종값으로 판정합니다.
- 페이지 프로젝트에 파일을 올리기 전에 그 프로젝트의 도메인 목록에 바꾸려는 호스트가 들어 있는지 봅니다. 한 교육기관 사이트에서 우리 대장에는 그 프로젝트가 서빙한다고 적혀 있었지만, 실제로는 그 뒤에 서빙 자리가 한 번 더 옮겨져 그 호스트가 목록에 없었으니 올려도 바뀌지 않았을 일입니다.
서빙하는 곳은 DNS 레코드와 응답 헤더와 그 기계에서 실제로 기다리는 프로그램으로 판정합니다. 라이브 파일과 저장소 파일의 내용이 같다는 것은 누가 그것을 내주는지 말해 주지 않습니다. 같은 저장소를 다른 기계가 서빙해도 내용은 같기 때문입니다.
3. 도메인 사이 이동은 작은 워커로
정적 호스팅의 리다이렉트 설정 파일은 경로 기준 규칙만 받고, 도메인 기준 규칙은 받지 않습니다. 옛 주소와 www를 새 도메인으로 모으려고 설정 파일에 도메인 기준 규칙을 넣었더니 배포는 성공했는데 규칙은 아무 말 없이 무시됐습니다. 우리 기록에 따르면 이 사실은 석 달 남짓 앞서 한 번 적어 둔 것이었는데, 그것을 다시 찾아내느라 같은 길을 한 번 더 돌았습니다.
- 도메인 사이의 301 이동은 정적 사이트에 붙이는 작은 워커 스크립트로 처리합니다. 저장소 안에 있어서 버전 관리가 되고, 토큰 권한에도 기대지 않습니다.
- 이동 규칙은 새 도메인이 라이브로 확인된 뒤에 겁니다. 먼저 걸면 새 도메인이 아직 서지 않은 채로 손님을 그쪽으로 보내서 사이트가 열리지 않습니다.
- 사이트가 이상하면 파일을 고치기 전에 DNS부터 봐서 서빙 구조를 확인합니다. 정적 호스팅인 줄 알고 설정 파일을 두 번 고쳐 배포했는데, 그 서브도메인은 터널 뒤의 작은 서버 프로그램이 내주고 있었고 원인은 그 프로그램의 폴더 설정이었습니다.
- 새 서브도메인을 제안하기 전에는 그 이름이 이미 살아 있는지 확인합니다. 그럴듯한 이름이라 비어 있을 줄 알았던 자리에 운영 중인 서비스가 있었고, 배포 담당 AI가 DNS 중복 거부에서 멈춘 덕분에 그 서비스가 무사했습니다. 성격이 다른 시스템은 다른 도메인에 둡니다.
깃허브 페이지를 쓴다면 하나 더 있습니다. 직전 빌드가 도는 중에 새 커밋을 올리면 새 빌드가 걸리지 않을 수 있습니다. 「빌드 완료」가 찍혀도 커밋 해시가 옛것일 수 있고, 한 협동조합 사이트는 그 때문에 8분을 기다리는 동안 내려가 있었습니다. 판정은 빌드 상태가 아니라 커밋 해시로 하고, 옛것이면 빌드를 강제로 겁니다.
4. 캐시: 사람이 보는 그 주소가 바뀌었나
배포 직후에 옛 본문이 200으로 나오는 일과, 없는 주소에도 첫 화면이 200으로 나오는 일은 「끝났다」를 무엇으로 판정하나의 2절에 있습니다. 여기서는 캐시 설정과 검증 방법에서 넘어진 자리를 적습니다.
HTML까지 오래 캐시하면 새 글이 나왔다 사라졌다 합니다. 우리 위키에 새 노드를 올릴 때마다 404가 났다 안 났다 했는데, 원인은 모든 경로에 한 시간 캐시를 거는 설정이 HTML에도 걸려 있었던 것입니다. HTML은 요청마다 원본에 다시 확인하도록 캐시 시간을 0으로 두고, 오래 캐시하는 것은 이름에 버전이 붙은 정적 파일로 한정합니다.
캐시를 건너뛰는 쿼리를 붙인 확인은 라이브 검증이 아닙니다. 안내 화면의 이미지를 바꾸고 주소 뒤에 버전 쿼리를 붙여 200과 크기를 확인한 다음 「반영 완료」라고 보고했는데, 사람이 연 쿼리 없는 주소는 옛 이미지 그대로였습니다. 쿼리를 붙이면 캐시 열쇠가 달라져서 원본을 새로 받아 오는 우회로가 됩니다. 원본을 확인하는 데는 쓸모가 있지만, 사람이 보는 주소를 확인한 것은 아닙니다.
- 판정은 사람과 같은 주소로 합니다.
- 정적 파일을 바꾸면 그 파일을 부르는 HTML 쪽의 주소에 버전을 올려 강제로 갱신합니다.
- 우리가 잰 바로는 터널 뒤에서 서버가 직접 내주는 사이트도 자바스크립트와 스타일 파일과 이미지는 엣지가 네 시간 캐시했습니다.
도메인 배포를 200으로만 검증하면 남의 앱을 통과시킵니다. 한 협동조합의 홈페이지를 새로 만들어 도메인에 연결했다고 기록하고 「전수 검증 통과」라고 적은 일이 있었습니다. 다음 날 재 보니 그 도메인은 한 번도 옮겨진 적이 없었고, 예전부터 돌던 예약 앱이 잘 돌고 있어서 200이 났던 것이고, 두 사이트의 제목까지 비슷해서 눈으로는 가려지지 않았습니다. 그 잘못된 기록 때문에 다음 날 배포 담당 AI가 라이브 예약 서비스를 내릴 뻔했습니다.
- 누가 내주는지 응답 헤더의 서버 이름을 봅니다.
- 어디를 가리키는지 네임서버를 봅니다.
- 배포한 파일에만 있는 문자열이 라이브 본문에 있는지 봅니다.
- 「검증 통과」라고 적을 때는 무엇을 어떤 명령으로 봤는지 함께 적어서, 다음 사람이 그 판정을 다시 해 보거나 의심할 수 있게 합니다.
5. 도메인과 조직을 잇는 증거는 첫 화면에
우리 기록에 따르면 비영리 기관용 업무 도구를 신청했는데 「조직과 도메인의 관계를 확인할 수 없다」는 이유로 거절됐습니다. 푸터에 사업자등록번호를 넣어 다시 냈지만 회신이 없었고, 9일 뒤에 재 보니 문제는 푸터가 아니었습니다. 심사하는 사람이 여는 첫 화면, 브라우저 탭 제목, 링크 미리보기가 모두 다른 브랜드 이름이었습니다. 사람은 첫 화면을 보고, 기계는 문서 머리의 메타 정보와 구조화 데이터를 보며, 둘 다 푸터는 보지 않습니다.
- 첫 화면에 조직의 정식 명칭과 식별 번호를 스크롤하지 않아도 보이게 둡니다.
- 문서 제목과 설명, 링크 미리보기용 제목과 사이트 이름을 조직 이름으로 맞춥니다.
- 조직 정보를 담은 구조화 데이터를 넣고, 그 도메인의 메일 주소를 페이지에 드러냅니다.
- 판정은 화면 좌표로 해서, 넓은 화면과 휴대폰 화면 모두에서 조직 이름과 식별 번호가 첫 화면 높이 안에 있어야 합격입니다.
그 과정에서 클라우드플레어의 이메일 가리기 기능이 페이지 소스에서 메일 주소를 지우고 있었다는 것도 드러났습니다. 사람 눈에는 스크립트로 되살아나 보이지만, 소스를 읽는 쪽에는 보이지 않습니다. 메일 링크 바깥까지 이 기능을 끄는 주석 표지로 감싸고, 감싸지 않은 다른 페이지는 여전히 가려지는지 같이 재서 기능이 꺼진 것인지 표지가 먹은 것인지 가립니다.
거절 사유를 글자 그대로 받아 서류를 더 내기 전에, 심사하는 사람이 보는 화면을 그대로 열어 무엇이 안 보였는지 재는 것이 먼저입니다.
6. AI가 우리 사이트를 읽게 하려면
우리 사이트의 robots.txt에 AI 크롤러를 허용한다고 적어 두었는데, 클라우드플레어가 그 파일 맨 위에 AI 크롤러 12종을 막는 블록을 스스로 끼워 넣고 있었습니다. 대시보드의 크롤러 통계에서는 한 AI 검색 크롤러의 하루 요청 60건이 모두 실패로 잡혀 있었습니다.
클라우드플레어에는 「AI 봇 차단」과 「관리형 robots.txt」라는 이름이 비슷한 두 기능이 따로 있고, 하나는 요청 단계에서 막고 다른 하나는 파일에 차단 문구를 넣습니다. AI가 우리 사이트를 읽게 하려면 둘 다 끄고, AI 크롤러 이름으로 robots.txt를 요청해 끼워 넣은 블록이 사라졌는지 확인합니다.
이미지는 웹용 압축 형식으로 바꿔 올립니다. 우리 홈페이지 저장소는 81메가바이트 가운데 80메가바이트가 원본 사진이었는데, 바꾸고 나니 사진 한 장이 95% 가까이 줄었고 저장소 전체는 7메가바이트가 됐습니다. 페이지에서 실제로 부르는 사진은 23장 가운데 7장뿐이어서, 부르지 않는 원본은 배포 저장소에서 뺐습니다.
7. 가입 확인 메일과 카톡 인앱 브라우저
클라우드 데이터베이스의 기본 가입 확인 메일은 믿지 않습니다. 한 가입 신청 화면에서 최근 신청자 세 명이 모두 메일 확인을 못 해 로그인이 막혀 있었는데, 네이버와 한메일과 지메일 사용자가 한 명씩이었습니다. 기본 발송 서버는 시간당 보낼 수 있는 양이 적고 국내 메일은 스팸 필터에 걸리기 쉬웠습니다. 관리자가 신청을 승인할 때 메일 확인도 함께 처리하게 고쳤는데, 승인은 관리자가 신원을 직접 확인하는 일이라 확인 메일보다 강한 검증입니다. 「가입했는데 로그인이 안 된다」는 연락을 받으면 비밀번호보다 메일 확인 여부부터 봅니다.
카톡과 문자로 링크를 뿌리는 시스템은 신원을 브라우저 저장소에만 두지 않습니다. 현장 기록에 따르면 한 협동조합의 출퇴근 기록 시스템에서 직원 25명 전원이 하루 동안 한 번도 출퇴근을 찍지 못했습니다. 개인 링크를 카톡에서 열면 카톡 안의 브라우저에 신원이 저장되는데, 벽의 QR을 휴대폰 카메라로 찍으면 기본 브라우저가 열리고 두 브라우저는 저장소를 나누지 않습니다. 개발자는 한 브라우저 안에서만 시험하니 늘 잘 됐습니다.
- 일이 개인 링크 페이지 안에서 끝나게 합니다. 출퇴근 버튼을 그 페이지에 넣고, 각자 자기 링크를 홈 화면에 저장하게 했습니다.
- 신원이 없을 때 막다른 화면을 띄우지 않고, 그 자리에서 간단한 본인 확인으로 다시 발급합니다.
- 관리자 화면이 만드는 링크의 주소가 실제로 쓰는 정본 주소와 같은지 봅니다. 관리자 화면을 정적 호스팅의 기본 주소로 열고 링크를 만들면 QR과 주소가 갈립니다.
- 「등록했는데 등록이 안 됐다고 나온다」는 증상을 들으면 서버를 보기 전에 두 가지를 묻습니다. 링크를 어디서 열었는지, QR을 무엇으로 찍었는지입니다.
8. 떠나는 업체의 사이트에서 자산 되찾기
외주 사이트를 넘겨받는 순서는 도메인 회수와 사이트 직접 관리의 4절에 있습니다. 여기서는 업체 형태에 따라 달랐던 점을 적습니다.
임대형 홈페이지는 관리자 로그인 없이는 자산을 못 건집니다. 현장 기록에 따르면 한 협동조합이 임대형 홈페이지를 해지하기 전에 사이트 전체를 내려받았더니 파일 39개가 왔는데, 로고와 배경과 스타일 파일뿐이고 본문과 게시판 글과 사진 원본과 첨부는 0개였습니다. 임대형 빌더는 내용을 스크립트로 불러오기 때문에 내려받기 도구가 따라갈 링크가 없습니다.
- 해지 건이 생기면 관리자 계정부터 확보합니다. 도메인은 만료일까지 살지만 임대 서버의 내용은 해지하는 즉시 지워집니다.
- 그래도 전체 내려받기는 돌려 둡니다. 5분이면 로고 같은 브랜드 자산은 건집니다.
- 내려받은 첫 화면의 스크립트를 읽어 폼이 어디로 데이터를 보내는지 봅니다. 그 조합의 「출자 내역 확인」 버튼은 다른 업체의 업무 시스템으로 요청을 보내고 있었고, 조합원 데이터는 홈페이지가 아니라 그 시스템에 있었습니다. 해지할 대상과 계속 연동할 대상을 섞으면 사고가 납니다.
개발 업체가 떠난 공유 호스팅에서 유일한 데이터베이스를 되찾을 때는 화면의 엑셀 내보내기가 아니라 호스팅 관리 화면의 데이터베이스 백업을 씁니다. 한 의료사협 기록에 따르면 그 업체의 백업 파일은 만든 다음 날 자동으로 지워지고 하루 횟수 제한이 있었습니다.
- 파일 전송, 데이터베이스, 관리 화면의 비밀번호는 서로 따로입니다. 파일 전송 비밀번호만 새로 정하면 웹 프로그램과 데이터베이스에는 영향이 없습니다.
- 오래된 서버는 암호화된 파일 전송이 꺼져 있을 수 있고, 로그인 실패를 되풀이하면 접속 주소가 막힐 수 있으니 간격을 넉넉히 둡니다.
- 되찾은 옛 소스의 데이터베이스 접속 설정 파일에 데이터베이스 비밀번호가 들어 있는 경우가 있습니다.
- 한글 파일 이름이 옛 인코딩이면 목록을 받는 단계에서 깨지니 인코딩을 지정해서 받습니다.
- 받은 자료에는 실명과 건강 정보가 들어 있어서 저장소 밖에 두고 소유자만 읽게 권한을 잠그며, 원본 쪽에서는 아무것도 지우거나 바꾸지 않습니다.
우리 요청이 남의 원서버를 눕혔는지 가를 때는 셋을 봅니다. TCP 연결이 붙는지 보고, 붙으면 프로그램은 살아 있는 것입니다. 다른 회선에서도 안 되는지 보고, 안 되면 우리 주소가 막힌 것이 아닙니다. 같은 업체의 다른 IP는 되는지 보고, 되면 업체 전체 장애가 아닙니다. 세 가지가 다 그렇다면 과부하이고, 계속 찌르면 회복을 막습니다. 남의 서버에 거는 상한은 남의 서버를 두드릴 때에 있습니다.
도구 메모
- 누가 서빙하나: 응답 헤더의 서버 이름, 네임서버, 배포 파일에만 있는 문자열을 함께 본다
- 캐시 확인: 응답 헤더의 캐시 상태를 보고, 판정은 쿼리 없는 실제 주소로 한다
- 페이지 도메인 복구: 등록이 아니라 재검증을 호출하고, 상태는 몇 초 간격으로 읽은 최종값으로 판정한다
- 깃허브 페이지: 빌드 목록의 맨 위 항목 커밋 해시를 방금 올린 커밋과 비교한다