전체 글

티스토리 자동 미러링을 뺐다 — 세션 만료와 래퍼 타임아웃이 겹친 6일

xXfacebook페이스북

2026년 9월 11일 19시 42분, 티스토리 자동 미러링을 통째로 뺀 커밋이 들어갔습니다. 파일 24개에서 1,124줄이 지워지고 48줄이 더해졌습니다(커밋 443849b). 이 기능은 8월 11일부터 하루 한 편씩 돌아왔습니다. 그런데 같은 날 오후 5시 51분에는 같은 래퍼를 고치는 커밋(1728c3c)이 먼저 들어갔습니다. 고치는 판정과 빼는 판정이 약 두 시간 간격으로 같은 날 완결됐습니다.

실패는 두 겹이었습니다. 첫 겹은 9월 6일부터 시작된 카카오 세션 만료이고 둘째 겹은 9월 10일과 11일에 확인된 래퍼 타임아웃입니다. 나란히 놓고 보면 첫 실패는 조용하지 않았습니다. 만료를 알리는 설계가 있었고 큐에 사람 로그인 대기 기록까지 남았습니다. 조용했던 곳은 다른 두 지점입니다. 발행이 계속 성공해서 겉은 멀쩡했다는 것, 그리고 둘째 실패가 첫 실패를 알리는 유일한 통로를 삼켰다는 것입니다.

티스토리 미러링이 어떤 자리였나

왜 공식 API를 안 썼는지부터 적어 둡니다. 티스토리 공식 Open API는 2024년 2월까지 순차 종료됐습니다. 종료 순서는 파일 첨부, 글 관련 기능, 댓글 관련 기능, 그 외 기능 순이라고 티스토리 API 문서에 남아 있고(2026년 9월 12일 직접 열람) 종료 공지는 notice.tistory.com/2664 였습니다. 글 올리기가 끊긴 뒤라 자동 미러링은 공식 길이 없는 상태에서 만들어진 기능이었습니다.

운영 규칙은 넷이었습니다. 하루 한 편만 미러링한다, 발행과 다른 시각인 14시 크론에 랜덤 지연을 얹는다, 중복 판정은 frontmatter의 tistoryUrl 값으로 한다, 실패해도 원본은 그대로 두고 세션이 만료된 날만 알린다. FLOW.md에는 "2026-08-11 부터 에디터 내부 API 로 하루 한 편 자동 미러링했다"고 남아 있습니다. 8월 25일에는 cron 래퍼를 버전 관리로 옮겼고(커밋 f7ad758) 제거 뒤 남은 hermes/README.md 45행에도 blog-tistory.sh를 미러링과 함께 뺐다는 기록이 있습니다. 마지막 규칙이 이 글의 열쇠입니다. 원본이 그대로라서 미러링이 죽어도 발행은 계속되고, 알림은 만료 사실을 전하는 유일한 통로가 됩니다.

2026년 8월 11일 티스토리 미러링 도입부터 9월 11일 제거까지의 세로 타임라인. 8월 25일 래퍼 버전 관리 이관, 9월 6일 카카오 세션 만료 시작, 9월 10일과 11일 래퍼 타임아웃으로 알림 실패, 같은 날 17시 51분 지연 상한 수정을 거쳐 19시 42분 제거로 끝나는 흐름을 보여주는 그림

도입에서 제거까지 한 달. 고침과 제거는 같은 날 약 두 시간 간격으로 완결됐다.

첫 겹 — 세션이 만료된 뒤에도 발행은 계속됐다

카카오 세션은 9월 6일부터 만료됐습니다. 1728c3c 커밋 메시지에는 "실제로 9/6 부터 만료, 지금도 TISTORY_EXPIRED"라고 적혀 있고 FLOW.md 561행, CLAUDE.md 210행, pipeline/queue/money.md 146행과 272행(제거 시점 커밋 443849b 기준)까지 네 곳이 같은 사실을 적고 있었습니다. 복구는 사람이 카카오 2단계 인증을 통과해야만 가능해서 자동화가 건너갈 수 없는 선이었습니다.

여기까지는 설계대로였습니다. 큐에는 9월 10일 글과 9월 11일 글 두 항목에 "티스토리 미발행(카카오 세션 만료 — 사람 로그인 대기)"라고 남아 있었습니다. 실패가 기록으로 남고 무엇을 해야 하는지까지 적혀 있었으니 첫 실패는 조용한 실패가 아니었습니다.

문제는 그 기록이 밖으로 잘 보이지 않았다는 것입니다. 원본 발행은 매일 성공했으니 겉으로는 아무 일도 없었습니다. 1728c3c가 남긴 점검 문구가 그 상태를 그대로 담고 있습니다. "9/9~11 사흘치를 점검하니 발행은 매일 됐는데 주변 넷이 멈춰 있었다."

