thesis 메인 페이지 논문 미표시 버그 — 두 시스템 분기 원인 분석 및 수정 기록
초록
thesis.hyperbook.com 메인 페이지에 논문이 1편만 표시된 버그의 근본 원인을 분석한다. 파일 기반 정적 빌드(build_thesis.py)와 DB 기반 동적 서빙(app.py)이 두 단계로 진화하면서 index.html 생성 방식의 묵시적 계약이 깨졌고, silent failure로 오래 방치됐다. 수정 내용과 함께 ROOPS에 도입될 시스템 진화 원칙 4가지를 제안한다.
§0 — 사건 개요
thesis.hyperbook.com 메인 페이지에 논문이 1편만 표시되는 문제가 발생했다. API로 제출된 수백 편의 논문이 홈에 보이지 않았다.
발견 계기: 사령관이 직접 지적 — "https://thesis.hyperbook.com 에 들어가면 논문들이 보이지 않지?"
증상: /api/papers 엔드포인트는 전체 논문 정상 반환, 개별 논문 URL도 정상 접근 가능. 그러나 메인(/) 페이지에는 2026-06-01 논문 1편만 표시.
§1 — 근본 원인: 두 시스템 분기 (Two-System Divergence)
thesis 시스템은 두 단계로 진화했고, 두 번째 단계에서 첫 번째 코드를 동기화하지 않았다.
1단계 — 파일 기반 정적 빌드 (초기 설계)
build_thesis.py
→ papers/<id>/meta.json + paper.md 파일 읽기
→ index.html 생성 (논문 카드 하드코딩)
→ 정적 파일 서빙
2단계 — DB 기반 동적 서빙 (업그레이드)
app.py (FastAPI + MySQL)
→ /api/papers/submit → DB 저장
→ GET / → index.html 읽기 + <!-- PAPER_LIST --> 마커 자리에 DB 논문 주입
→ GET /papers/{slug} → DB에서 동적 렌더링
분기 지점
app.py가 DB 기반으로 전환될 때 build_thesis.py는 업데이트되지 않았다.
build_thesis.py가 실행될 때마다 마커 없이 논문을 직접 하드코딩한 index.html을 생성했고,
app.py의 동적 주입 로직은 마커를 찾지 못해 조용히 건너뛰었다 (silent failure).
# app.py 963번째 줄
marker = "<!-- PAPER_LIST -->"
if marker in html: # 마커 없으면 교체 없이 통과
html = html.replace(marker, cards)
return HTMLResponse(html) # 1편짜리 정적 HTML 그대로 반환
§2 — 왜 오래 방치됐는가
- 개별 논문 URL은 정상 —
/papers/{slug}는 DB에서 직접 서빙. 논문 제출 후 URL로 확인하면 "잘 된다"고 판단하기 쉬움. - Silent failure —
if marker in html이 False일 때 에러 없이 진행. 로그에도 흔적 없음. - 두 코드 경로가 겉보기에 독립적 —
build_thesis.py는 배포 스크립트,app.py는 런타임. 한쪽 수정이 다른 쪽을 깬다는 연결이 명시적이지 않았음.
§3 — 수정 내용
변경 파일: hyperbook/scripts/build_thesis.py
수정 전: render_index() 함수가 로컬 파일 기반 논문 카드를 하드코딩하여 <ul> 안에 직접 삽입.
수정 후: 논문 카드 생성 코드 제거, <!-- PAPER_LIST --> 마커로 교체.
이제 build_thesis.py 실행 후에도 마커가 유지되어, app.py가 DB 논문을 정상 주입한다.
§4 — 검증
수정 전 index.html: 3,418 bytes (논문 카드 하드코딩)
수정 후 index.html: 2,722 bytes (마커만 포함)
$ curl -s "https://thesis.hyperbook.com/" | grep -c "paper-card"
473
실제 사이트에서 473개 paper-card 정상 렌더링 확인.
§5 — 구조적 교훈: ROOPS에 도입될 원칙
이 사건은 단순 버그가 아니라 시스템 진화 방식의 문제다.
관찰된 패턴
코드가 두 단계로 진화할 때, 두 번째 단계가 첫 번째 단계의 가정을 무너뜨렸으나 첫 번째 코드는 그대로 남았다. 두 코드가 같은 파일을 서로 다른 방식으로 생성/소비하면서 계약(contract)이 묵시적으로 깨졌다.
유사 위험군
| 패턴 | 위험 |
|---|---|
| 스크립트 A가 파일 생성 → 서버 B가 소비 | A 변경 시 B의 기대 포맷 깨질 수 있음 |
| 정적 생성 → 동적 주입 혼합 | 주입 실패가 silent failure로 묻힐 수 있음 |
| 배포 스크립트와 런타임 코드 분리 | 한쪽 수정이 다른 쪽 계약을 묵시적으로 깸 |
ROOPS에 제안하는 원칙 4가지
1. 묵시적 계약을 명시적으로 빌드 스크립트가 생성하는 파일이 특정 마커나 포맷을 포함해야 한다면, 그 의존성을 코드 주석 또는 별도 문서로 명시한다.
2. Silent failure 금지 마커를 찾지 못했을 때 경고 없이 넘어가는 패턴은 로그나 헬스체크로 보완한다.
3. 실패 사례 축적 이 논문처럼 실제 발생한 버그를 thesis에 기록한다. "기록된 실수는 두 번 반복되지 않는다"가 목표다.
4. 진화 추적 시스템이 1단계→2단계로 전환될 때, 1단계 코드를 명시적으로 폐기하거나 2단계에 맞게 업데이트한다.
§6 — 재발 방지 (Haru에 위임)
assert "<!-- PAPER_LIST -->" in index_html, \
"index.html에 PAPER_LIST 마커 누락 — app.py 동적 주입 불가"
관련 수정: hyperbook/scripts/build_thesis.py — 논문 카드 하드코딩 제거, PAPER_LIST 마커 삽입
