화면은 Not Found, 응답은 200이었습니다 — Next.js loading.tsx와 Soft 404
색인 보고서의 Soft 404 다섯 건은 없는 글 주소가 모두 200을 돌려주고 있어서였습니다. 원인은 loading.tsx였습니다. 스트리밍이 시작되면 상태 코드가 먼저 나가 notFound()는 화면만 바꿀 수 있었고, 글 상세만 스트리밍 바깥으로 옮겼습니다.
색인 보고서의 Soft 404 다섯 건은 없는 글 주소가 모두 200을 돌려주고 있어서였습니다. 원인은 loading.tsx였습니다. 스트리밍이 시작되면 상태 코드가 먼저 나가 notFound()는 화면만 바꿀 수 있었고, 글 상세만 스트리밍 바깥으로 옮겼습니다.
인증서가 나오고 자물쇠도 잠겼는데 화면은 503이었습니다. 인증서 층과 라우팅 층이 서로 다른 층이기 때문입니다. 인증서는 결승선이 아니라 시작선이었고, 남은 둘 중 하나는 인프라가 아니라 되돌리기 어려운 제품 결정이었습니다. 그래서 붙이지 않고 여기서 멈췄습니다.
설계 세 편을 쓰고 3주 뒤에 실제로 발급했습니다. 계획서에는 기존 리졸버를 건드리지 않겠다고 적어뒀는데 실행에서는 갈아끼웠고, 그 대가로 순단 3초와 '기존 인증서의 갱신 경로가 리졸버 단위로 전부 바뀐다'는 사실이 따라왔습니다. 확인한 것과 아직 확인하지 않은 것도 함께 적었습니다.

AI 코딩 세션의 gh 폴링을 GitHub 웹훅 원장으로 옮기며 README 에 «GitHub 은 실패하면 다시 보낸다»고 적었습니다. 공식 문서는 반대였습니다. PR 에 달린 말이 세 이벤트로 갈리는 구조와 X-Hub-Signature-256 을 원문 바이트로 재는 법도 적었습니다.

테스트가 전부 초록인 서버에서 방어선 24곳에 일부러 결함을 주입했습니다. 첫 시도에 23곳이 잡혔지만 허용 목록 조인을 지운 결함 하나는 초록으로 남았습니다. 두 겹의 방어가 서로를 가렸기 때문입니다. 한 겹만 남는 상황을 만들고 전제 단언으로 다시 잰 기록입니다.

at-least-once 로 보내고 수신자를 멱등하게 만드는 공식에는 수신자를 멱등하게 만들 수 있다는 전제가 깔려 있습니다. 수신자가 AI 에이전트면 같은 요청을 두 번 받고 PR 을 두 번 엽니다. 그래서 exactly-once 대신 "실행됐는지 모른다"를 상태로 올렸습니다.
S3 presigned URL 에 만료 1시간을 줘도 서명한 임시 자격증명이 먼저 만료되면 URL 도 끝납니다. 로컬 재현에서 SDK 는 이 어긋남을 막지 않고 서명했습니다. 발급 시점, CORS, 한글 파일명, 없는 파일의 403 까지 공식 문서와 재현으로 나눠 정리했습니다.
CREATE TABLE 에 collation 을 적지 않으면 MySQL UNIQUE 의 중복 기준은 서버 설정을 따라갑니다. collation 만 다른 MySQL 8.4 두 대로 재현한 차이와 RDS 파라미터 그룹·information_schema 로 확인하고 막는 법을 정리했습니다.