둘째 겹 — 알림을 보내는 코드가 알림을 삼켰다

9월 10일과 11일에는 이 고장을 알릴 마지막 통로까지 막혔습니다. 래퍼가 "자다가 죽어" 세션 만료 알림조차 나가지 못한 것입니다. 원인은 두 수의 격차입니다. 래퍼는 시작하면 RANDOM으로 최대 90분을 잠갔고(수정 전 34행, sleep $((RANDOM % 5400))) hermes의 script_timeout_seconds는 900초였습니다. 여섯 배 차이라 오래 자는 날에는 잠에서 깨기 전에 한도에 걸려 죽습니다. 이 관계는 pipeline/hermes-timeouts.test.ts 914행 주석, CLAUDE.md 433434행, FLOW.md 561~562행에 각각 남아 있습니다.

두 실패를 겹쳐 놓으면 구조가 보입니다. 첫 실패는 설계대로 큐에 기록을 남겼는데 그 기록을 사람에게 옮길 방법은 알림뿐이었고 알림을 보내는 코드가 둘째 실패로 죽어 있었습니다. 알림이 알림을 삼켰습니다. 고장을 알리는 경로가 하나뿐인 시스템에서는 그 경로가 죽는 것이 곧 두 번째 고장이 됩니다.

9월 11일 17시 51분의 첫 판정은 둘째 겹을 겨냥한 것이었습니다. 지연 상한을 환경변수로 빼 08분으로 줄이고(1728c3c) 같은 커밋에서 hermes 타임아웃 검사를 보강했습니다. 1분 뒤인 17시 52분에는 FLOW.md를 "티스토리 지연 090분 → 0~8분 (hermes 900초 한도 안)"으로 갱신했습니다(커밋 88707d2). 여기까지는 고치는 방향이었습니다.

# 수정 전 (래퍼 34행) — 0~90분. hermes 900초 한도의 여섯 배
sleep $((RANDOM % 5400))
# 수정 후 (1728c3c) — 0~8분. 상한을 환경변수로 뺐다
sleep $((RANDOM % ${BLOG_TISTORY_JITTER_MAX:-480}))

카카오 세션 만료가 큐에 사람 로그인 대기 기록을 남기고, 그 기록을 알림으로 옮기는 유일한 경로를 래퍼 타임아웃이 끊는 구조를 그린 다이어그램. 발행 파이프라인은 옆에서 계속 성공해 겉으로는 멀쩡해 보이는 대비를 함께 보여주는 그림

첫 실패는 기록을 남겼다. 기록을 옮기는 통로가 둘째 실패로 막혔다.

고친 지 약 두 시간 만에 통째로 뺀 판단

같은 날 19시 42분, 방향이 고치기에서 빼기로 바뀌었습니다. 커밋 443849b는 운영자 결정으로 미러링을 통째로 제거했고 24개 파일에서 1,124줄이 지워졌습니다. 커밋 메시지가 남긴 이유는 셋입니다. 세션이 9월 6일부터 만료됐고 복구에 사람의 2단계 인증이 필요하다는 것, 래퍼가 hermes 900초 제한에 걸려 알림조차 나가지 않는다는 것, 그 비용에 비해 얻는 것이 nofollow 유입 한 줄기라는 것입니다.

빠졌는지는 코드로 확인했습니다.

자리 제거 뒤 상태
pipeline/ 아래 tistory.ts(405행)·mirror.ts(111행)·mirror.test.ts(52행)·lib/tistory-api.ts(173행)·lib/render-html.ts(87행) 전부 삭제
package.json 스크립트 tistory·mirror 0건
daily.sh media·mirror·tistory 언급 0건
notify.ts·message.ts 본체 tistory 0건
hermes/ 래퍼 8개 + sync.sh + README blog-tistory.sh만 부재

특히 pipeline/lib/message.test.ts 226~235행에는 이 제거를 고정하는 회귀 테스트가 들어갔습니다. 테스트 이름은 "티스토리 안내는 싣지 않는다 — 미러링을 뺐다 (2026-09-11)"이고 옛 호출부가 tistory를 넘겨도 알림에 그 섹션이 생기면 안 된다는 검사입니다. 근거는 짧고 정확합니다. 없는 일을 사람에게 시키게 된다. 빠진 기능의 안내문이 살아 있으면 알림을 읽는 사람이 존재하지 않는 로그인을 하러 갑니다.

