o2o-site-AEO/docs/RENDERING.md
Mina Choi 4ff92fb0a6 [docs] docs: 문서 정리 — DEVLOG 요약 · 렌더링 흐름 문서 추가 · 삭제한 방향 문서 링크 정리
DEVLOG 가 1,900줄이 넘어 최근에 무엇을 왜 바꿨는지 찾기 어려웠다.

- DEVLOG.md: 개발 이력상 남길 가치가 있는 항목만 요약
- RENDERING.md: 정적 사이트 · 미리보기 · 발행 세 경우의 흐름과 담당 파일
- README · AGENTS · PRODUCT: 삭제한 DEVELOPMENT_DIRECTION.md 링크 정리(삭제 자체는 앞 커밋),
  admin 은 필요한 규모가 되면 개발한다는 안내, 템플릿 문서 링크

문서만 바꿈

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:45:24 +09:00

12 KiB

렌더링 한눈에 보기

사이트가 화면에 그려지는 경우는 세 가지다. 셋 다 그리는 코드는 solution/site/src/App.tsx 하나이고, 누가 언제 그리느냐만 다르다.

경우 누가 그리나 입력
정적 사이트 워커가 미리 구운 HTML → 브라우저가 이어받음 HTML 안에 심어 둔 payload
미리보기 브라우저가 처음부터 API 가 그때그때 만든 payload
발행 워커가 Node 렌더러를 돌려 HTML 을 굽는다 DB → payload 파일

경로는 레포 루트 기준이다. site/ 는 solution/site/, backend/ 는 solution/backend/ 를 줄인 것이다.


1. 정적 사이트 — 손님·크롤러가 /s/<slug> 를 받을 때

flowchart TD
  A["손님 · 크롤러<br/>GET /s/&lt;slug&gt;"] --> B["nginx<br/>location ^~ /s/"]
  B --> C["out/s/&lt;slug&gt;<br/>(심볼릭 링크)"]
  C --> D["out/versions/&lt;slug&gt;/&lt;ver&gt;/index.html"]
  D -->|크롤러는 여기까지| E["HTML · JSON-LD · meta"]
  D --> F["브라우저가 /assets/index-해시.js · .css 를 받음"]
  F --> G["entry-client.tsx<br/>window.__SITE_PAYLOAD__ 있음"]
  G --> H["hydrateRoot(App)<br/>버튼·달력 등 동작이 붙는다"]
  D --> I["사진 /s/&lt;slug&gt;/img/*<br/>노래 /s/&lt;slug&gt;/*.mp3"]
단계 하는 일 파일
요청 받기 /s/<slug> 는 구운 파일을 그대로 준다. /s 는 목록, /s/ 는 /s 로 301 nginx/site.conf.example (location = /s, location ^~ /s/)
공개 버전 찾기 out/s/<slug> 는 지금 공개 중인 버전 폴더를 가리키는 링크다 site/scripts/prerender.ts publishVersion
HTML 본문·<head>(title·canonical·JSON-LD)·심어 둔 payload 가 한 파일에 있다 out/versions/<slug>/<ver>/index.html
번들 해시 이름의 JS·CSS. 1년 캐시 nginx/site.conf.example location ^~ /assets/ → out/assets/
이어받기 심어 둔 payload 로 같은 화면을 다시 만들어 마크업에 동작을 붙인다 site/src/entry-client.tsx (hydrateRoot)
그리기 템플릿의 레이아웃을 고르고 섹션을 순서대로 그린다 site/src/App.tsx → site/src/pages/SectionList.tsx

검색엔진이 읽는 건 구운 HTML 이다. 렌더러를 고쳐도 이미 구운 HTML 은 사장님이 다시 발행하기 전까지 그대로다.


2. 미리보기 — 빌더 iframe /preview?placeId=…

flowchart TD
  A["빌더에서 템플릿·색·섹션 저장<br/>POST …/site/template · …/site/theme"] --> B["onSiteThemeSaved 신호"]
  B --> C["SitePreview.tsx<br/>iframe 다시 로드"]
  C --> D["GET /preview?placeId=…<br/>nginx location = /preview"]
  D --> E["out/preview/index.html<br/>빈 껍데기 + 번들"]
  E --> F["entry-client.tsx renderPreview"]
  F --> G["GET /v1/place/{id}/site/preview"]
  G --> H["SiteService.preview_payload<br/>build_snapshot → prepare_site_payload"]
  H --> F
  F --> I["themeVars · 폰트 로드"]
  I --> J["createRoot(App)"]
  J --> K["postMessage o2o:preview-painted"]
  K --> L["빌더가 스피너를 걷는다<br/>(12초 상한)"]
