이 블로그의 About 페이지는 이력서 역할을 합니다. 필요한 정보는 다 있지만, “웹 접근성·프론트엔드·AI 도구에 관심 있는 개발자”라는 소개를 글자로만 하고 있었습니다. 그래서 정적 사이트 위에 직접 만져 볼 수 있는 소개 페이지 /planet/을 만들었습니다.
어린 왕자의 B612처럼 작은 행성 하나가 있고, 캐릭터를 움직여 표지판에 다가가면 소개·경력·글·프로젝트·링크가 열립니다. 이 글은 만들면서 부딪힌 네 가지 문제를 다룹니다.
- 3D 페이지를 누구나 읽을 수 있게 만들기
- 공 모양의 땅 위에서 걷기
- 모델 용량과 로딩 시간 줄이기
- AI 시안과 AI 코딩 에이전트로 만들기
만들고 싶었던 것
원칙은 두 가지였습니다.
- 걸어 다니며 발견하는 재미. 행성 하나, 캐릭터 하나, 장소 다섯 개.
- 탐험하지 않아도 읽을 수 있을 것. 상단 메뉴를 누르면 걸어가지 않고 바로 내용이 열립니다. “일반 보기”를 누르면 3D를 끄고 같은 내용을 문서로 읽습니다.
점프, 아이템 수집, 자유 카메라, 물리 엔진은 처음부터 뺐습니다. 범위를 좁게 잡아 기능을 늘리지 않고 완성도를 높이는 데 집중했습니다.
페이지에 새로 들인 라이브러리는 Three.js 하나입니다. 3D 모델은 Blender Python 스크립트로 만들고, glTF Transform으로 줄였습니다. 이 도구들은 빌드 때만 쓰고 방문자에게는 내려가지 않습니다.
시안, 그리고 실제 화면
시안은 AI로 생성한 목업입니다. 남색 우주, 모래색 행성, 유리 돔 속 장미와 화산, 돌 산책길, 청록색 코트의 탐험가, 오른쪽의 어두운 정보 패널을 보고 방향을 정했습니다.

아래는 실제 화면에서 경력 장소를 연 모습입니다. 캐릭터 뒤쪽 위에서 행성을 내려다보는 구도, 배색, 소품 구성은 시안을 따랐습니다. 오른쪽 패널의 금색 타임라인도 시안에서 옮겼고, 패널 위 탭으로 다른 장소로 바로 넘어갈 수 있습니다.

시안만큼 나오지는 않았습니다. 시안은 빛이 더 풍부하고 소품마다 손으로 그린 듯한 디테일이 있습니다. AI가 그린 그림에는 용량도 성능 제한도 없지만, 실제 페이지는 폰에서도 매 순간 3D를 그려야 합니다. 다음에는 그림 한 장보다 브라우저에서 거칠게 그려 본 화면으로 방향을 정하겠습니다.
일부러 뺀 것도 있습니다. 시안에는 “소리 끔” 버튼이 있었지만, 배경음과 발걸음 소리를 아이폰에서 들어 보니 페이지와 어울리지 않아 소리를 넣지 않았습니다.
HTML이 원본인 3D
블로그는 Astro로 만든 정적 사이트이고, 이 페이지 하나를 위해 React 같은 프레임워크를 들이고 싶지 않았습니다. 그래서 모든 정보를 평범한 HTML로 두고, 3D는 그 위에 얹는 겉모습으로만 만들었습니다.
- Astro가 빌드할 때 경력·프로젝트·최근 글을 HTML로 만듭니다. 3D가 없어도 이 HTML만으로 모든 내용을 읽을 수 있습니다.
- 페이지의 Astro
<script>↗가 3D 코드를import()로 나중에 불러옵니다. Three.js는 별도 파일로 분리돼 이 페이지에서만 내려받습니다. - 3D를 띄우지 못하면 캔버스만 숨기고 HTML을 그대로 둡니다.
3D 쪽은 장소 ID(intro, career 등)만 알고 실제 내용은 모릅니다. 캐릭터가 장소에 도착하면 해당 ID의 HTML을 <dialog> ↗ 안으로 옮기고, 닫으면 제자리로 돌려놓습니다. 내용을 복사하지 않으니 스크린리더가 같은 내용을 두 번 읽지 않습니다. About 페이지와 같은 데이터를 쓰므로 경력을 한 곳에서 고치면 두 페이지가 함께 바뀝니다.
패널은 dialog.showModal()로 엽니다. 뒤쪽 화면 조작을 막는 것과 Esc로 닫는 것은 브라우저가 처리합니다. 닫은 뒤에는 패널을 열었던 버튼으로 초점을 돌려놓습니다.
키보드 동작은 초점 위치에 따라 나눴습니다.
| 입력 | 탐험 화면에 초점이 있을 때 | 패널에 초점이 있을 때 |
|---|---|---|
| W / ↑, S / ↓ | 앞·뒤로 걷기 | 방향키는 평소처럼 스크롤 |
| A / ←, D / → | 좌·우로 걷기 | 평소 동작 |
| Enter | 가까운 장소 열기 | 버튼·링크 누르기 |
| Esc | 자동 이동 멈추기 | 패널 닫기 |
| Tab | 다음 요소로 이동 | 패널 안에서 이동 |
모바일에서는 표지판을 탭하는 방식이 기본입니다. 화면을 끌거나 방향 버튼을 길게 눌러 걸을 수도 있습니다.