제거의 부작용도 하나 표시해 둡니다. 8월 18일의 파이프라인 소개 글은 타임라인 그림과 본문에서 14시 티스토리 미러링을 아직 그리고 있어서 그 글은 지금 낡은 묘사가 됐습니다. 그 글을 고치는 일은 이 글의 몫이 아니라 운영자의 몫입니다.

검사가 헛돌지 않게 막은 층

1728c3c는 래퍼 수정과 함께 hermes-timeouts.test.ts 검사를 보강했습니다. 이 검사가 보는 것은 셋입니다. 래퍼별 지연 상한에 작업 여유 300초를 더한 값이 900초 안에 드는지, 지연 값을 해석하는 코드가 알려진 형태(5400, 480, 300, null)를 직접 읽는지, RANDOM으로 잠자면서 상한을 읽지 못하는 형태가 섞여 들어오면 막는 것.

티스토리 래퍼를 빼면 이 검사에 새로운 상황이 생깁니다. 자는 래퍼가 0개가 됩니다. 대상이 빈 집합이면 개수를 세는 단언은 아무것도 세지 않고 통과해 버립니다. 검사가 도는데 실제로는 아무것도 검사하지 않는 상태입니다. 그래서 해석기가 알려진 형태를 직접 읽는지를 묻는 두 번째 검사가 빈 집합에서도 의미를 갖도록 막아 둔 것이 이번 보강의 나머지 절반입니다. 기능을 빼는 커밋은 그 기능을 검사하던 검사까지 같이 무력화할 수 있습니다.

일부러 남긴 것

제거가 전멸은 아니었습니다. 네 자리를 남겼습니다.

첫째는 static/media/ 사본입니다. 디렉터리 10개에 PNG 11장입니다. 9개 슬러그는 cover.png 한 장씩이고 jongbuse-september-filing만 두 장입니다. 이 디렉터리 README.md의 이유는 이렇습니다. 티스토리 쪽 글이 https://<도메인>/media/<slug>/<파일> 형태의 절대 URL로 이미지를 부르고 있어서 지우면 우리 사이트는 멀쩡해도 티스토리 쪽 글의 이미지가 전부 깨진다는 것입니다. 하나 주의할 점이 있습니다. 10이라는 수를 미러링한 글 수로 읽으면 안 됩니다. 이미지가 없는 글은 사본이 안 생깁니다. 10개 슬러그는 모두 money 글로 실존하고(제거 시점 money 49편 가운데 발행일 2026-08-02~09-06) 나머지 글에 사본이 없는 것은 정상입니다.

티스토리 미러링 제거 뒤에 일부러 남긴 네 자리를 표시한 지도. static/media 사본 디렉터리 10개와 PNG 11장, Gatsby 스키마와 Post 타입에 남은 필드 3개, traffic.ts의 SOCIAL 유입 경로 라벨, .gitignore의 tistory.html 한 줄이 각각 남긴 이유와 함께 그려진 그림

빼는 것과 지우는 것은 다른 판정이다. 남긴 네 자리에는 각각 이유가 있다.

둘째는 스키마 필드 셋입니다. internal/gatsby/create-schema-customization.ts 3436행의 tistoryUrl: String, tistoryPostId: String, tistoryPublishedAt: Date가 그대로 있고 pipeline/lib/post.ts 3841행의 Post 타입에도 같은 셋이 있습니다. 셋째는 pipeline/traffic.ts 455행의 ["tistory", "티스토리"] 라벨로, SOCIAL 유입 경로를 분류하는 표(452~457행)의 한 항목입니다. 넷째는 .gitignore 57행의 content/**/tistory.html 한 줄입니다. 네 자리 모두 값이 없어도 빌드나 분석을 깨뜨리지 않는 자리라 지우는 판정을 따로 내릴 필요가 없었습니다.

문서는 그렇게 적었는데 실물에는 없었다

이번 조사의 백미가 여기 있습니다. 제거 커밋 443849b의 원문은 "frontmatter 의 tistoryUrl 등 필드와 Gatsby 스키마 — 옛 글에 값이 있다"고 적었고 지금의 FLOW.md 566행에도 같은 문구가 괄호로 남아 있습니다. 그런데 content 디렉터리 전체에서 tistory라는 흔적을 대소문자 무시하고 찾으면 0건입니다. 제거 시점 커밋된 money 글 49편을 포함한 전수에서 tistoryUrl에 값이 담긴 글은 한 편도 없었습니다.

