RicoCheese기술 뉴스와 기록
← 목록으로
뉴스2026-09-2316분

DEV.to를 걸어 다니는 3D 도서관으로 만들었다 — 디버깅은 악몽이었다

개발자 Mika Flowers가 Forem API로 받아온 DEV.to 실시간 카탈로그를 1인칭 3D 도서관으로 구현한 과정을 공개했다. 바이브 코딩으로 시작했지만 결국 애니메이션 루프를 직접 파고들어야 했던 이유를 정리했다.

#webdev#javascript#react#frontend#ai
I Turned DEV.to Into a Walkable 3D Library — Debugging It Has Been a Nightmare

개요 #

개발자 Mika Flowers가 DEV.to를 1인칭 3D 도서관으로 다시 만든 프로젝트 'DEV Library'를 공개했다. 아티클 카드 몇 장이 떠다니는 테마 씬이 아니라, Forem API에서 실시간으로 받아온 실제 카탈로그를 책장과 방과 구역으로 렌더링한 공간이다. 책등에는 진짜 커버 이미지가 붙고, 피드를 스크롤하는 대신 WASD로 걸어 다닌다.

Dev

earlyversion

한 문장으로 옮기면 즐거운 아이디어다. 그런데 실제로 만드는 과정은 예상하지 못한 방식으로 무언가가 계속 깨지고, 그 원인을 하나씩 찾아내는 일의 연속이었다고 그는 말한다.

시작은 Sanity x Dev.to 해커톤을 겨냥한 바이브 코딩이었다. 원하는 것을 설명하면 그대로 나왔다. 시네마틱 조명, 공간 오디오, 본인도 끝까지 생각해보지 않았던 시스템까지 통째로. 문제는 프로젝트가 일정 규모를 넘어서는 순간이었다. 목업 열댓 개가 아니라 수천 건의 실시간 아티클이 스트리밍으로 흘러들어오는 씬을 버텨야 하자, 설명만으로 빠져나갈 수 없는 문제가 나오기 시작했다. "다시 해줘"가 통하지 않게 된 시점부터 그는 애니메이션 루프를 열고 ref를 따라가며 내부 동작을 직접 이해해야 했다.

게임 엔진 없이 Three.js로만 #

기술 스택은 Next.js와 Three.js다. 게임 엔진도, 비주얼 씬 에디터도, 오브젝트를 끌어다 놓고 움직임을 확인할 뷰포트도 없다. Three.js는 WebGL로 브라우저에서 3D 그래픽을 렌더링하는 자바스크립트 라이브러리이고, 모든 것이 코드다. X/Y/Z 값을 방향키로 미세 조정할 인스펙터 패널 같은 건 없다. 책장이 6유닛 위로 떠 있으면 마우스로 내리는 게 아니라, 위치를 지정하는 코드 줄을 찾아 숫자를 바꿔야 한다.

사소한 불편처럼 들리지만, 곡선을 그리며 절차적으로 생성되는 복도를 기준으로 책장 수백 개를 배치해야 한다면 얘기가 달라진다. Unity나 Unreal이라면 어긋난 부분이 바로 눈에 보이고 끌어다 맞추면 된다. Three.js에서는 좌표를 눈으로 확인하지 못한 채 추론해야 한다. 숫자를 바꾸고, 씬이 다시 빌드되기를 기다리고, WASD로 직접 걸어 들어가 맞는지 확인하고, 아니면 그 과정을 반복한다.

이 프로젝트의 버그 상당수가 '어떻게 보이는가'가 아니라 '어디에 있는가'에 관한 것인 이유가 여기 있다. 색이 조금 어긋나도 보기에 멀쩡하지만, 책장 위치가 조금 어긋나면 벽을 뚫고 나가거나 허공에 떠버린다. 게다가 어느 쪽인지는 직접 그 지점까지 걸어가기 전에는 알 수 없다. 좌표 수학은 시각 스타일링만큼 바이브 코딩에 관대하지 않았다.

출발점은 도서관이 아니었다 #

books!