폰에서는 가벼운 모드로 시작합니다. 터치 화면, 데이터 절약 모드, 느린 회선, 적은 기기 메모리 중 하나에 해당하면 그림자와 빛 번짐을 끄고 해상도를 낮춥니다. 몇 초 동안 계속 느리면 한 번만 가벼운 모드로 내리고, 1분 동안 입력이 없으면 움직임을 멈춥니다. 운영체제의 “동작 줄이기” 설정도 따릅니다.
공 위에서 걷기
땅이 공 모양이라, 평평한 땅처럼 좌표에 걸음만큼 더하는 방식은 쓸 수 없습니다. 그래서 규칙 두 개를 정했습니다.
- 캐릭터는 늘 공 표면 위에 있다. 한 걸음마다 공을 조금 돌리고 캐릭터를 다시 표면에 올려놓습니다. 오래 걸어도 떠오르거나 땅에 파묻히지 않습니다.
- 머리는 늘 행성 바깥을 향한다. 카메라도 이 방향을 기준으로 따라와서, 행성 반대편에서도 화면이 뒤집히지 않습니다.
가장 어려운 문제는 화면이 조금씩 돌아가는 것이었습니다. 한참 걷다 제자리로 돌아오면 화면 방향이 처음과 달라져 있었습니다. 처음에는 화면 위쪽에 있던 글 보관소가 아래쪽에 가 있는 식입니다. 지금은 지구본을 늘 북쪽이 위로 오게 보듯, 행성의 한 점을 기준으로 화면 위쪽을 정합니다. 그래서 같은 자리에 서면 언제나 같은 화면이 나옵니다.
장소를 누르면 캐릭터가 산책길을 따라 알아서 걸어갑니다. 장소 위치를 바꿀 때마다 모든 장소에 걸어서 갈 수 있는지 테스트로 확인했습니다.
모델 용량과 로딩 줄이기
캐릭터와 소품, 지형은 Blender의 Python 스크립트로 만들었습니다. 모양을 바꾸고 싶으면 숫자만 고쳐 다시 뽑으면 됩니다. 지형 높이 공식은 웹 코드와 Blender 스크립트가 똑같이 써서, 화면의 언덕과 캐릭터가 밟는 높이가 어긋나지 않습니다. 모델은 glTF를 파일 하나로 묶은 GLB로 내보냈습니다. Three.js가 기본 로더를 제공하고 압축 확장을 같은 파일 안에서 쓸 수 있습니다.

첫 버전은 모델 파일이 12MB였고, 느린 회선(4Mbps)에서 탐험을 시작하기까지 25초 가까이 걸렸습니다. 네 단계를 거쳐 줄였습니다.
| 단계 | 내려받는 양 | 탐험 시작 (4Mbps) |
|---|---|---|
| 첫 버전 | 10.75MB | 24.7초 |
| 압축·병렬 로딩 | 3.83MB | 5.6초 |
| 화산·바오밥 추가 | 6.22MB | 10.5초 |
| Draco 전환 | 4.13MB | 8.0초 |
압축. glTF Transform ↗으로 최적화 스크립트를 만들었습니다. 중복 데이터를 합치고, 텍스처를 WebP로 바꾸고, 좌표를 정수로 저장(KHR_mesh_quantization ↗)한 뒤 도형을 한 번 더 압축합니다. 압축 뒤에는 모든 부품의 위치와 크기를 원본과 비교해, 눈에 띄게 달라지면 멈춥니다. 가장 큰 파일은 지형이었고, 6MB 중 2.2MB가 모래 요철 텍스처 한 장이었습니다. 이 텍스처는 크기를 절반으로 줄이고 미세한 알갱이는 셰이더로 덧그렸습니다.
받는 순서. 첫 버전은 파일을 하나 받고 풀고 다음 파일을 받았습니다. 지금은 캐릭터와 소품을 한꺼번에 요청하고, 가장 큰 지형은 소품이 도착한 뒤에 받습니다. 그동안은 코드로 만든 임시 지형을 보여 주고, 진짜 지형이 도착하면 바꿔 끼웁니다.

