Joyuno's Dev Blog

2026-10-16 (금) 개발일지

dev-blog

오늘은 ai-tech-digest 대표 이미지가 사이트에서 깨져 보이는 문제를 잡았다. 처음엔 빌드 캐시가 꼬인 단순 이슈라 생각했는데, 파보니 외부 호스트 단일 의존이 문제였고, 결국 빌드 단계에 조건부 self-host 로직을 박는 쪽으로 정리됐다.

ai-tech-digest 대표 이미지 — 외부 호스트 429 를 빌드 단계에서 차단

5/17~5/18 글 3건의 대표 이미지가 빈 박스로 떴다. 처음엔 jekyll 빌드 캐시가 꼬인 줄 알고 _site 를 통째로 날려봤지만 증상이 그대로였다. 그제서야 직접 이미지 URL 을 curl -I 로 찔러봤고, 깨진 3건 모두 opengraph.githubassets.com 호스트에서 HTTP 429 를 뱉고 있다는 걸 확인했다. 응답 헤더에 retry-after 가 없는 상태라 캐시 만료를 기다려도 언제 풀릴지 가늠이 안 됐다.

처음엔 jekyll 플러그인 버전 문제를 의심했다. _site 를 날려 재빌드해도 증상이 그대로라서 빌드 산출물 문제는 아니었다. 그제서야 빌드 시점의 외부 호스트 의존 이 원인일 가능성으로 시야를 옮겼다. 깨진 글 3건의 OG 이미지 URL 을 모두 curl -I 로 찔러보니 3건 전부 opengraph.githubassets.com 에서 429 Too Many Requests 를 받고 있었다. 외부 호스트가 특정 시간대에 레이트리밋을 걸고 있었고, 우리 빌드가 그 응답을 받아 그대로 깨진 URL 을 박아버린 것.

해법 은 두 갈래로 좁혔다. 하나는 curl -f 로 빌드 시점에 외부 호스트를 검증하고 실패 시 og:image 태그를 생략하는 것, 다른 하나는 캐시된 이미지를 빌드 산출물에 self-host 로 박는 것이었다. 전자는 실패 시점마다 이미지가 사라져 UX 가 일관적이지 않았고, 후자는 빌드 시간이 3~5초 늘지만 결과물이 안정적이었다. 결국 _self_host_image() 헬퍼를 빌드 단계에 추가 해, OG 이미지 URL 이 opengraph.githubassets.com 을 가리키면 로컬 assets/img/og/ 로 복사해 경로를 교체하도록 했다.

이렇게 하니 429 로 깨지던 3건이 전부 정상 렌더링됐다. 이번 글에서는 이 이슈 두 개를 다룬다. 하나는 opengraph 429 를 조건부 self-host 로 좁힌 과정, 다른 하나는 _site 빌드 산출물에서 상대 경로가 깨진 문제다.

og:image 가 빈 박스로 렌더링 — 외부 호스트 429 를 조건부 self-host 로 좁힌 이유

5/17~5/18 글 3건의 대표 이미지가 사이트에서 빈 박스로 떴다. 처음엔 jekyll 빌드 캐시가 꼬인 줄 알고 _site 를 통째로 날려봤지만 증상이 그대로였다. 그제서야 직접 이미지 URL 을 curl -I 로 찔러봤고, 깨진 3건 모두 opengraph.githubassets.com 호스트에서 HTTP 429 를 뱉고 있었다.

첫 가설 은 외부 호스트의 레이트리밋이었다. 429 는 일시적이니 재시도하면 풀리겠지 싶어 curl 을 3분 간격으로 다시 찔러봤지만 매번 같은 응답이 돌아왔다. 그때부터 단순 일시 오류가 아니라 해당 호스트가 이 경로를 영구적으로 거부 하고 있을 가능성에 무게가 실렸다. curl -I 응답 헤더를 뜯어보니 Retry-After 가 없었고, 429 본문도 rate limit 이 아니라 캐시 서버의 정책 거부로 보였다.

원인 은 결국 외부 호스트 단일 의존이었다. opengraph.githubassets.com 가 429 를 반환하는 순간 OG 이미지가 통째로 누락됐고, jekyll 은 실패한 원격 리소스를 빈 태그로 남겨버렸다. 처음엔 캐시 문제로 오판했던 이유는, 5/17~5/18 글 3건만 깨진 걸 보고 로컬 빌드 캐시를 먼저 의심했기 때문이다. 하지만 _site 를 날려도 증상이 그대로라서 원인이 외부 호스트의 일시적 응답 거부라는 쪽으로 좁혀졌다.

해법은 두 갈래로 나뉘었다. 단기로는 깨진 글들의 이미지를 다시 내려받아 assets 로 옮기고, 이후 빌드 시점에 외부 호스트가 4xx/5xx 를 주면 해당 이미지만 조건부로 self-host 하는 로직을 _self_host_image() 에 넣었다. 이 함수는 curl -sI 로 상태코드와 content-length 를 확인하고, 429·403·404 류나 5KB 미만이면 원본 URL 을 버리고 로컬 파일을 참조하도록 URL 을 다시 쓴다.

전환점: 로그를 시간순으로 나열하지 말고 “첫 가설(캐시 오염) → 반례(캐시 무관) → 실제 원인(외부 호스트 의존)” 순으로 사고 흐름을 따라가야 한다.