단계 하는 일 파일
다시 그릴 때를 안다 저장이 끝나면 iframe 을 새로 고친다. 보던 스크롤 위치는 지킨다 solution/frontend/src/features/builder/SitePreview.tsx, solution/frontend/src/features/publish/siteTheme.ts onSiteThemeSaved
껍데기 받기 본문이 빈 HTML. noindex 가 붙어 있다 nginx/site.conf.example location = /preview → out/preview/index.html (prerender.ts writePreviewShell)
payload 받기 로그인 토큰을 붙여 API 를 부른다 site/src/entry-client.tsx renderPreview
payload 만들기 발행과 같은 함수로 만든다. 버전도 파일도 만들지 않는다 backend/router/v1/site/site.py site_preview → backend/services/site_service.py preview_payload → services/snapshot.py build_snapshot → services/site_payload.py prepare_site_payload
템플릿 확인 모르는 id 면 API 가 422, 화면은 에러 문구로 멈춘다 backend/common/template_catalog.py, solution/shared/src/lib/catalog.ts templateOf
그리기 색 변수·폰트를 먼저 넣고 처음부터 그린다 entry-client.tsx (themeVars, fontHref ← site/src/seo/head.ts), App.tsx
완료 알림 두 프레임 뒤 부모 창에 알린다. 빌더는 출처와 iframe 을 확인한다 entry-client.tsx signalPreviewPainted, SitePreview.tsx PAINT_TIMEOUT_MS

미리보기는 사진을 내려받지 않는다. 원래 주소를 그대로 쓴다.


3. 발행 — 무엇을 읽고 무엇을 쓰나

flowchart TD
  A["사장님 '발행하기'<br/>POST /v1/place/{id}/site/build"] --> B["SiteService.start_build<br/>jobs 에 BUILD"]
  B --> C["워커 worker/handlers.py<br/>build_service.run_build"]
  C --> D["build_snapshot<br/>DB 값 모으기 · site_versions 행 추가"]
  D --> E{"1차 게이트<br/>상호·업종·사실 확인 · 템플릿 id"}
  E -->|실패| X["버전 FAILED · 발행 로그"]
  E --> F["emit_payload<br/>payloads/&lt;slug&gt;.json"]
  F --> G["render_service.render_site<br/>node prerender.js --stage-only"]
  G --> H["mirrorMedia → prerenderSite<br/>out/versions/&lt;slug&gt;/&lt;ver&gt;/"]
  H --> I["보고서<br/>payloads/.status/&lt;slug&gt;.json"]
  I --> J{"2차 게이트<br/>publish_gate.evaluate"}
  J -->|실패| X
  J --> K["render_service.activate_site<br/>node prerender.js --activate=slug:ver"]
  K --> L["out/s/&lt;slug&gt; 링크 전환<br/>루트 sitemap · robots · llms 갱신"]
  L --> M["Azure 업로드 · 썸네일 · IndexNow"]
  M --> N["DB 기록<br/>버전 BUILT · sites PUBLISHED · 발행 로그"]
단계 하는 일 파일
잡 넣기 검증 안 된 사업장은 막는다. 같은 사업장 BUILD 는 겹치지 않는다 backend/router/v1/site/site.py start_build → services/site_service.py start_build
잡 집기 BUILD 잡을 run_build 로 넘긴다 backend/worker/handlers.py
스냅샷 DB 값을 한 벌로 모아 site_versions.snapshot 에 박제한다 services/snapshot.py build_snapshot, services/build_service.py run_build
1차 게이트 상호명·업종·사실 확인 여부, 템플릿 id services/publish_gate.py check_facts_verified, common/template_catalog.py resolve_template_id
payload JSON 으로 쓴다. 임시 파일에 쓰고 이름을 바꾼다 services/site_payload.py emit_payload → write_payload
굽기 Node 를 직접 실행한다. 파일 잠금으로 한 번에 하나만 돈다 services/render_service.py render_site (.render.lock)
렌더 공개 금지 값 걸러내기 → 사진 내려받기 → HTML·JSON-LD·llms.txt → 대조 site/scripts/prerender.ts sanitizePayloadForPublish · mirrorMedia · prerenderSite · verifyJsonLd
2차 게이트 보고서의 대조 결과·고유 콘텐츠 건수로 판정 services/publish_gate.py evaluate, services/render_report.py
공개 전환 검증된 버전인지 보고서로 다시 보고 링크를 바꾼다 render_service.activate_site → prerender.ts publishVersion · writeRootMachineFiles
바깥 알리기 설정된 경우만 돈다 services/azure_static.py publish, services/site_thumbnail.py store, services/indexnow.py submit
DB 기록 버전·사이트·사업장 상태와 발행 로그를 남긴다 services/build_service.py run_build · _log