프로젝트의 원형은 'Oniria'(줄여서 Oni)라는 1인칭 3D 꿈 일기였다. 기억은 빛나는 구체로, 기억 사이의 관계는 실제로 날아서 이동할 수 있는 연결선으로 표현했다. 여기에 시네마틱 조명, 공간 오디오, 재귀적 꿈 탐험, 기억 감쇠, 꿈 이력 전체를 한눈에 조망하는 'Observatory' 모드가 빠르게 붙었다. 정신을 차려보니 main 브랜치보다 커밋 197개가 앞서 있었고 코드는 1만 3천 줄에 달했다. 해커톤 하나에 이건 과하지 않은가.

돌아온 답은 규모를 줄이라는 게 아니라, 문제를 다시 정의하라는 쪽이었다.

"모든 아이디어를 똑같이 중요하게 다룬다면 해커톤에는 범위가 너무 크다. 하지만 그게 Oniria 자체가 '과하다'는 뜻은 아니다. 지금 문제는 야심이 아니라 제품 초점이다."

그래서 90초짜리 경험 하나를 고르고 나머지는 전부 그 뒤로 묶어뒀다. 그 과정에서 자신이 실제로 만든 것 — 서로 연결된 것들의 그래프를 1인칭으로 날아다니는 경험 — 을 들여다보다가 다른 질문이 떠올랐다. 이게 꼭 꿈에 관한 것일 필요가 있을까?

먼저 나온 건 코드베이스 탐색기 아이디어였다(폴더는 세계, import는 복도). 출시로 이어지지는 않았지만 중요한 걸 열어줬다. 이 엔진의 본질은 꿈이 아니라, 연결되고 탐색 가능한 구조라면 무엇이든 하나의 장소로 바꿔놓는 방식이라는 것. 그래서 더 이상한 걸 요구했다. DEV.to 사이트 전체를 엔진 안으로 들여오는 것, 말 그대로 웹을 서핑하는 것.

첫 번째 문제: 음모론자의 상황판 #

dev library

기술적으로는 동작했다. 아티클, 프로필, 태그, 검색 결과가 전부 공간에 동시에 떠 있었다. 인상적이긴 했다. 그리고 완전히 탐색 불가능했다. 서로 관계없는 노드 구름 한가운데 떨어져서 어디부터 봐야 할지 감을 잡을 수 없었다.

엄밀히 말해 버그라기보다 설계 실패였지만, 이후 모든 작업의 기조를 정했다. 해법은 새 기능이 아니라 비유였다. 도서관. 방은 주제, 책장은 피드, 책은 아티클. 사람에게는 각자의 방이 주어진다. 이 아이디어 하나가 그때까지 쌓아온 렌더링 개선을 전부 합친 것보다 사용성을 크게 끌어올렸다. 건축은 떠다니는 라벨이 결코 하지 못하는 방식으로 어디로 갈지를 알려주기 때문이다.

사람들이 길을 걷는 모습 Photo by Mark Amores on Pexels

두 번째 문제: 서 있는 동안 도서관이 재배치됐다 #

dev2

dev

목업 몇 개 대신 실제 DEV 아티클이 스트리밍되기 시작하자 도서관에는 책장 수백 개, 곧 수천 개가 필요해졌다. 그 시점에 탐색이 조용히 망가졌다.

원인은 책장이 서로를 기준으로 위치를 잡고 있었다는 데 있었다. 그럴듯하게 들리지만, 이 말은 탐색이 세계가 보장하는 속성이 아니라 레이아웃에서 파생된 부수 효과였다는 뜻이다. 아티클이 한 페이지 더 들어올 때마다 플레이어 발밑에서 책장 위치가 움직일 수 있었다. 일반 웹사이트에서 리스트를 다시 렌더링하는 건 지루한 일이다. 1인칭 공간에서는 그 안에 서 있는 사람을 순간이동시킨다.

프로젝트 전체가 매달리게 된 아키텍처 결정이 여기서 나왔다.

경로가 권위를 가진다. 나머지는 모두 경로를 장식할 뿐이다.

결정론적 통로 하나가 도서관 전체의 좌표계가 됐다. archivePathPoint(bay) 함수가 논리적 'bay' 인덱스를 월드 좌표로 변환한다. 책장, 안개, 스카이라인, 에너지 레인, 카메라 클램핑까지 전부 각자 위치를 즉흥적으로 만들어내는 대신 이 고정된 경로에서 값을 샘플링한다. 책장 배치 자체도 시드 기반 결정론적 함수로 옮겨서, 책장 ID·대략적인 bay·경로의 어느 쪽인지·지터 같은 안정적인 입력을 받도록 했다. React 상태가 갱신될 때마다 책장이 새 자리로 튀지 않으면서도 세계는 여전히 유기적으로 보인다.

