시뮬레이션은 이미 있었다. 없던 것은 그 화면에서 무슨 일이 벌어지는지 설명하는 글이었다. 이 사이트는 그 글을 놓을 자리를 만들려고 지었다.
문제는 화면이 아니라 본문이었다
앞서 운영하던 사이트에는 인터랙티브 과학 시뮬레이션 276개가 쌓여 있었다. 구조화 데이터도 갖췄고 사이트맵도 380개 URL을 싣고 있었다. 그런데 자유 낙하 페이지의 눈에 보이는 텍스트를 실제로 세어 보니 한글 601자였고, 그마저 대부분 버튼 라벨과 축 이름과 숫자였다.
이 하나가 두 가지를 동시에 설명한다.
- 광고 심사: 리뷰어 눈에는 텍스트가 없는 화면이다
- AI 검색: 언어 모델은 캔버스를 실행하지 못한다. 안에서 무슨 일이 일어나는지 볼 방법이 없다
스키마는 "이게 무엇인지" 알려 주는 라벨이고, 실제로 읽히고 인용되는 것은 본문이다. 그래서 이 사이트의 설계 원칙은 하나로 줄었다. 시뮬레이션 한 개에 해설 글 한 편. 글이 별도 코너가 아니라 시뮬레이션의 본문 역할을 한다.
시뮬레이션은 손대지 않는다
시뮬레이션은 자체완결 HTML 파일이다. 프레임워크 안으로 끌고 들어와 컴포넌트로 뜯는 대신, public/sim-assets/[id]/index.html에 정적 파일로 두고 iframe으로 부른다.
이식은 명령 한 줄이다. 원본 주소와 slug를 주면 HTML을 받아 이전 사이트를 가리키는 것들을 걷어낸다. canonical 링크, og:*와 twitter:* 메타 전부, JSON-LD 스크립트, 그리고 제목 끝에 붙은 이전 사이트명이다. 대신 noindex를 심는다. 자산 파일은 /sim-assets/[id]/index.html로 직접 열리는 주소를 갖는데, 색인되어야 하는 것은 해설 페이지지 iframe 안의 캔버스가 아니기 때문이다. 캔버스와 스크립트와 스타일에는 손대지 않는다. 동시에 프론트매터만 채워진 해설 초안을 status: draft로 만들어 둔다.
이렇게 두면 시뮬레이션 쪽 스타일과 스크립트가 사이트의 CSS와 섞이지 않는다. 원본이 자기 외부 라이브러리를 쓰든 자기 폰트를 쓰든 iframe 안에서 끝난다. 대신 크롤러가 읽는 몸통은 iframe 바깥, 즉 MDX 해설 본문이다. 이건 우회가 아니라 의도한 분업이다.
| 층 | 고른 것 | 이유 |
|---|---|---|
| 프레임워크 | Next.js 15 App Router | 정적 생성으로 충분하고, 나중에 얹을 것이 있다 |
| 콘텐츠 | MDX 파일 + zod 프론트매터 | 글이 곧 데이터다. DB로 옮길 때 1:1로 대응된다 |
| 스타일 | CSS Modules + CSS 변수 | 색을 중립 여섯 개와 데이터 계열 세 개로 묶어 두기 위해 |
| 시뮬레이션 | public 정적 파일 + iframe | 원본 HTML을 고치지 않는다 |
| 배포 | Vercel | 이미 쓰고 있었다 |
발행은 한 글자다
모든 글은 프론트매터에 status를 갖는다. 로더가 published가 아닌 파일을 통째로 건너뛰므로, 초안은 빌드에도 사이트맵에도 RSS에도 llms.txt에도 나타나지 않는다.
이전 사이트에는 시뮬레이션이 276개 있었다. 그걸 한 번에 옮기지 않고, 해설을 쓴 것부터 한 편씩 이식해 status만 바꿔 공개한다. 파일을 무더기로 옮기지도, 설정을 고치지도 않는다. 이 방식의 값은 "언젠가 다 하겠다"는 계획을 세우지 않아도 된다는 데 있다.
id는 한 번 정하면 바꾸지 않는다. 지금은 파일 이름일 뿐이지만 나중에 데이터베이스 기본키가 될 자리다.
왼쪽으로만 나가는 레이아웃
본문은 760px 컬럼이고 오른쪽에 196px 사이드바가 붙는다. 표나 시뮬레이션처럼 넓어야 하는 것은 본문 폭을 넘어야 하는데, 오른쪽은 사이드바가 축을 잡고 있으므로 왼쪽으로만 나간다. 표는 최대 120px, 시뮬레이션은 최대 248px.
넓히는 양은 고정값이 아니라 뷰포트에서 컨테이너를 뺀 여백으로 계산한다. 창이 좁아지면 나갈 여백이 없어져 값이 저절로 0이 된다. 조건문도, 분기점도 없이 가로 스크롤이 생기지 않는다.
박스는 쓰지 않았다. 카드도, 회색 채움도, 둥근 모서리도, 그림자도 없다. 구분은 1px 헤어라인과 여백으로만 한다. 다크 모드도 없다 — 순백 한 벌에 commit했다.
AI가 읽을 것을 따로 챙긴다
- 글 종류별 JSON-LD — 시뮬레이션 해설은
LearningResource, 프로젝트는SoftwareApplication - 질문·답변 블록은 화면에 그리면서 같은 자리에서
FAQPage구조화 데이터를 함께 낸다 BreadcrumbList로 계층을,/about의Person과sameAs로 사람을 알린다sitemap.xml·feed.xml·llms.txt·llms-full.txtrobots.txt의 Content Signals로 색인과 인용은 허용하고 학습에는 동의하지 않는다고 밝힌다- 글마다 서버에서 그리는 OG 이미지
llms.txt는 언어 모델에게 이 사이트에 무엇이 있는지 목록으로 건네는 파일이고, llms-full.txt는 거기에 본문까지 붙인 것이다. 필수 규격은 아니고 효과가 증명된 것도 아니지만, 만드는 비용이 라우트 둘이어서 넣었다.
아직 아닌 것
- 발행된 해설은 이제 한 편이다. 이전 사이트의 나머지 275개는 아직 이식하지 않았다. 해설을 쓰는 대로 한 편씩 옮긴다
- 회원·학습진도·댓글은 만들지 않았다. 필요해지면 만든다
- 광고는 붙이지 않았다. 읽을 것이 쌓인 다음에 판단할 문제다
사이트가 하는 일은 글을 놓는 것뿐이다. 나머지는 글쓰기이지 개발이 아니다.