입력

무엇 어디서 읽는 쪽
DB 값 place_facts place_units place_faqs place_photos place_songs place_posts place_reviews place_social_posts area_contents site_sections sites services/snapshot.py build_snapshot
템플릿 목록 solution/shared/src/data/templates.json 백엔드 common/template_catalog.py, 렌더러 shared/src/lib/catalog.ts
payload site/payloads/<slug>.json (SITE_PAYLOAD_DIR) prerender.ts loadOne — schemaVersion 1 · 슬러그 · 버전을 본다
번들 목록 site/dist/client/.vite/manifest.json prerender.ts readAssets — 엔트리 JS·CSS 파일명
번들 파일 site/dist/client/assets/, site/public/fonts/ prerender.ts writeSharedAssets
사진 payload.media[].url 이 가리키는 바깥 주소 prerender.ts mirrorMedia (15초 · 8MB)
노래 site/songs/*.mp3 prerender.ts copySongs

출력

무엇 어디에 누가 쓰나
payload site/payloads/<slug>.json site_payload.py write_payload
렌더 보고서 site/payloads/.status/<slug>.json prerender.ts writeReport (백엔드가 render_report.py 로 읽는다)
HTML out/versions/<slug>/<ver>/index.html — JSON-LD 는 따로 파일이 없고 <head> 안 <script type="application/ld+json"> 이다 prerender.ts prerenderSite, site/src/seo/head.ts renderHead
llms.txt out/versions/<slug>/<ver>/llms.txt prerenderSite → renderLlmsTxt
사진 out/versions/<slug>/<ver>/img/<주소해시>.<확장자> mirrorMedia
노래 out/versions/<slug>/<ver>/*.mp3 copySongs
버전 보고서 out/versions/<slug>/<ver>/.render-report.json — 있으면 그 버전은 다시 굽지 않는다 prerender.ts main
공개 링크 out/s/<slug> → ../versions/<slug>/<ver> (상대 링크) publishVersion. 옛 일반 폴더는 versions/<slug>/legacy 로 옮긴다
옛 버전 정리 최근 5개·30일은 남긴다 pruneOldVersions
공용 번들 out/assets/, out/fonts/, 대장 out/assets/.builds.json writeSharedAssets · pruneAssets (HTML 이 참조하는 건 안 지운다 — referencedAssets)
미리보기 껍데기 out/preview/index.html writePreviewShell
루트 파일 out/robots.txt out/sitemap.xml out/llms.txt out/s/index.html(목록) out/<INDEXNOW_KEY>.txt writeRootMachineFiles · writeIndexNowKey
DB site_versions(스냅샷·빌드 상태·JSON-LD·고유 콘텐츠 수), sites(PUBLISHED·current_version_id·published_at·썸네일), places.status, place_posts·place_reviews 발행 표시, site_publish_logs services/build_service.py run_build

--stage-only 로 굽는 단계는 out/s/<slug> 를 건드리지 않는다. 공개 주소가 바뀌는 건 2차 게이트를 통과한 뒤 --activate 한 번뿐이다. 공개 전환과 DB 기록은 한 트랜잭션이 아니다(PUBLISH_VERSION.md).


템플릿·레이아웃은 어디서 골라지나

sites.template_id 에 저장된 id 를 solution/shared/src/data/templates.json 에서 찾아 그 템플릿의 layout (basic paper round cinema bigtype boutique graphic)을 얻는다(shared/src/lib/catalog.ts templateOf). site/src/App.tsx 가 그 값으로 site/src/layouts/index.ts 의 LAYOUTS 에서 뼈대를 꺼내 Frame 으로 감싸고, 안쪽은 site/src/pages/SectionList.tsx 가 켜진 섹션을 순서대로 그린다. 레이아웃이 sections 에 따로 등록한 섹션은 그 컴포넌트를, 등록하지 않은 섹션은 site/src/sections/ 의 공용 컴포넌트를 쓴다.

관련 문서

  • TEMPLATES.md — 템플릿 추가, 세 폴더(frontend · shared · site)의 역할
  • PUBLISH_VERSION.md — 발행 버전, 공개 링크 전환, 롤백
  • ARCHITECTURE.md — 백엔드와 렌더러가 디렉토리 하나로만 만나는 이유