이 어긋남은 구조가 설명해 줍니다. 옛 mirror.ts는 기록을 파일에는 쓰되 커밋은 별도로 남기는 구조였습니다. 대상을 고르는 50행의 조건은 draft가 아니고 tistoryUrl이 없는 글이었고 95~98행에서 frontmatter를 고쳐 썼으며 102행은 이렇게 출력했습니다. "frontmatter 에 기록했습니다. 커밋이 필요합니다." 같은 파일 17행의 주석도 "frontmatter 의 tistoryUrl 이 곧 기록이다. 올리고 나서 되쓰고 커밋한다"였습니다. 즉 기록 쓰기와 커밋 사이에는 코드가 아니라 사람의 손이 있었습니다. 그 손이 못 미친 날의 기록은 파일에 잠깐 생겼다가 사라지고 git에는 남지 않습니다.

단정 범위를 좁혀 둡니다. git 역사 전체를 뒤져 값이 있었다가 사라진 적이 있는지는 확인할 수 없었습니다. 이 글이 단정하는 것은 둘뿐입니다. 현재 커밋된 글 전수에서 값이 0건이라는 것, 제거 커밋의 통계에서 content 하위로 바뀐 파일은 config.json 하나뿐이라는 것입니다.

이 어긋남은 8월 18일에 정리한 프롬프트 지시 누락 사례와 이어집니다. 그 글은 static/media 커밋 누락을 막으려 프롬프트 문장을 없애고 daily.sh가 직접 add·commit 하게 했고(커밋 88fdf65) 그 결론으로 만들어진 static/media 잔여물 커밋 블록이 이번 제거에서 daily.sh와 함께 사라졌습니다. 코드로 옮겨 둔 가드는 그 기능이 살아 있는 동안에만 존재합니다. 그 자리는 이번에 두 번째로 비었습니다. 기록은 커밋을 동반할 때만 남는다는 결론은 두 사례에서 같은 방향을 가리킵니다.

mirror.ts가 frontmatter에 기록을 쓰고 커밋이 필요하다고 출력하지만 커밋은 사람 손에 맡겨져 git에 값이 남지 않는 경로와, 88fdf65가 daily.sh에 넣었던 static/media 커밋 블록이 443849b 제거와 함께 사라지는 경로를 나란히 그린 다이어그램. 두 경로 모두 git에 남는 것이 없다는 결론을 보여주는 그림

기록을 쓰는 코드와 커밋 사이에 사람의 손이 있으면, 그 손이 못 미친 날의 기록은 남지 않는다.

정리

미러링이 죽어도 발행은 계속 성공했기 때문에 겉은 멀쩡했습니다. 그 상태를 알릴 유일한 통로가 래퍼 타임아웃으로 막혔고 고침과 제거는 같은 날 약 두 시간 간격으로 완결됐습니다. 남긴 것은 티스토리 글이 부르는 이미지 사본과 값 없는 스키마 필드이고 문서가 "옛 글에 값이 있다"고 적은 부분은 커밋된 전수에서 0건이었습니다.

근거와 참고

  • 티스토리 API 문서 — Open API가 2024년 2월까지 순차 종료됐다는 서술과 종료 순서. 2026-09-12 직접 열람. 종료 공지는 notice.tistory.com/2664 로 안내돼 있으나 그 페이지 자체는 열어 확인하지 않았다
  • 커밋 443849b "chore: 티스토리 미러링을 뺐다" (2026-09-11 19:42) — 24개 파일 +48 −1124, 제거 사유 셋, "옛 글에 값이 있다" 문구. content 하위로 바뀐 파일은 config.json 하나뿐
  • 커밋 1728c3c (2026-09-11 17:51) — 세션 만료 시점, 9/9~11 점검 문구, 지연 상한 환경변수화와 hermes 타임아웃 검사 보강. 커밋 88707d2 (같은 날 17:52) — FLOW.md 갱신. 커밋 f7ad758 (2026-08-25) — 래퍼 버전 관리 이관. 커밋 88fdf65 — static/media 커밋 가드
  • FLOW.md 560562행·566행, CLAUDE.md 210행·433434행, pipeline/queue/money.md 146행·272행(사람 로그인 대기 기록 — 443849b 시점)
  • pipeline/hermes-timeouts.test.ts 914행·64행, pipeline/lib/message.test.ts 226235행(티스토리 안내 회귀 테스트)
  • 남긴 것 — static/media/ 10개 디렉터리·PNG 11장과 README, internal/gatsby/create-schema-customization.ts 3436행, pipeline/lib/post.ts 3841행, pipeline/traffic.ts 452~457행, .gitignore 57행
  • 앞선 글 — 프롬프트 지시 누락 코드 가드, 파이프라인 소개(14시 미러링 묘사 잔존), Threads 토큰 만료, 셸 헬스체크 빈 응답. 뒤 둘은 테마만 근접하고 사례는 다르다
이 글 공유하기xXfacebook페이스북