DEV.to를 걷는 3D 도서관으로 만들었더니… 디버깅 지옥이 펼쳐졌습니다 😱
2026. 9. 24.
DEV.to를 걷는 3D 도서관으로 만들었더니… 디버깅 지옥이 펼쳐졌습니다 😱


DEV 라이브러리는 DEV.to를 1인칭 3D 공간으로 재해석한 저만의 아이디어입니다. 단순히 몇몇 아티클 카드가 떠다니는 테마형 장면이 아닙니다. Forem API에서 실시간으로 데이터를 가져와 마치 실제로 걸어 다닐 수 있는 거대한 아카이브처럼 구현했죠. 선반들, 방들, 구역들, 그리고 실제 책 표지가 입혀진 책등까지. 피드를 스크롤하는 대신 WASD 키를 사용해 이 공간을 탐색합니다.
문장으로 들으면 정말 근사한 아이디어처럼 들리죠. 하지만 실제로 이걸 구현하는 과정은 예상치 못한 문제들이 연이어 터져 나오고, 제가 그 원인을 하나하나 파헤쳐 나가는 기나긴 여정의 연속이었습니다.
이 모든 건 Sanity x Dev.to 해커톤을 위한 '느낌 가는 대로 코딩(vibe coding)'에서 시작되었습니다. 원하는 바를 묘사하면 시네마틱한 조명, 공간 오디오, 심지어 제가 깊이 고민해보지 않았던 시스템들까지 마법처럼 구현되더군요.
프로젝트가 커지면서 자동 생성된 응답으로는 도저히 해결할 수 없는 방식으로 문제들이 발생하기 전까지는 모든 게 순조로웠습니다. 가짜 아티클 몇 개가 아닌 수천 개의 실시간 아티클 스트림을 감당해야 하는 순간부터, 단순히 문제를 "이렇게 고쳐줘"라고 말하는 방식은 통하지 않았죠. 애니메이션 루프를 열어보고, 참조(ref)를 추적하며, 내부에서 무슨 일이 벌어지고 있는지 직접 이해해야만 했습니다. 제가 원해서가 아니라, "다시 시도해봐"가 더 이상 작동하지 않았기 때문입니다. 오늘 이야기는 주로 그 과정에 대한 것입니다.
기술 스택
이 프로젝트는 Next.js와 Three.js 기반으로 구축되었습니다. 유니티나 언리얼 같은 게임 엔진도, 시각적인 장면 에디터도, 객체를 드래그해 움직임을 확인할 수 있는 뷰포트도 전혀 사용하지 않았습니다. Three.js는 WebGL을 이용해 브라우저에서 3D 그래픽을 렌더링하는 자바스크립트 라이브러리이고, 모든 것이 코드로 이루어져 있습니다. 화살표 키로 X/Y/Z 값을 미세 조정할 수 있는 인스펙터 패널 같은 건 없습니다. 만약 선반이 여섯 칸 너무 높이 떠 있다면, 그걸 드래그해서 내리는 게 아니라 위치를 설정하는 코드 라인을 찾아 숫자를 바꿔야 하죠.
이게 사소한 불편함처럼 들릴지 몰라도, 수백 개의 선반을 구불구불한 절차적 생성 복도에 상대적으로 배치하려고 할 때는 이야기가 달라집니다. 유니티나 언리얼 같은 툴에서는 불일치를 즉시 확인하고 드래그해서 제자리에 놓을 수 있습니다. 하지만 Three.js에서는 좌표를 '눈 감고' 추론해야 합니다. 숫자를 변경하고, 장면이 다시 빌드되기를 기다리고, WASD로 걸어가서 제대로 배치됐는지 확인하고, 아니면 다시 반복하는 식이죠. 이 프로젝트의 모든 위치, 회전, 오프셋은 어딘가 함수 속 숫자 형태로만 존재하며, 눈으로 보고 직접 조작할 수 있는 대상이 아닙니다.
저도 예전에 Three.js로 간단한 인터랙티브 웹사이트를 만들면서 뼈저리게 느꼈죠. 비주얼 툴에서 클릭 한 번이면 될 일을, 숫자 하나 고치고 새로고침하며 수도 없이 시도했던 기억이 납니다. 이런 이유로 아래 설명할 많은 버그가 '무엇처럼 보이는지'보다는 '어디에 있는지'에 관한 것입니다. 좌표 계산은 시각적 스타일링처럼 '느낌 가는 대로 코딩'을 용서하지 않습니다. 색상이 약간 틀어져도 괜찮아 보이지만, 선반 위치가 약간 틀어지면 벽을 뚫고 들어가거나 허공에 떠버리는데, 직접 걸어가 보기 전까지는 알 수 없으니까요.
시작은 여기가 아니었다

