주소가 게시 시점에 굳어 저장되는 구조, 축 링크를 두 번 옮긴 순서, 한글 slug 가 간헐적으로 보인 두 가지 어긋남을 표로 갈랐다. 화면이 실패를 없음으로 그릴 때 작성 도구에서 왜 더 오래 숨는지, Promise.all 이 거절과 던짐에서 다른 경로를 타는 이유를 채웠다. CSS module 이 왜 전역 규칙에 닿지 않는지, 403 과 404 가 원인을 어떻게 좁혔는지도 적었다. link-audit.py 를 감사 Case 의 evidence 로 걸어 배정한 증거 하나를 메웠다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
6.9 KiB
Markdown
125 lines
6.9 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: an-address-frozen-at-publish-time
|
|
title: 계약은 앵커라고 적었고 만드는 쪽은 경로를 만들었다 — 이미 저장된 행까지 고쳤다
|
|
topic: addresses-frozen-at-publish-time
|
|
topicName: 주소가 만들어지고 굳어지는 곳
|
|
project: TechLog
|
|
status: 게시 전
|
|
lastVerifiedOn: 2026-09-04
|
|
assets:
|
|
- key: decision-path-404
|
|
file: ../../../final/assets/diagrams/decision-path-404/decision-path-404.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/db/decision-path-after-v15.txt
|
|
- ../../../final/evidence/raw/api/decision-anchor-fixed.txt
|
|
- ../../../final/evidence/raw/audit/dead-link-sweep.txt
|
|
- ../../../final/evidence/raw/audit/link-audit.py
|
|
sourceRevision: tech-log@2026-09-02
|
|
source:
|
|
- final/document.md#§9.2
|
|
---
|
|
|
|
# 계약은 앵커라고 적었고 만드는 쪽은 경로를 만들었다 — 이미 저장된 행까지 고쳤다
|
|
|
|
공개 화면의 「다음에 읽을 것」 두 번째 항목이 404 였다. 계약은 결정의 공개 주소가 목록 위의 앵커라고 이미 적어 두었는데, 주소를 만드는 두 곳이 그 대신 별도 경로를 만들고 있었다. 주소는 게시할 때 만들어 DB 에 저장되므로 코드만 고치면 이미 게시된 링크는 깨진 채 남는다.
|
|
|
|
## 관계
|
|
|
|
- **서버가 준 주소는 라우트 표에 맞춰 보고, 맞는 라우트가 없으면 링크로 그리지 않는다**
|
|
이 사건에서 세운 두 겹 가드다.
|
|
- **결정에는 상세 화면이 없어 목록 항목이 문서 전체를 실어야 했다**
|
|
같은 앵커 구조에서 난 계약 쪽 사건이다.
|
|
- **축 링크가 자기 자신을 가리켰고, 고친 뒤에는 백엔드를 먼저 배포했다**
|
|
같은 시기에 주소를 옮기다 난 다른 사건이다.
|
|
|
|
## 문제
|
|
|
|
`/references/external-idp-federation-application-boundary` 의 「다음에 읽을 것」 두 번째 항목이 404 였다.
|
|
|
|
화면 코드 어디에도 그 주소를 만드는 곳이 없다. 주소는 게시할 때 서버가 만들어 DB 에 저장한 문자열이고, 화면은 그것을 그대로 링크로 그린다.
|
|
|
|
## 결론
|
|
|
|
결정에는 상세 화면이 없고 공개 라우트는 목록 하나뿐인데, 게시할 때 만든 주소는 목록 아래에 slug 를 붙인 경로였다.
|
|
|
|
계약 : 공개 주소가 앵커라고 이미 적혀 있었다
|
|
게시 시점 : 앵커가 아니라 경로를 만들어 저장했다
|
|
조회 시점 : 저장된 주소를 읽어 링크로 내보냈다
|
|
방문자 : 맞는 라우트가 없어 404
|
|
|
|
고친 것은 넷이다.
|
|
|
|
두 곳이 앵커를 만들게 했다
|
|
이미 게시된 행도 V15 마이그레이션에서 함께 고쳤다 — 코드만 고치면 기존 링크는 깨진 채 남는다
|
|
공개 라우트의 slug 를 앵커가 있으면 그 뒤를 조각으로 읽게 했다
|
|
목록 항목이 앵커를 달 수 있도록 계약에 slug 를 더하고, 화면이 그 slug 를 element id 로 달고 앵커로 들어오면 데이터를 받아 그린 뒤 스크롤하게 했다
|
|
|
|
배포 후 서버가 내보내는 주소 26개와 주제·축 9개를 더해 35개 전부 200 인 것을 확인했다.
|
|
|
|
## 검증 환경
|
|
|
|
tech-log-design-package : 1aae8dc
|
|
tech-log-backend : 8cd8ee3 · V15 마이그레이션 적용
|
|
tech-log-frontend : fe6b56a
|
|
확인 방식 : 배포본에서 서버가 내보내는 주소를 전수로 훑어 상태 코드를 셌다
|
|
|
|
## 재현 조건
|
|
|
|
1. 결정을 하나 게시하고 다른 기록에서 그것을 관계로 건다
|
|
2. 공개 화면에서 그 관계 링크를 누른다
|
|
3. 저장된 주소와 공개 라우트 패턴을 대조한다 — 맞는 라우트가 없으면 404 다
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
## 주소가 만들어져 저장되고 방문에서 끝난다
|
|
|
|
:::evidence key="decision-path-404" alt="계약·게시 시점 경로 생성·저장 테이블·조회 시점 경로 생성·방문자·공개 라우트 여섯 참가자 사이의 순서도" caption=" " zoom="true"
|
|
:::
|
|
|
|
계약은 결정의 공개 주소가 목록 위의 앵커라고 규정한다. 게시 시점의 경로 생성기는 그 대신 목록 아래에 slug 를 붙인 경로를 만들어 공개 투영에 저장한다. 조회 시점의 다른 생성기가 저장된 주소를 읽어 방문자에게 링크로 내보낸다. 방문자가 그 주소를 요청하면 공개 라우트에는 목록 하나뿐이라 맞는 라우트가 없다.
|
|
|
|
계약은 고칠 것이 없었다. 공개 주소가 앵커라는 것이 이미 계약에 있었고, 만드는 쪽 두 곳이 그것을 따르지 않았다.
|
|
|
|
## 화면 코드에는 흔적이 없다
|
|
|
|
이 부류는 저장소를 훑어서는 나오지 않는다. 주소는 게시할 때 서버가 만들어 데이터베이스에 저장한 문자열이고, 화면은 그것을 받아 그대로 링크로 그린다. 코드에 `to=` 나 `href=` 리터럴로 남아 있지 않다.
|
|
|
|
감사 스크립트가 그 이유를 적어 두었다.
|
|
|
|
> 주소는 게시 시점에 서버가 만들어 DB(`public_resource_projection.navigation_path`)에 저장한 문자열이다. 그래서 저장소 안의 `to=` / `href=` 리터럴만 훑는 감사로는 잡히지 않는다 — 실제로 결정 링크가 그렇게 숨어 있었다.
|
|
|
|
## 저장된 행까지 고쳐야 한다
|
|
|
|
주소가 게시 시점에 굳어지므로 코드만 고치면 이미 게시된 링크는 깨진 채 남는다. 마이그레이션에서 저장된 행을 함께 고쳤다.
|
|
|
|
공개 라우트의 slug 를 읽는 규칙도 함께 손봤다. 마지막 슬래시 뒤를 자르는 방식이면 앵커가 붙은 주소에서 `decisions#slug` 전체가 slug 로 저장된다. 앵커가 있으면 그 뒤를 조각으로 읽게 했다.
|
|
|
|
목록 항목이 앵커를 달 수 있도록 계약에 slug 를 더하고, 화면이 그 slug 를 element id 로 달고 앵커로 들어오면 데이터를 받아 그린 뒤 스크롤하게 했다.
|
|
|
|
## 두 겹 가드
|
|
|
|
| 어디 | 무엇을 보나 | 이 부류가 또 나면 |
|
|
|---|---|---|
|
|
| 백엔드 `PublicPathsTest` | 종류마다 만들어 낸 경로가 실제 공개 라우트 패턴에 맞는가 | 만드는 쪽에서 멈춘다 |
|
|
| 프론트 `resolvesToPublicRoute` | 서버가 준 주소가 라우트 표에 있는가 | 링크로 그리지 않아 방문자가 404 를 만나지 않는다 |
|
|
|
|
만드는 쪽에만 두면 이미 저장된 주소를 못 잡고, 그리는 쪽에만 두면 링크가 안 그려지는 것으로 끝나 원인이 남는다.
|
|
|
|
## 배포 뒤 전수 감사
|
|
|
|
배포 후 사이트 전체를 훑어 서버가 내보내는 주소 26개와 주제·축 9개를 더해 35개 전부 200 인 것을 확인했다.
|
|
|
|
:::evidence key="dead-link-sweep" alt="서버가 내보내는 주소 35개를 전수로 훑은 감사 출력" caption=" " zoom="false"
|
|
:::
|
|
|
|
같은 방식으로 다시 검사하는 스크립트를 증거와 함께 남겼다. 다음에 라우트를 더하면 그 스크립트를 다시 돌린다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
이 감사는 서버가 내보내는 주소만 본다. 본문 안에 작성자가 손으로 쓴 링크는 대상이 아니다.
|
|
|
|
<!-- body:end -->
|