이 규칙 하나가 버그 한 부류를 통째로 없앴다. 책장이 세계를 정의하는 존재이기를 멈춘 순간, 카탈로그를 더 많이 스트리밍해도 위험하지 않게 됐다.

세 번째 문제: zoneRef.current is not a function #

한 번 더 요청하는 대신 자리에 앉아 코드를 한 줄씩 따라가게 만든 버그다. 앞서 말한 전환 — 바이브 코딩만으로는 부족해지는 지점 — 을 가장 선명하게 보여준 사례이기도 하다.

javascript
Runtime TypeError
zoneRef.current is not a function

900 |  if (nearestSection !== currentSection) {
901 |    currentSection = nearestSection
902 |    zoneRef.current(nearestSection)

zoneRef.current를 함수처럼 호출했다는 건 그 안에 함수가 들어 있지 않았다는 뜻이다. 애니메이션 루프로 들어가 zoneRef가 실제로 어디서 할당되는지 추적해보니, currentSection을 의존성으로 가진 useEffect 안에서 설정되고 있었다. 그런데 애니메이션 루프는 그 effect가 처음 실행되기 전에 이미 돌아가는 중이었다. ref는 존재했다. 다만 렌더 루프가 그것을 찾는 시점 기준으로는 아직 아니었을 뿐이다. 전형적인 stale closure 타이밍 문제다.

AI 보조 개발에서 아무도 제대로 알려주지 않는 부분이 이 지점이라고 그는 말한다. AI는 시네마틱 렌더링, 공간 오디오, 완전히 탐험 가능한 3D 도서관을 놀라운 속도로 건네준다. 하지만 "잠깐, 이 effect는 렌더 루프 기준으로 정확히 언제 실행되지?"를 붙들고 보낸 15분은 건네주지 못한다. 그 부분은 여전히 직접 해야 한다.

지금 상태 #

Reading Dev Book

지금 카탈로그는 요청 하나로 전체를 막아두는 대신 페이지 단위로 나눠 스트리밍된다. 책장은 시드 기반으로 배치돼 콘텐츠가 더 로드돼도 뒤섞이지 않는다. 카메라는 씬이 재구축돼도 살아남는다. 구역은 고정된 분류 체계가 아니라 실제 아티클 태그를 따라 스스로 정리된다. 가까운 책장은 밝아지며 커버를 깨우고, 먼 책장은 어두워진다. 이 부분이 생각보다 중요했다고 그는 덧붙였다. 2D에서는 타이포그래피와 레이아웃이 위계를 만들지만, 책장 수십 개가 한눈에 들어오는 3D 아카이브 안에서는 거리와 밝기가 유일한 위계 수단이기 때문이다.

아직 끝나지 않았다. 멀리 있는 책장이 값어치보다 더 많은 프레임 타임을 잡아먹기 전까지 카탈로그를 얼마나 로드된 상태로 둘 수 있는지는 계속 실험 중이다. 스카이라인과 안개 시스템은 튜닝은 마쳤지만 성능이 낮은 GPU에서 검증하지 못했다. 그리고 아직 걸어 들어가 보지 않은 어딘가에 zoneRef 모양의 버그가 최소한 하나는 더 숨어 있을 거라고 그는 본다.

그럼에도 처음으로 원래 만들려던 것처럼 동작하기 시작했다. DEV.to를 테마로 삼은 씬이 아니라, 살아 있는 카탈로그 전체를 실제로 걸어 다닐 수 있는 장소로 만들려는 시도. 망가진 ref 하나, 깜빡이는 커버 하나, 화면을 집어삼키는 책 한 권씩 고쳐가면서.

Sanity 챌린지 제출작으로 이어질 때의 이야기도 이어서 공개할 예정이다. 그때는 CMS 쪽이 조연이 아니라 주인공이 된다.


이 글은 위 출처를 바탕으로 한국 독자를 위해 재작성한 기사입니다. 원문의 사실과 수치에 근거하며, 별도의 견해를 포함하지 않습니다.

댓글GitHub Discussions