이 프로젝트는 '오니리아(Oniria)'라는 이름으로 시작했습니다. 줄여서 '오니(Oni)'라고 불렀죠. 1인칭 3D 꿈 일기였습니다. 기억들이 빛나는 구체처럼 떠다니고, 그 구체들 사이에는 물리적으로 날아다닐 수 있는 관계의 끈이 연결되어 있었습니다. 시네마틱 조명, 공간 오디오, 재귀적인 꿈 다이빙, 기억 소멸, 전체 꿈 기록을 한눈에 볼 수 있는 '관측소' 모드까지, 빠르게 성장했습니다. 정신 차려 보니 저는 main 브랜치보다 197개의 커밋을 앞서 있었고, 1만 3천 줄의 코드 깊이에 빠져 있었습니다. 그리고 명백한 질문을 스스로에게 던져야 했죠. "이게 해커톤 프로젝트에 너무 과한가?"
답변은 문제를 축소하는 대신 재정의했습니다.
"모든 아이디어를 똑같이 중요하게 다룬다면 해커톤에는 너무 큰 범위다. 하지만 그렇다고 오니리아 자체가 '너무 과하다'는 의미는 아니다. 문제는 이제 '제품의 집중도'이지, '야망'이 아니다."
그래서 저는 90초짜리 경험 하나를 선택하고, 나머지 모든 기능을 얼려두었습니다. 그 과정 어딘가에서 제가 실제로 만든 것을 — 즉, 연결된 것들의 그래프를 1인칭으로 날아다니는 것 — 들여다보고 다른 질문을 던졌습니다. "이게 꿈에 대한 것이 아니라면 어떨까?"
처음에는 코드베이스 탐색기 아이디어가 떠올랐습니다 (폴더는 세계, 임포트는 복도). 이 아이디어는 실현되지 않았지만, 무언가를 열어젖혔죠. 이 엔진은 사실 꿈에 대한 것이 아니라, 어떤 '연결되고 탐색 가능한 구조'든 공간으로 만들 수 있는 방법이라는 깨달음이었습니다. 그래서 저는 더 기묘한 것을 요구했습니다. DEV.to 웹사이트 전체를 이 엔진으로 가져와 달라고요. 말 그대로 웹을 '서핑'하는 겁니다.
개발하다 보면 가끔 이런 순간이 옵니다. 원래 의도와는 전혀 다른 방향으로 프로젝트가 흘러가면서 오히려 더 멋진 아이디어가 피어나는 경우 말이죠. 저도 한때 사내 프로젝트를 진행하다가 특정 모듈의 재활용 가능성을 발견하고 완전히 다른 서비스로 확장했던 경험이 있습니다.
문제 #1: 첫 버전은 '음모론 게시판' 같았습니다

기술적으로는 작동했습니다. 아티클, 프로필, 태그, 검색 결과 등 모든 것이 동시에 공간에 떠다녔죠. 기술적으로는 인상 깊었지만, 완전히 탐색 불가능했습니다. 어디를 먼저 봐야 할지 감도 잡히지 않는, 서로 관련 없는 노드들의 구름 속에 스폰되는 느낌이었습니다.
이건 버그라기보다는 디자인 실패에 가까웠지만, 이후의 모든 것에 대한 방향을 설정해주었습니다. 해결책은 새로운 기능이 아니라 '은유'였습니다. 도서관, 즉 방은 주제, 선반은 피드, 책은 아티클. 사람들은 자신만의 방을 가질 수 있게 했습니다. 하나의 아이디어가 제가 그때까지 이루었던 어떤 렌더링 개선보다도 더 큰 유용성을 제공했습니다. 왜냐하면 건축 구조는 떠다니는 라벨이 결코 할 수 없는 방식으로 "어디로 가야 할지"를 알려주기 때문입니다.
이처럼 기술적 구현이 아무리 뛰어나도 사용자 경험을 고려한 직관적인 디자인이 없다면 무용지물이 될 때가 많습니다. 제가 참여했던 한 프로젝트에서도 복잡한 데이터를 시각화하려다 오히려 사용자에게 혼란만 주었던 적이 있었죠. 결국 '은유'를 도입해 해결했던 기억이 생생합니다.
문제 #2: 도서관이 제가 서 있는 동안 스스로 재배열되었습니다


