항해 기록
분석 리포트의 완성도가 올라왔다. 이제 무료 회원제로 전환한다. 등급은 없고 회원 여부만 본다.
- 규제 판단이 먼저 — 무료·단일 등급이라 유사투자자문업 신고 대상이 아니다. 기준은 "직접적으로 대가를 수령"인데 과금이 없다. 화면을 만들기 전에 이걸 먼저 확인했고, 유료화·후원을 도입한다면 신고 검토가 다시 먼저다
- 파이썬은 파이프라인으로, 화면은 Next.js로 —
export_json.py가 경계에서web/data/public/(누구나·티저)과web/data/member/(회원·근거)로 가른다 - 구글 인증 (Auth.js v5, JWT 세션, DB 없음) — 세션 하나만 보면 되니 데이터베이스가 필요 없다
- 죽은 덩어리 773줄 제거 —
build_frontpage.py(691줄 f-string 하나가 화면 전부였다)·render_reports.py·정적 HTML 17개가 서로만 참조하는 닫힌 묶음이 됐다
판단
★ 경계는 파일로 가른다 — CSS로는 못 막는다
회원 내용을 한 파일에 담아 CSS로 가리거나 조건부 렌더링하면 소스 보기로 다 뚫린다. 데이터가 이미 클라이언트에 가 있으니까. 그래서 층을 나눴다.
| 장치 | 무엇을 막나 |
|---|---|
import "server-only" | 클라이언트 컴포넌트가 회원 데이터를 import하면 빌드가 깨진다. 진짜 방어선 |
| 서버 컴포넌트 렌더 | 세션 확인 후 렌더된 HTML만 내려간다 — 원문·JSON은 안 간다 |
회원 JSON을 public/ 밖에 배치 | public/ 아래는 URL로 그대로 서빙된다 |
/api/member/*는 비로그인에 404 | 401은 "여기 뭔가 있다"를 알려준다 |
가장 중요한 건 첫 줄이다. 나머지는 사람이 지켜야 하는 규칙이지만, server-only는 어기면 빌드가 안 된다. 이 배가 계속 배운 것 — 사람이 지켜야 하는 규칙은 결국 깨지고, 경계에서 기계가 막아야 한다.
그리고 확인했다. 배포본에서 /analysis 307 · /api/member/dashboard 404 · /data/member/*.json 404. 여기서 멈추지 않고 클라이언트 번들을 직접 긁어 유의 사건 20건의 제목과 종목 9개 이름을 검색했다 — 0건. 상태 코드만으로는 부족하다. 페이지가 막혀도 번들에 데이터가 박혀 있을 수 있으니까.
대시보드 차트 9개는 React로 다시 쓰지 않았다. build_dashboard.py에 신뢰구간·템플릿이 이미 있고 테스트 10종이 그걸 지킨다. 세션을 확인하는 API로 감싸서 재사용했다. 잘 도는 걸 다시 쓰는 건 일이 아니라 위험이다.
★ 배포가 일곱 번 막혔다 — 다섯이 저장소 밖에 있었다
코드는 처음부터 맞았다. 저장소 안을 아무리 봐도 원인이 없었다.
- 설정이 코드를 이긴다 — Root Directory가 저장소 루트, Framework Preset이
python, Output Directory 오버라이드가 켜져 있었다. 셋 다 대시보드에서만 고칠 수 있고, 틀리면 빌드는 성공하고 상태만 Error가 되며 로그에 이유가 없다. 그리고 설정 변경은 재배포를 일으키지 않는다 — 커밋을 밀어야 한다 - 내가 넣은 최적화가 배포를 깼다 — 로컬 빌드 경고 하나가 거슬려서
next.config.ts에outputFileTracingRoot를 넣었다. 빌드는 통과하고 상태만 Error, 로그는 침묵. 경고 하나 때문에 배포를 깨뜨릴 이유는 없었다 - 도메인이 셋이다 — Vercel이
.vercel.app별칭을 자동 생성하고 지울 수 없다. Auth.js는 접속한 도메인 기준으로 콜백을 만들어서, 등록 안 된 별칭으로 들어오면 앱은 멀쩡히 배포된 채 로그인만 조용히 죽는다
그리고 내 검증이 세 번 통과했는데, 전부 엉뚱한 도메인을 보고 있었다
이게 이번 항해에서 가장 부끄러운 대목이다. 로그인 화면의 문자열로 판정했는데 그 문자열이 오류 페이지에도 있었다. 대조군을 뒀지만 대조군마저 다른 별칭에 걸어서 둘 다 같은 답을 줬다. 통과했다고 세 번 보고했고, 세 번 다 틀렸다.
테스트 대상과 실제 사용 경로가 달랐던 것 — 이 배가 반복해 당한 그 모양이다. 데이터가 멈춘 걸 못 본 것도, 낡은 업종 데이터로 전략을 계산한 것도 같은 병이었다. 이번엔 그게 나 자신의 검증에서 나왔다.
곁가지: 지우기 전에 테스트를 먼저 옮겼다
죽은 화면 생성기를 지울 때, 그 화면이 지키던 규칙까지 조용히 사라지는 것이 이 저장소의 사고 패턴이다. test_build_frontpage.py 11종 중 5개 개념을 먼저 이관했다. 특히 README 숫자 가드가 지울 파일 안에 있었다 — 그냥 지웠으면 문서의 숫자를 아무도 안 지키게 됐을 것이다.
옮기다가 진짜 불일치도 찾았다. 같은 카테고리가 앱에선 "국내 증시정책", 월간 리포트에선 "증시 정책"이었다. 파일 5개가 각자 이름표를 들고 있었기 때문이다. 이름과 색은 카테고리의 정체성이라 화면마다 다르면 같은 분류인지 알아볼 수 없다 → 단일 출처로 모으고 테스트로 고정했다.
남은 항로
- 커스텀 도메인을 붙여 Primary로 지정 — 그래야 별칭 3개가 하나로 정리되고 콜백 URI도 하나로 줄어든다
- 카카오·네이버 로그인 — 구글부터 확실히 돌려놓고
- 유료화는 아직 아니다. 도입한다면 규제 검토가 화면보다 먼저다