왼쪽의 임시 지형은 크레이터가 각지고 표면이 매끈합니다. 오른쪽처럼 진짜 지형이 도착하면 둥근 크레이터와 흙 요철이 생깁니다. 캐릭터와 소품은 두 화면 모두 완성본이라, 방문자는 지형이 바뀌는 동안에도 탐험할 수 있습니다.
서버 압축 확인. 화산과 바오밥을 새로 넣자 용량이 다시 늘었습니다. 배포된 사이트의 응답 헤더를 보니, Netlify가 HTML은 brotli로 압축하지만 GLB는 원본 그대로 보내고 있었습니다. 처음 쓴 EXT_meshopt_compression ↗은 서버가 gzip이나 brotli를 한 번 더 걸어 준다는 전제로 데이터를 배치합니다. 로컬에서 brotli를 걸면 30% 더 줄었으니, 그만큼을 버리고 있던 셈입니다. 그래서 파일 자체에서 도형을 강하게 압축하는 Draco ↗(KHR_draco_mesh_compression ↗)로 바꿨습니다. 한 번 방문할 때 받는 모델 15개는 5.50MB에서 3.21MB가 됐습니다. Draco 해제기(약 250KB)를 따로 받지만, 해제는 Web Worker에서 돌아 화면이 멈추지 않습니다.
모델 주소 끝에는 파일 내용의 해시를 붙이고 1년 동안 캐시하게 했습니다. 다시 방문하면 모델을 모두 브라우저 캐시에서 읽습니다. 지금은 화산과 바오밥까지 도착해야 탐험을 시작하는데, 이 두 모델도 임시 모습을 먼저 보여 주면 시작 시점을 더 앞당길 수 있습니다.
Codex와 Claude Code, 누가 무엇을 했나
이 페이지는 두 코딩 에이전트에게 번갈아 맡기며 9월 18일부터 25일까지 만들었습니다. 한쪽이 막히거나 결과가 마음에 들지 않으면 다른 쪽에 넘겼습니다.
- 계획. Codex(GPT-6 Astra)와 아이디어를 나누고 시안 이미지, 설계 문서, 작업 계획을 만들었습니다.
- 첫 구현. Claude Code(Claude Fable 5.1)가 계획을 이어받아 페이지 구조, 공 위 이동, 패널, 접근성, 대체 화면을 태스크별로 구현하고 리뷰했습니다.
- 모양 만들기. 첫 구현이 시안과 거리가 멀어 Codex에 개선을 맡겼습니다. 캐릭터·소품·지형을 Blender 스크립트로 새로 만드는 방식이 여기서 나왔습니다.
- 시안 맞추기와 최적화. 두 도구가 번갈아 구도·지형·표지판을 고쳤고, Claude Code가 로딩 분석과 모델 압축을 맡았습니다. 후반에는 Claude Opus 5.5가 장소 재배치와 조명, 그림자를 맡았습니다.
둘의 강점은 달랐습니다. Claude Code는 계획을 태스크로 쪼개고, 구현자와 리뷰어를 나눠 검토하게 하는 데 강했습니다. 공 위 이동의 수학, 키보드 초점, 로딩 최적화처럼 규칙이 분명하거나 측정할 수 있는 작업이 여기서 단단해졌습니다. Codex는 그림과 모양을 만드는 쪽에서 앞섰습니다. 시안 이미지와 Blender 전환, 화산 모델이 Codex에서 나왔습니다. 다만 Blender로 모양을 시안 수준까지 끌어올리는 작업은 토큰을 많이 썼습니다.
어느 쪽도 시안의 분위기를 알아서 맞춰 주지는 않았습니다. “캐릭터 피부가 너무 어둡다”, “바오밥이 떠 보인다”, “모바일에서는 Enter를 누를 수 없다” 같은 문제는 제가 두 화면을 나란히 놓고 폰으로 직접 만져 보며 찾았습니다. 이렇게 한 문장씩 짚어 주면 에이전트는 그 문장을 모양과 숫자로 빠르게 옮겼습니다. 반대로 “시안처럼”이라고만 하면 추측으로 채운 결과가 나왔습니다.
마치며
요청 한 번으로 완성되는 페이지는 없었습니다. 초반의 계획 → 구현 → 개선은 잘 맞물렸지만, 그 뒤로는 코드를 고치고 화면을 확인하는 일을 오래 반복했습니다. 특히 모바일 구도와 성능은 예상보다 어려웠습니다. 그래도 별도 3D 엔진이나 유료 에셋 없이, Blender + Python + Three.js 조합만으로 웹용 3D를 만들 수 있었습니다.
About 페이지와 전달하는 정보는 같습니다. 달라진 건 방문자가 정보를 만나는 방식입니다. 3D가 불편한 사람도 같은 내용을 똑같이 읽을 수 있습니다. 다음에 비슷한 것을 만든다면 용량과 성능의 한계를 먼저 정하고, 그 안에서 시안을 그리겠습니다.
2026년 9월 26일 수정: 핵심 네 가지(HTML 원본 구조, 공 위 이동, 용량·로딩, 에이전트 협업) 위주로 글을 줄였습니다. 그래픽·애니메이션 세부 기법, 모델 목록, 단계별 작업 표를 빼고, 용량 최적화 수치를 한 표로 합쳤습니다.