수많은 목업 데이터 대신 실제 DEV 아티클이 스트리밍되기 시작하자, 도서관은 수백 개, 그리고 수천 개의 선반을 필요로 했습니다. 그때 내비게이션이 소리 없이 망가졌죠.
원인은 선반들이 서로 상대적으로 위치를 잡고 있었기 때문이었습니다. 합리적으로 들리지만, 이것은 내비게이션이 세계가 실제로 보장하는 것이 아니라 레이아웃의 부수적인 결과물이었다는 의미였습니다. 아티클 페이지가 하나 더 스트리밍될 때마다, 선반 위치가 플레이어 아래에서 바뀔 수 있었습니다. 일반적인 웹사이트에서 목록을 다시 렌더링하는 것은 시시한 일이지만, 1인칭 공간에서는 그 안에 서 있는 사람을 순간이동시킬 수 있습니다.
이것이 프로젝트 전체가 매달리게 된 핵심 아키텍처 결정입니다.
경로가 절대적이다. 다른 모든 것은 경로를 꾸밀 뿐이다.
하나의 결정론적 통로가 전체 도서관의 좌표 시스템이 되었습니다. archivePathPoint(bay)라는 함수는 논리적인 '베이' 인덱스를 월드 공간 위치로 변환합니다. 선반, 안개, 스카이라인, 에너지 레인, 카메라 클램핑 등 모든 것이 이제 자체적인 위치를 즉흥적으로 결정하는 대신, 동일한 고정 경로에서 샘플링됩니다. 선반 배치 자체도 시드(seed) 기반의 결정론적 함수로 옮겨졌습니다. 안정적인 입력값(선반 ID, 대략적인 베이, 경로 측면, 지터)을 받아, 리액트 상태가 업데이트될 때마다 선반이 새로운 위치로 점프하지 않고도 세계가 여전히 유기적으로 보이도록 했습니다.
그 하나의 규칙이 통째로 한 가지 범주의 버그를 한 번에 해결했습니다. 선반이 세계를 정의하는 요소가 아니게 된 순간부터, 카탈로그를 더 많이 스트리밍하는 것이 더 이상 위험한 일이 아니게 되었습니다. 특히 대규모 데이터를 다루는 프로젝트에서는 이런 '예측 불가능성'이 정말 무서운데요. 저도 한 번은 실시간 데이터를 시각화하는 대시보드를 만들다가, 새로운 데이터가 들어올 때마다 그리드가 요동치는 바람에 사용자들이 멀미를 호소했던 아찔한 경험이 있습니다. 결국 핵심 기준점을 고정하고 나머지 요소를 상대적으로 배치하는 방식으로 해결했었죠.
문제 #3: zoneRef.current is not a function
이 문제는 제가 실제로 앉아서 코드를 한 줄 한 줄 추적하게 만든 주범입니다. 그리고 서두에서 언급했던 변화, 즉 '느낌 가는 대로 코딩'만으로는 더 이상 충분하지 않게 된 그 지점을 가장 명확하게 보여주는 예시이기도 합니다.
Runtime TypeError
zoneRef.current is not a function
900 | if (nearestSection !== currentSection) {
901 | currentSection = nearestSection
902 | zoneRef.current(nearestSection)
zoneRef.current가 함수처럼 호출되고 있었다는 것은, 그 안에 함수가 없었다는 의미였습니다. 애니메이션 루프로 들어가 zoneRef가 실제로 어디에서 할당되는지 추적해 보니, currentSection에 의존하는 useEffect 내부에서 설정되고 있었습니다. 하지만 애니메이션 루프는 해당 이펙트가 처음 실행되기 전에 이미 돌아가고 있었죠. zoneRef가 존재는 했지만, 렌더링 루프가 이를 필요로 하는 시점보다 '아직' 할당되지 않은 '오래된 클로저(stale-closure)' 타이밍 문제였습니다.
이런 종류의 버그는 정말 골치 아프죠. 특히 리액트 훅을 깊이 사용하면서 useEffect의 타이밍이나 클로저 문제가 발생했을 때, 스택 트레이스만으로는 원인을 파악하기 어려울 때가 많았습니다. 제가 실무에서 비슷한 문제를 만났을 때도 결국 애니메이션 루프를 일시 정지시키고 단계별로 변수 값을 추적하며 꼬인 실타래를 풀어나갔던 기억이 생생하네요.
이것이 바로 AI 기반 개발에서 아무도 제대로 준비시켜 주지 않는 부분입니다. AI는 놀랍도록 빠르게 시네마틱 렌더링, 공간 오디오, 그리고 완전히 탐색 가능한 3D 라이브러리를 구현해 줄 수 있습니다. 하지만 이 문제를 해결하는 데 필요했던 "잠깐, 이 이펙트가 렌더링 루프에 비해 언제 실제로 실행되는 거지?"라는 15분간의 고민은 AI가 제공할 수 없습니다. 그 부분은 여전히 직접 해야만 합니다.
현재 상황

이제 카탈로그는 한 번에 모든 것을 가져오는 대신, 페이지 단위로 스트리밍됩니다. 선반들은 콘텐츠가 더 로드될 때마다 위치가 뒤섞이지 않도록 결정론적으로 시드(seed)됩니다. 카메라는 장면이 다시 빌드되어도 원래 위치를 유지하고, 구역들은 고정된 분류 체계 대신 실제 아티클 태그별로 스스로를 조직합니다. 가까운 선반들은 반응하며 밝아지고 표지를 '깨우는' 반면, 멀리 있는 선반들은 어두워지는데, 이것이 예상했던 것보다 훨씬 더 중요했습니다. 2D에서는 타이포그래피와 레이아웃에서 계층 구조가 나오지만, 수십 개의 선반이 한 번에 보이는 3D 아카이브 내부에서는 거리와 밝기만이 유일한 계층 구조를 제공하니까요.
이런 섬세한 디테일 하나하나가 사용자 경험에 미치는 영향이 정말 큽니다. 저도 웹 기반 게임을 개발하면서 초반에는 성능 최적화에만 몰두했는데, 막상 출시하고 나니 사소한 시각적 피드백이나 인터랙션 디자인이 사용자 몰입도를 훨씬 높여준다는 걸 깨달았죠.
아직 완성된 것은 아닙니다. 멀리 있는 선반들이 프레임 시간을 너무 많이 소모하기 전에 얼마나 많은 카탈로그를 로드 상태로 유지할 수 있을지 계속 고민 중입니다. 스카이라인과 안개 시스템은 조정되었지만, 성능이 약한 GPU에서도 제대로 작동하는지 검증되지는 않았습니다. 그리고 아직 제가 마주치지 않은 zoneRef 모양의 버그가 어딘가에 적어도 하나는 더 있을 것이라고 확신합니다.
하지만 처음으로, 이 프로젝트는 제가 만들고자 했던 대로 작동하고 있습니다. DEV.to 테마의 장면이 아니라, 살아있는 카탈로그 전체를 실제로 걸어 다닐 수 있는 공간으로 만들려는 시도. 망가진 참조 하나, 깜빡이는 표지 하나, 그리고 화면을 가득 채우는 책 한 권을 고쳐나가면서 말이죠.
곧 더 많은 소식을 전해드리겠습니다. 특히 이 프로젝트가 저의 Sanity 챌린지 제출작으로 전환될 때, CMS 부분이 조연이 아닌 주연이 되는 이야기가 펼쳐질 겁니다.
원문: https://dev.to/mikachu/i-turned-devto-into-a-walkable-3d-library-debugging-it-has-been-a-nightmare-4lkd 수집일: 2026-09-24 01:51:57