GITHUB 레포 딥다이브 · 2026-08-04 · THEBUGGEDDEV/ANATOMY · 9개 장기를 브라우저에서 돌려보고 클릭해 배우는 "Anatomy Atelier" — AI가 뽑아낸 앱인데 three.js가 진짜다

anatomy 딥다이브
"AI가 만든 3D 해부학 앱"을 뜯어보니, 레이캐스팅을 버린 영리한 three.js가 있었다

thebuggeddev/anatomy(앱 이름 "Anatomy Atelier")는 심장·뇌·폐·간·신장·눈·장·췌장·피부 9개 장기를 브라우저에서 3D로 돌려보고, 표면의 점을 클릭해 이름·기능·질환을 배우는 인터랙티브 해부학 학습 웹앱이다. 설치도, 로그인도 없이 웹페이지 하나로 돈다.

기술적으로는 Next.js 16 + React 19 + three.js로 짜였는데, 흥미로운 사실이 하나 있다 — 이 앱은 OpenAI의 "사이트 생성기"(코드명 Sol / site-creator)가 뽑아낸 스타터 템플릿 위에 얹혀 있다. 즉 "AI 에이전트가 만든 웹앱"의 표본이기도 하다. 그런데 그 속을 열어 보면, 장난감이 아니라 실무급 three.js 엔지니어링이 들어 있다.

이 레포의 매력은 두 문장으로 요약된다 — "3D 클릭을 무거운 레이캐스팅이 아니라 화면 좌표 거리로 푼다", 그리고 "가만히 있을 땐 GPU를 한 톨도 안 쓴다(그릴 게 있을 때만 그린다)." 이 두 가지 결정이 이 앱을 노트북·폰에서도 부드럽게 돌아가게 만든다. 3D 웹을 배우려는 사람에게는 "교과서보다 뜯어보기 좋은 실제 코드"다.

(저장소 thebuggeddev/anatomy · 라이선스 없음(명시 안 됨 = 기본 저작권) · 언어 TypeScript · 3D 엔진 three.js 0.185 · 프레임워크 Next.js 16 + React 19 · 최신 커밋 2026-08-02 · 제작 thebuggeddev · 장기 모델 9개(~29MB) · TrendShift Daily #14)
목차
  1. 한 줄 정체
  2. 왜 주목받는가 — "AI가 만든 앱"인데 엔지니어링이 진짜
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 — 명령형 엔진 + 네 개의 영리한 결정
  5. 디렉토리 구조 해부
  6. 학습 포인트 — 기술별 배울 것
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 로드맵 (주차별)
  10. 핵심 키워드 사전
  11. 참고 링크

1한 줄 정체

이 프로젝트가 정확히 무엇인지 한 문장으로

anatomy(Anatomy Atelier)는 "9개 인체 장기의 3D 모델을 브라우저에서 자유롭게 돌려보고, 표면의 라벨 점을 클릭하면 그 부위의 이름·기능·관련 질환이 뜨는, 설치가 필요 없는 웹 해부학 학습 앱"이다. 한마디로 "손으로 만지며 배우는 3D 인체 도감"이다. 의료 기록을 다루거나 진단을 하는 도구가 아니라, 순수하게 "보고 돌려보고 클릭하며 익히는" 교육용 시각화가 목적이자 결과물이다.

그리고 한 겹 더 있다. 이 앱의 package.json 이름은 site-creator-vinext-starter이고 호스트는 anatomy-atelier.openai.site다. 즉 이건 OpenAI의 코딩 에이전트가 "해부학 학습 사이트 만들어 줘"라는 주문을 받아 생성한 결과물이라는 뜻이다. 그래서 이 레포는 두 가지로 읽힌다 — (1) 잘 만든 3D 해부학 교보재이자, (2) "요즘 AI가 앱을 만들면 이 정도 나온다"의 실물 샘플.

한 장의 비유

"박물관의 회전 유리 진열대 — 만지면 설명이 뜨는 홀로그램 장기"

과학관에 가면 유리 진열대 안에 인체 장기 모형이 떠 있고, 손잡이를 돌리면 모형이 빙글 돈다. 표면의 작은 버튼을 누르면 "여기는 좌심실, 온몸으로 피를 내보내는 펌프"라고 설명 카드가 튀어나온다. Anatomy Atelier가 웹에서 하는 일이 정확히 이것이다.

그런데 진짜 재미있는 건 진열대 뒤편의 기계 장치다. 보통은 "손님이 어느 버튼을 눌렀나"를 알아내려고 모형 속으로 광선을 쏴서 맞히는 방식(레이캐스팅)을 쓴다 — 정확하지만 비싸다. 이 앱은 대신 "화면에 찍힌 버튼들 중 손가락과 가장 가까운 걸 고른다"는 훨씬 싼 방법을 쓴다. 손님은 차이를 못 느끼지만, 기계는 훨씬 덜 뜨겁다. 이 "티 안 나게 영리한 지름길"들이 이 레포의 진짜 볼거리다.

용어
three.js
브라우저에서 3D 그래픽을 그리는 가장 널리 쓰이는 자바스크립트 라이브러리. 브라우저의 저수준 3D API인 WebGL을 사람이 다루기 쉽게 감싼 것이다. "장면(scene)에 물체(mesh)·카메라(camera)·조명(light)을 놓고 매 프레임 그린다"는 게 기본 골격. 이 앱은 three.js 0.185 버전을 쓴다. 게임 엔진(Unity·Unreal)의 웹 경량판이라고 생각하면 편하다.
용어
Anatomy Atelier · "site-creator" · Sol
Atelier(아틀리에)는 "공방·작업실"이라는 뜻으로, 앱이 스스로 붙인 이름이다. site-creator는 이 앱의 뼈대가 된 OpenAI의 사이트 자동 생성 템플릿이고, Sol은 그 생성을 수행한 에이전트/모델의 코드명이다(TrendShift 소개문의 "GPT 5.6 Sol"). 정리하면: 사람이 주제(해부학)를 주면, AI가 이 스타터를 바탕으로 앱을 조립한 것. 그래서 저장소 곳곳에 쓰지 않는 스타터 부속품(D1 데이터베이스 예제, ChatGPT 로그인 코드 등)이 남아 있다.

왜 이 앱이 "가벼운데 부드럽다"는 인상을 주는지, 숫자로 감을 잡아 보자. 3D 웹앱이 무거워지는 두 지점 — 매 프레임 그리기클릭 판정 — 을 이 앱이 어떻게 비껴갔는지가 요약돼 있다.

┌──────────────────────────────────────────────────────────────────┐ │ 보통의 3D 웹앱 vs. Anatomy Atelier │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ [ 그리기 ] │ │ 보통: 가만히 있어도 초당 60번 계속 그림 → GPU 계속 뜨거움 │ │ 이 앱: 움직임이 있을 때만 그림(dirty) → 멈추면 GPU ≈ 0 │ │ │ │ [ 클릭 판정 ] │ │ 보통: 장기 표면에 광선을 쏴 삼각형과 충돌 계산 (레이캐스팅) │ │ 이 앱: 화면에 찍힌 점들 중 24px 안 가장 가까운 것 (화면좌표) │ │ │ │ [ 라벨 위치 옮길 때 ] │ │ 보통: React 상태 갱신 → 리렌더 → 느려짐 │ │ 이 앱: DOM 좌표만 직접 이동(translate3d) → 리렌더 0 │ │ │ │ 결과: 노트북·폰에서도 발열·끊김 없이 매끈하게 회전 │ │ │ └──────────────────────────────────────────────────────────────────┘

이 세 줄의 "지름길"이 이 레포를 공부할 값어치로 만든다. 화려한 결과물(예쁜 3D 장기) 뒤에 "어떻게 하면 3D를 싸게 돌릴까"라는 실전 기법이 층층이 깔려 있기 때문이다. 아래에서 하나씩 뜯어본다.

2왜 주목받는가 — "AI가 만든 앱"인데 엔지니어링이 진짜

비슷한 스킬·데모가 쏟아지는 트렌딩 목록에서 이게 상위(#14)에 오른 이유

첫째, "AI가 뽑아낸 앱"이라는 화제성이다. 2026년 트렌딩 목록은 온통 "AI 스킬·에이전트·코딩 도구"로 가득한데, 이 앱은 그 흐름의 결과물을 눈으로 보여 준다. "OpenAI Sites/Sol이 만들었다는데, 이게 이 정도 퀄리티라고?"라는 놀라움이 클릭을 부른다. 추상적인 "AI가 코딩한다"가 아니라, 돌아가는 예쁜 3D 앱이라는 구체물이라서 설득력이 있다.

둘째, 결과물 자체가 예쁘고 즉시 만족스럽다. 따뜻한 "아틀리에" 색감, 부드러운 GSAP 애니메이션, 장기가 스르륵 등장하고 카메라가 다가가는 연출 — 3D 웹은 보통 "데모는 멋진데 실제로 열면 버벅"이기 쉬운데, 이 앱은 설치 없이 링크만 열면 바로 매끄럽게 돈다. "보는 재미"가 즉각적이라 공유가 잘 된다.

비유

자동 반죽기가 구운 빵. "기계가 자동으로 빵을 굽는다"는 말은 흔하지만, 실제로 나온 빵이 먹을 만한지는 별개다. 대부분의 자동 빵은 모양만 빵이고 속은 설익었다. 그런데 이 빵은 잘라 보니 결이 살아 있고 속까지 잘 익었다. 사람들이 놀라는 건 "기계가 구웠다"가 아니라 "기계가 구웠는데 이게 진짜 빵이네"다. Anatomy Atelier의 화제성도 같은 지점이다 — AI가 만들었다는 사실보다, 만든 결과가 실제로 잘 만들어졌다는 사실.

셋째, 속을 열면 배울 게 많다. 이게 개발자들이 별을 누르는 진짜 이유다. react-three-fiber(리액트용 three.js 래퍼) 없이 순수 three.js를 명령형 클래스로 다루고, 클릭 판정을 레이캐스팅 대신 화면좌표로 풀고, 안 움직일 땐 렌더를 멈추고, 투명 페이드를 깊이 프리패스로 처리한다 — 이 기법들은 "3D 웹을 실무 수준으로 최적화하는 법"의 살아 있는 예제다. 코드가 잘 정리돼 있고 주석도 충실해서, 뜯어보며 배우기에 좋다.

넷째, 실용적 틈새를 정확히 찌른다. 웹에서 3D 해부학을 보려면 보통 무겁고 유료인 상용 서비스(BioDigital 계열)나, 계정·플러그인이 필요한 도구를 거쳐야 했다. 이 앱은 "9개 핵심 장기만, 로그인 없이, 즉시"라는 좁고 명확한 목표에 집중한다. 의대생·간호학생·중고생·호기심 많은 일반인이 부담 없이 열어 보기에 딱 맞는 크기다.

비교 대상성격Anatomy Atelier의 위치
BioDigital Human상용·방대(수천 구조)·계정 필요·무거움훨씬 가볍고 무료, 대신 9개 장기로 범위 좁음
Sketchfab 임베드범용 3D 뷰어(주석·설명 UI 없음)해부학 학습 UI(라벨·질환·비교)가 처음부터 내장
Z-Anatomy 등 오픈강력하지만 데스크톱·Blender 기반, 진입장벽브라우저 링크 하나, 설치 0
직접 만든 three.js 데모대개 성능·접근성 미흡render-on-demand·키보드·스크린리더까지 챙김
이 레포의 진짜 강점
"AI가 만든 예쁜 결과물"과 "뜯어볼 만한 실전 코드"가 한 몸

보통 화제성 있는 데모는 겉만 번지르르하고 코드는 엉성하다. 이 레포는 그 반대에 가깝다 — AI가 생성한 앱인데도 성능·접근성·자원 정리(GPU 메모리 해제)까지 챙긴 구석이 많다. "AI 생성물이라 대충일 것"이라는 편견을 열어 보며 깨는 재미, 그리고 그 안에서 순수 three.js 최적화 기법을 통째로 배우는 실속. 이 둘의 조합이 트렌딩 상위에 오른 이유다.

과장 주의 (정직한 경계)
"완성된 제품"이 아니라 "잘 뽑힌 데모" — 껍데기와 미완성이 섞여 있다

냉정하게 봐야 할 것들이 있다. ① README가 앱과 무관하다 — 스타터 보일러플레이트("vinext-starter", ChatGPT 로그인, D1 DB) 그대로라서 해부학 얘기가 한 줄도 없다. ② 라이선스가 없다(LICENSE 파일 부재 = 기본적으로 모든 권리 보유, 자유 사용 불가). ③ 3D 장기 모델의 출처·저작권 표기가 전혀 없다 — 어디서 온 모델인지 불명이라, 그대로 가져다 쓰기엔 위험하다. ④ 화면의 일부 기능은 껍데기다: "Layers(레이어)"는 사실 와이어프레임 토글일 뿐 골격계·근육계 같은 진짜 해부 계통 전환이 아니고, Compare·Quiz·Animation·Lessons 상당수가 정적 모달(내용 없는 UI 틀)이다. ⑤ 상단 "검색"은 장기 목록 필터이지 부위 이름 검색이 아니다. 즉 "배우는 교보재"보다 "구경하고 코드 뜯는 샘플"로 접근하는 게 정확하다.

3기술 스택 전체 지도

"2026년 최신 웹 스택 총집합" — Next 16·React 19·Tailwind v4·three.js, 그리고 엣지 런타임까지

이 앱의 스택은 거의 모든 부품이 최신 버전이라는 점에서 그 자체로 "2026년 프론트엔드 지형도"다. AI 생성기가 최신 스타터를 썼기 때문에, 지금 시점의 "권장 조합"이 그대로 박제돼 있다. 크게 세 층 — 화면(프론트), 빌드, 인프라 — 으로 나눠 본다.

프론트엔드 (화면을 그리는 것)

영역무엇을 쓰나역할
프레임워크Next.js 16.2 (App Router + RSC)페이지·서버 렌더링 뼈대. 첫 화면은 서버가 그려 보내고 3D는 클라이언트에서
UI 런타임React 19.2화면 상태·컴포넌트. 3D는 React 밖(명령형)에서 돌리고 얇게만 연결
3D 엔진three.js 0.185WebGL 위의 3D. 장면·카메라·조명·모델 로딩·렌더 루프 전부
애니메이션GSAP 3.15장기 등장·카메라 이동·툴 전환의 부드러운 연출(easing)
아이콘lucide-react 1.28툴바·UI 아이콘 세트
스타일Tailwind CSS v4 + 커스텀 globals.css(562줄)따뜻한 "아틀리에" 디자인 시스템. v4는 설정을 CSS에서 함
언어TypeScript 5.9전부 타입 지정. 특히 장기 데이터가 엄격히 타입화됨
폰트next/font — DM Sans + Cormorant Garamond본문 산세리프 + 제목 세리프의 "공방" 무드
용어
RSC(React Server Components) · "3D는 클라이언트에서"
RSC는 컴포넌트를 서버에서 미리 그려 HTML로 보내는 방식이다(첫 로딩이 빠르고 SEO에 좋다). 하지만 3D(WebGL)는 브라우저의 GPU가 필요하니 서버에서 못 그린다. 그래서 이 앱은 첫 화면(로딩 스켈레톤·레이아웃)은 서버가 그리고, three.js 뷰어는 브라우저에 도착한 뒤 동적 import로 뒤늦게 불러온다(import("../lib/three/viewer")). 이렇게 하면 무거운 three.js가 첫 번들에 안 실려 초기 로딩이 가벼워진다. "서버가 할 일과 브라우저가 할 일을 가른다"의 좋은 예.

빌드 도구 (코드를 실행 가능한 형태로)

영역무엇을 쓰나역할
번들러Vite 8 + vinext 0.0.50vinext = Vite로 Next.js를 Cloudflare에서 돌리는 런타임
린트ESLint(eslint-config-next)코드 규칙 검사
테스트node --test (tests/rendered-html.test.mjs, 91줄)서버 렌더 결과에 로딩 스켈레톤이 들어갔는지 검증

인프라 · 배포 (어디서 돌리나)

영역무엇을 쓰나역할
서버 런타임Cloudflare Workers (@cloudflare/vite-plugin, wrangler 4)사용자 근처 엣지에서 페이지를 서빙(worker/index.ts, 47줄)
DB(선택)Drizzle ORM + Cloudflare D1(SQLite)스타터 부속. 이 앱은 실제로 안 씀(db/schema.ts 비어 있음)
배포 이중화Cloudflare(vinext) + Vercel(vercel.jsonnext build)두 곳 모두에 배포 가능하게 설정됨
런타임 요구Node ≥ 22.13개발·빌드 환경 기준

상태 관리에 주목할 점 하나 — Redux도 Zustand도 없다. 3D의 모든 상태는 AnatomyApp.tsx 안의 평범한 React useState/useRef로 관리된다. 복잡한 상태 라이브러리를 끌어오는 대신, "React는 UI만, 3D 내부 상태는 three.js 쪽 객체가 직접"이라는 역할 분리로 단순함을 유지한 것이다.

스택에서 배우는 것
"react-three-fiber를 안 쓴다"는 선택이 이 앱의 성격을 규정한다

리액트로 three.js를 다룰 때 요즘은 react-three-fiber(r3f)로 3D를 JSX처럼 선언적으로 쓰는 게 유행이다. 그런데 이 앱은 일부러 r3f 없이 three.js를 명령형 클래스로 직접 다룬다. 덕분에 "React 리렌더가 3D 성능을 갉아먹는" 흔한 함정을 원천 차단하고, 렌더 루프·자원 정리를 손끝까지 제어한다. r3f의 편리함 대신 성능·제어권을 택한 셈 — 어느 쪽이 옳다기보다, "프레임워크에 3D를 맡기지 않고 직접 쥐는 법"을 통째로 보여 주는 귀한 표본이다.

4아키텍처 심화 — 명령형 엔진 + 네 개의 영리한 결정

이 레포의 진짜 알맹이. "React는 얇게, three.js는 두껍게"라는 뼈대와, 성능을 만든 네 가지 트릭

축 0 — 전체 구조: React 껍질 + three.js 알맹이

이 앱의 뼈대는 "두꺼운 명령형 3D 엔진을, 얇은 React 다리로 연결"하는 구조다. React는 버튼·패널 같은 UI만 그리고, 실제 3D는 AnatomyViewer라는 클래스가 React 바깥에서 독립적으로 돌린다. 둘 사이는 OrganViewer.tsx라는 얇은 다리(bridge) 하나로만 이어진다.

┌─────────────────────────────────────────────────────────────────────┐ │ 브라우저 화면 │ │ │ │ ┌───────────────────────────┐ ┌──────────────────────────────┐ │ │ │ React UI (선언적) │ │ three.js 엔진 (명령형) │ │ │ │ │ │ │ │ │ │ AnatomyApp.tsx (334줄) │ │ lib/three/viewer.ts (642줄) │ │ │ │ · 장기 라이브러리 목록 │ │ · Scene / Camera / Light │ │ │ │ · 정보 패널 · 카드 · 모달│ │ · OrbitControls │ │ │ │ │ │ │ · render-on-demand 루프 │ │ │ │ ▼ │ │ ▲ │ │ │ │ OrganViewer.tsx (188줄) │◀──▶│ hotspots.ts (371줄) │ │ │ │ ★ React ↔ three 다리 │ 명령 │ · 스프라이트 점 · 화면좌표픽 │ │ │ │ · 툴 버튼 · 콜아웃 위치 │ 전달 │ loaders.ts (213줄) │ │ │ └───────────────────────────┘ │ · GLB 로드 · LRU 캐시 │ │ │ │ └──────────────────────────────┘ │ │ ▼ │ │ │ anatomy-data.ts (311줄) ── 9개 장기 + 핫스팟 │ │ │ (이름·기능·질환·좌표의 단일 진실원) ▼ │ │ public/models/*.glb (9개, ~29MB) │ └─────────────────────────────────────────────────────────────────────┘

이 그림의 핵심은 화살표가 한 곳에만 있다는 것이다 — React와 three.js가 OrganViewer.tsx에서만 만난다. 나머지는 서로를 모른다. 그래서 3D 내부가 아무리 바빠도 React는 리렌더되지 않고, 반대로 React UI가 바뀌어도 3D 루프는 방해받지 않는다. 이제 이 엔진을 빠르게 만든 네 개의 결정을 하나씩 본다.

결정 ① — 클릭 판정을 "레이캐스팅" 대신 "화면좌표 거리"로

가장 독특한 선택이자 이 앱의 상징이다. 3D에서 "사용자가 어느 부위를 클릭했나"를 알아내는 표준 방법은 레이캐스팅 — 마우스 위치에서 3D 공간으로 광선을 쏘고, 그 광선이 장기 표면의 어느 삼각형과 부딪히는지 계산하는 것이다. 정확하지만, 수만 개 삼각형과의 충돌을 매번 따져야 해서 비싸다.

용어
레이캐스팅(raycasting) vs. 화면좌표 픽킹(screen-space picking)
레이캐스팅은 마우스에서 3D 세계로 가상의 광선을 쏴 물체와의 교차점을 찾는다(3D 공간 계산). 화면좌표 픽킹은 반대로, 3D 라벨 점들을 먼저 2D 화면 좌표로 투영해 놓고, "마우스와 화면상 거리가 가장 가까운 점"을 고른다(2D 거리 계산). 이 앱의 hotspots.tspick() 함수는 후자다 — 각 라벨 점을 화면에 투영해 24픽셀 반경 안에서 가장 가까운 것을 집는다. 삼각형 충돌 계산이 아예 없다. 라벨(점)만 고르면 되지 표면의 임의 지점을 맞힐 필요가 없으니, 이 앱엔 이게 충분하고 훨씬 싸다.

여기에 두 가지 영리한 보조 장치가 붙는다. 첫째 snapToSurface() — 데이터에 손으로 적어 둔 라벨 좌표를 앱 시작 때 실제 장기 표면(메시 정점)에 착 붙인다. 단, "방향 원뿔(direction cone)"로 걸러서 점이 장기를 뚫고 반대편에 붙는 사고를 막는다. 둘째 가림 처리(occlusion) — 매 프레임 "이 점이 카메라를 향하고 있나(바깥쪽 법선 vs 카메라)"를 재서, 뒤통수로 돌아간 점은 서서히 투명해진다. 이 역시 레이캐스팅 없이 방향 계산만으로 해낸다.

결정 ② — "가만히 있으면 안 그린다" (render-on-demand)

보통 three.js 튜토리얼은 requestAnimationFrame으로 초당 60번 무조건 다시 그린다. 화면이 안 변해도 계속. 이건 노트북 배터리와 폰 발열의 주범이다. 이 앱의 animate() 루프는 다르다 — dirty(더러움) 플래그가 켜졌거나, 애니메이션 타이머가 도는 동안에만 실제로 renderer.render()를 호출한다. 사용자가 손을 떼고 장면이 멈추면 그리기 비용이 사실상 0이 된다.

용어
render-on-demand · dirty 플래그 · IntersectionObserver
render-on-demand(요청 시에만 렌더)는 "바뀐 게 있을 때만 다시 그린다"는 전략이다. 무언가 바뀌면(회전·줌·페이드) dirty=true로 표시하고, 루프는 그때만 그린 뒤 다시 잠든다. 여기에 IntersectionObserver(요소가 화면에 보이는지 감시)를 더해 캔버스가 스크롤 밖으로 나가면 루프를 멈추고, visibilitychange탭이 숨겨지면 정지, ResizeObserver로 크기 변화에만 반응한다. "필요할 때만 일한다"는 원칙을 네 겹으로 구현한 셈. 3D가 아니어도 무거운 캔버스·애니메이션 전반에 그대로 쓰이는 패턴이다.

결정 ③ — 투명하게 사라질 때 속이 비치지 않게 (깊이 프리패스)

장기가 스르륵 나타나거나 사라질 때 반투명해진다. 그런데 반투명 3D 물체는 고질적 문제가 있다 — 앞면 너머로 뒷면·내부가 비쳐 보여 지저분해진다. 이 앱은 setDepthPrepass로 이를 푼다: 페이드 중에는 "색은 안 칠하고 깊이만 기록하는" 투명 프록시 메시를 각 메시에 붙여, 반투명 상태에서도 가장 가까운 한 면만 남기고 뒤는 가린다. 코드에 그 이유가 친절히 주석으로 달려 있다.

결정 ④ — HDRI 없이 그럴듯한 조명 (절차적 PMREM + 구운 그림자)

3D를 사실적으로 보이게 하려면 보통 무거운 HDRI 환경맵(수 MB짜리 360도 사진)을 내려받아 반사에 쓴다. 이 앱은 그걸 안 받는다. 대신 16×32짜리 초소형 "따뜻함→차가움" 그라디언트를 코드로 만들어 PMREM(반사용 환경맵)으로 한 번 구워 쓴다. 그림자도 진짜 그림자 연산(무겁다) 대신, 캔버스에 방사형 그라디언트로 "가짜 접지 그림자"를 그려 바닥에 깔았다. 조명 리그는 은은한 키·필·림 라이트 + 장기별 색이 입혀지는 organ-glow 포인트 라이트로 구성된다.

비유

사진관의 조명 세팅. 진짜 야외 햇빛(HDRI)을 스튜디오로 끌어오는 대신, 노련한 사진사는 작은 반사판과 은은한 조명 몇 개로 "야외 같은" 빛을 만든다. 바닥 그림자도 진짜 그림자를 기다리는 대신 검은 천을 슬쩍 깔아 그림자처럼 보이게 한다. 결과 사진은 손님이 차이를 못 느끼지만, 준비는 훨씬 가볍고 빠르다. 이 앱의 조명·그림자가 정확히 이 방식이다 — "진짜"가 아니라 "충분히 그럴듯한 가짜"를 싸게.

축 1 — 장기 하나가 화면에 뜨는 전체 흐름

사용자가 "심장" 클릭 │ ▼ ① loaders.ts: public/models/heart.glb 요청 (LRU 캐시 확인 → miss면 fetch) │ · meshopt 압축 해제(MeshoptDecoder) │ · 바운딩박스 재서 3.8 크기 큐브에 맞춰 정규화 + 중심 이동 │ · 재질 손질(거칠기 0.42~0.62, 금속성 0, 이방성 up to 8) ▼ ② viewer.ts: Scene에 배치 → GSAP로 등장 연출(스케일 back.out + 카메라 dolly) │ · organ-glow 포인트 라이트 색을 장기 accent로 설정 ▼ ③ hotspots.ts: anatomy-data.ts의 핫스팟 좌표를 snapToSurface로 표면에 붙임 │ · 화면에 스프라이트 "점"으로 표시(sizeAttenuation=false → 항상 같은 픽셀 크기) ▼ ④ 상호작용: hover/클릭 → pick()(화면좌표 24px) → 선택된 점의 콜아웃(DOM)을 │ translate3d로 직접 이동(React 리렌더 없음) ▼ ⑤ 렌더: dirty일 때만 render() → 멈추면 GPU ≈ 0 └──▶ 툴바: 회전·줌·격리(Isolate)·단면(Cross-section, 클리핑 평면)· 레이어(=와이어프레임)·비교·리셋
용어
glTF / GLB · meshopt · 정규화(fit-normalize)
glTF는 "3D계의 JPEG"로 불리는 표준 3D 모델 포맷이고, GLB는 그걸 하나의 바이너리 파일로 묶은 것이다. 이 앱의 9개 장기는 meshopt로 압축된 GLB(KHR_mesh_quantization 사용)라서 용량이 작다(1.98~5.79MB). 로더는 각 모델을 불러온 뒤 크기를 재서 3.8 단위 큐브에 맞추고 중심을 원점으로 옮긴다(정규화). 덕분에 모델마다 크기·위치가 제각각이어도 핫스팟 좌표계가 일정해져, 라벨 데이터를 모델 독립적으로 쓸 수 있다. 로더엔 LRU 캐시(최근 3개 유지)·중복 요청 합치기·마우스 올리면 미리 받기(prefetch)·철저한 GPU 자원 해제(dispose)까지 붙어 있다.
아키텍처에서 배우는 것
"경계를 한 곳에 몰아넣으면" 양쪽이 서로를 방해하지 않는다

이 앱의 진짜 교훈은 특정 트릭이 아니라 경계 설계다. React와 three.js가 딱 한 파일(OrganViewer.tsx)에서만 만나고, 콜아웃 위치 같은 잦은 변화는 React 상태를 거치지 않고 DOM을 직접 움직인다. "자주 바뀌고 성능 민감한 것"과 "가끔 바뀌고 선언적으로 두면 편한 것"을 의도적으로 다른 세계에 배치한 것이다. 이 분리 감각은 3D뿐 아니라 지도·차트·에디터 같은 모든 고성능 인터랙티브 UI에 그대로 적용된다.

5디렉토리 구조 해부

파일 이름만 훑어도 "어디가 앱이고 어디가 스타터 잔재인지" 보인다
anatomy/ (레포 루트 · package.json 이름은 "site-creator-vinext-starter") ├─ app/ ★ 실제 앱은 전부 여기 (Next.js App Router) │ ├─ page.tsx (5) <AnatomyApp/> 하나만 렌더 │ ├─ layout.tsx (84) 메타데이터·OG태그·폰트·파비콘 │ ├─ globals.css (562) 따뜻한 "아틀리에" 디자인 시스템 전체 │ ├─ chatgpt-auth.ts (86) "ChatGPT로 로그인"(SIWC) 헬퍼 — 앱은 안 씀(스타터 잔재) │ ├─ components/ │ │ ├─ AnatomyApp.tsx (334) 최상위 UI: 라이브러리·정보패널·카드·모달 │ │ └─ OrganViewer.tsx (188) ★ React ↔ three 다리, 툴 버튼, 콜아웃, 로더 │ └─ lib/ │ ├─ anatomy-data.ts (311) ★ 9개 장기 + 핫스팟 (단일 진실원) │ └─ three/ │ ├─ viewer.ts (642) ★ 장면·카메라·조명·렌더 루프·툴 — 엔진 본체 │ ├─ hotspots.ts (371) ★ 스프라이트 점·화면좌표 픽킹·snapToSurface │ ├─ loaders.ts (213) GLTF+meshopt 로더·정규화·LRU 캐시 │ ├─ dispose.ts (15) GPU 자원 깊은 해제(메모리 누수 방지) │ └─ tsl-materials.ts (8) WebGPU/TSL 림라이트 스텁 (미래용, 현재 미사용) │ ├─ worker/index.ts (47) Cloudflare Worker 진입점(vinext + 이미지 최적화) ├─ build/sites-vite-plugin.ts 빌드 시 .openai 메타데이터를 dist에 패키징 ├─ db/ (schema.ts 비어있음) Drizzle D1 헬퍼 — 스키마 의도적으로 빔(안 씀) ├─ examples/d1/ 선택적 D1 노트 CRUD 예제 (스타터 부속) ├─ tests/rendered-html.test.mjs node:test — SSR 로딩 스켈레톤 검증 ├─ public/ (~34MB) │ ├─ models/*.glb (9개, ~29MB) meshopt 압축 장기 모델 │ ├─ anatomy/<organ>/*.webp 장기당 5뷰 일러스트(thumb/organ/micro/compare/location) │ ├─ draco/ · basis/ ✗ 안 쓰는 디코더(스타터 잔재, 죽은 무게) │ └─ og.jpg · favicon.svg ├─ package.json · vite.config.ts · next.config.ts · vercel.json ├─ .openai/hosting.json OpenAI Sites 바인딩(d1:null, r2:null) └─ README.md ✗ 스타터 보일러플레이트(해부학 얘기 없음)

이 구조가 알려주는 힌트 셋. 첫째, 앱의 알맹이는 app/lib/three/ 다섯 파일에 몰려 있다 — viewer.ts(642) + hotspots.ts(371) + loaders.ts(213)가 3D의 전부다. "무엇을 먼저 읽어야 하나"가 파일 크기로 드러난다. 둘째, anatomy-data.ts단일 진실원이다 — 장기 정보와 라벨 좌표가 여기 한 곳에 모여 있어, 새 장기를 추가하려면 원칙적으로 이 파일과 GLB 하나만 손대면 된다. 셋째, 스타터 잔재가 곳곳에 보인다: chatgpt-auth.ts·db/·examples/d1/·public/draco·basis/·무관한 README는 전부 AI 생성기가 깔아 준 기본 부속이고 앱은 안 쓴다.

AI 생성 앱을 읽을 때의 요령
"안 쓰는 부속"을 걸러내는 눈이 필요하다

AI가 스타터에서 앱을 뽑으면 "켰지만 안 쓴 기능"이 잔뜩 남는다. 이 레포의 Draco/Basis 디코더(~1.7MB)는 코드 어디서도 참조되지 않고(로더는 Meshopt만 씀), D1 데이터베이스·ChatGPT 로그인도 배선만 되고 미사용이다. 초보자가 이걸 "다 중요한가 보다" 하고 따라 읽으면 길을 잃는다. import를 실제로 타고 들어가며 "진짜 쓰이는 코드"만 추리는 습관이 AI 생성 코드베이스를 읽는 핵심 기술이 된다. 역설적으로, 이 레포는 그 훈련용으로도 좋은 교보재다.

6학습 포인트 — 기술별 배울 것

"3D 웹"만이 아니라 "성능·접근성·엣지배포·AI코드 읽기"까지 담긴 종합 교재

① React 안에서 three.js를 "명령형"으로 다루는 법 (가장 값진 배움)

react-three-fiber 없이 순수 three.js를 클래스로 쥐고, React와는 OrganViewer.tsx 다리 하나로만 잇는 패턴. "3D 렌더 루프는 React 밖에서, UI만 React 안에서"라는 경계 감각을 실제 코드로 익힐 수 있다. 이건 지도·차트·게임 캔버스 등 React와 명령형 라이브러리를 결혼시키는 모든 상황에 그대로 쓰인다.

② 클릭 판정: 레이캐스팅 대신 화면좌표 픽킹

hotspots.tspick()을 정독하면, "3D 상호작용 = 레이캐스팅"이라는 고정관념이 깨진다. "라벨만 고르면 될 땐 화면에 투영해 2D 거리로"가 훨씬 싸다는 것, 그리고 snapToSurface(방향 원뿔로 표면에 붙이기)·가림 처리(법선 vs 카메라)까지 레이캐스팅 없이 기하로 푸는 발상을 배운다.

③ render-on-demand — 가만히 있을 땐 안 그리기

dirty 플래그 + IntersectionObserver + visibilitychange + ResizeObserver로 "필요할 때만 렌더"를 구현하는 법. 이 하나만 익혀도 당신이 만드는 모든 캔버스/애니메이션의 배터리·발열이 극적으로 준다. three.js를 안 써도 통하는, 웹 성능의 근본 기법이다.

④ 투명 렌더링의 함정과 깊이 프리패스

반투명 물체가 왜 "속이 비쳐" 지저분해지는지, setDepthPrepass가 "색은 안 칠하고 깊이만 기록하는 프록시"로 이를 어떻게 푸는지. 알파 블렌딩·깊이 버퍼의 원리를 실전 코드로 만지는 드문 기회다.

⑤ 가벼운 조명·그림자 (절차적 PMREM · 구운 그림자)

무거운 HDRI를 내려받지 않고 코드로 만든 작은 그라디언트를 PMREM으로 굽는 법, 진짜 그림자 연산 대신 캔버스 그라디언트로 접지 그림자를 흉내내는 법. PBR 재질·환경맵·톤매핑(ACESFilmic)의 실용 감각을 얻는다.

⑥ glTF/GLB 파이프라인 전체

meshopt 압축 해제 → 크기 정규화 → 재질 손질(거칠기·이방성) → LRU 캐시 → 마우스오버 prefetch → 철저한 dispose(GPU 메모리 해제). "3D 에셋을 실무에서 다루는 처음부터 끝까지"가 loaders.ts·dispose.ts에 압축돼 있다. 특히 dispose는 3D 웹 메모리 누수의 단골 원인이라 꼭 볼 값어치가 있다.

⑦ 캔버스 UI의 접근성

3D 캔버스는 접근성의 사각지대인데, 이 앱은 aria-label·키보드 조작(←→ 회전, +/- 줌, Esc 해제)·스크린리더용 숨은 라벨 목록(hotspot-index)까지 챙긴다. "보이는 것을 못 보는 사용자에게 어떻게 전달하나"의 실전 예.

⑧ 엣지 배포 & "AI가 만든 코드" 읽는 눈

Cloudflare Workers(vinext)로 엣지에서 Next를 돌리는 구조, 그리고 스타터 잔재를 걸러 내며 진짜 코드만 추리는 독해력. 후자는 앞으로 AI 생성 코드베이스가 흔해질 시대에 점점 더 중요한 기술이다.

실습 아이디어(가볍게)
pick() 하나만 읽어도 절반은 이해한 것

이 레포의 정신을 가장 빨리 맛보는 길: app/lib/three/hotspots.ts를 열어 pick()snapToSurface() 두 함수만 정독하라. "3D 클릭을 2D 거리로 푼다"가 실제 코드로 어떻게 생겼는지 보이는 순간, 이 앱의 설계 철학 절반을 잡은 것이다. 그다음 viewer.tsanimate()에서 "언제 render()를 부르고 언제 건너뛰는지"만 확인하면 나머지 절반이다.

7하드웨어 / 시스템 요구사항

"WebGL 되는 브라우저면 끝" — 대신 3D라 GPU가 있으면 확실히 낫다
항목필요한 것
브라우저WebGL 지원 모던 브라우저(최신 Chrome·Edge·Safari·Firefox). React 19/Next 16 기준이라 구형 브라우저는 미지원
GPU사실상 필요 — powerPreference:"high-performance"로 3D를 그리므로, GPU가 없으면 뻑뻑함. 다만 통합 그래픽으로도 대부분 무난
모바일/저사양자동 저전력 모드: 화면 폭 ≤780px 또는 CPU 코어 <6이면 픽셀비 1.5 상한 + 안티앨리어싱 끔. prefers-reduced-motion도 존중
네트워크장기 GLB 9개 합쳐 ~29MB(1개당 2~6MB). 필요한 장기만 그때그때 받으므로(prefetch) 첫 로딩은 가벼움
이미지 포맷WebP 지원 필요(일러스트 5뷰). 요즘 브라우저는 다 됨
개발 환경Node ≥ 22.13, npm. 빌드는 vinext(Cloudflare) 또는 next build(Vercel)
현실 점검
"직접 배포"에는 숨은 장벽 — 라이선스와 모델 출처

"열어 보는" 데는 브라우저만 있으면 되지만, "포크해서 내 서비스로 배포"하려면 걸림돌이 있다. ① LICENSE가 없다 — 명시적 라이선스가 없는 코드는 기본적으로 저작권자(제작자)만 권리를 가지므로, 함부로 재배포·상업 이용하면 안 된다. ② 3D 장기 모델의 출처가 불명 — GLB 안에 저작자·CC 표기가 전혀 없어, 이 모델들이 어디서 왔고 어떤 조건인지 알 수 없다. 학습·참고용으로 코드를 읽는 건 문제없지만, 에셋을 가져다 쓰는 건 위험하다. "구경·학습은 자유롭게, 재사용은 신중하게"가 맞는 태도다.

실행 경로 (로컬에서 띄우기)

목적명령
의존성 설치npm install
개발 서버(Cloudflare)npm run dev — vinext dev(로컬 Workers)
빌드(Cloudflare)npm run build — vinext build
빌드(Vercel)npm run build:next — 표준 next build
테스트npm test — 빌드 + SSR 스켈레톤 검증
둘러보기(설치 없이)배포된 데모 페이지를 브라우저로 열기(호스트 anatomy-atelier.openai.site — 접속 가능 여부는 그때그때 확인 필요)

8직접 해볼 수 있는 실습 과제

읽기만 해도 좋고, 손대면 더 좋다 — 난이도별 5개
난이도 ★ · 입문

로컬에서 띄우고 "성능 트릭" 눈으로 확인하기 30분

git clonenpm install && npm run dev로 앱을 띄운다. 장기를 돌려 보고 점을 클릭해 본 뒤, 브라우저 개발자도구의 성능/렌더링 탭에서 FPS 미터를 켜라. 그리고 마우스를 떼고 가만히 두면 GPU 사용률이 뚝 떨어지는 것을 관찰하라(render-on-demand). 탭을 숨겼다 다시 열어도 멈췄다 재개되는지 확인. "안 그릴 때 안 그린다"를 눈으로 보는 게 목표다.

난이도 ★★ · 코드 읽기

화면좌표 픽킹 정독 + 레이캐스팅과 비교 2시간

app/lib/three/hotspots.tspick()·snapToSurface()를 줄 단위로 읽어라. 그리고 three.js 공식 예제의 Raycaster 방식과 비교해, "이 앱은 왜 삼각형 충돌 계산이 없는가", "24px 반경은 무슨 의미인가", "방향 원뿔([0.94, 0.82, 0.6, -1.1])이 없으면 무슨 사고가 나는가"를 스스로 설명해 보라. "라벨만 고르면 될 땐 2D로 충분하다"를 이해하면 성공.

난이도 ★★★ · 확장

10번째 장기(또는 핫스팟) 추가하기 반나절

app/lib/anatomy-data.tsOrgan 타입을 그대로 따라 새 장기 레코드를 하나 추가하고(이름·기능·질환·핫스팟 좌표), 대응하는 public/models/<id>.glb를 넣어라(무료 CC0 모델을 구하거나, 기존 장기 하나를 복제해 실험). 단일 진실원 설계 덕에 이 두 곳만 손대면 라이브러리에 뜨는지 확인하라. 안 뜬다면 "정규화·핫스팟 스냅"이 어디서 막혔는지 추적하는 것 자체가 좋은 디버깅 훈련이다.

난이도 ★★★★ · 성능 실험

render-on-demand를 "꺼서" 대가를 측정하기 1일

viewer.tsanimate()에서 dirty 플래그 검사를 무력화해(매 프레임 무조건 render()) 원래대로 되돌려 놓고, 두 경우의 GPU 사용률·노트북 발열·배터리 소모를 나란히 재라. IntersectionObserver 정지도 꺼서 스크롤 밖에서도 도는지 확인. "이 최적화가 실제로 몇 %를 아끼는가"를 숫자로 말할 수 있게 되는 게 목표다.

난이도 ★★★★★ · 도전

이 앱을 react-three-fiber로 다시 써 보기(또는 픽킹 패턴 이식) 주말 프로젝트

같은 장면을 react-three-fiber(선언적)로 재구현해, "명령형 vs 선언적"의 실제 차이를 몸으로 겪어 보라 — 어디서 편해지고 어디서 리렌더 성능이 새는지. 여력이 안 되면, 화면좌표 픽킹 + render-on-demand 패턴만 뽑아 완전히 다른 3D 앱(예: 지도 위 마커, 별자리 뷰어)에 이식하라. 이 두 패턴이 특정 앱을 넘어 재사용된다는 걸 손으로 확인하면, 이 레포의 진짜 교훈을 가진 것이다.

9관련 기술 심화 로드맵 (주차별)

"3D 웹 + 성능 + 엣지 배포"를 함께 쌓는 6주 코스
주차주제무엇을 · 어떻게
1주three.js 기초Scene·Camera·Mesh·Light·렌더 루프의 골격. three.js 공식 매뉴얼 + 이 레포 viewer.tsbuildEnvironment·animate를 대조하며 "장면 하나가 어떻게 서는가" 파악.
2주카메라 · 컨트롤 · 조명PerspectiveCamera·OrbitControls·조명 3점(키/필/림)·톤매핑(ACESFilmic). viewer.ts의 조명 리그와 organ-glow 포인트 라이트를 뜯어 "그럴듯한 빛"의 최소 구성 익히기.
3주glTF · PBR 재질 · 압축glTF/GLB 구조, PBR(거칠기·금속성), meshopt/Draco 압축, 환경맵(PMREM). loaders.ts 정독 + 무료 CC0 모델을 직접 불러와 정규화·재질손질 재현.
4주상호작용: 레이캐스팅 vs 화면좌표표준 Raycaster 먼저 익힌 뒤, hotspots.ts의 화면좌표 픽킹·snapToSurface·가림 처리와 비교. "언제 무엇을 쓰나"의 판단 기준 세우기.
5주성능 · 자원 관리render-on-demand·IntersectionObserver·depth-prepass·dispose(메모리 해제)·저전력 적응. viewer.ts·dispose.ts를 자기 프로젝트에 이식하는 연습.
6주React 결합 · 엣지 배포명령형 3D를 React와 잇는 브리지 패턴(OrganViewer.tsx), 그리고 Cloudflare Workers(vinext)·Vercel 듀얼 배포. 마지막으로 "AI 생성 스타터에서 진짜 코드만 추리기" 복기.

10핵심 키워드 사전

이 레포를 읽다 마주치는 용어를 한자리에
용어
three.js브라우저 3D 라이브러리. WebGL을 감싸 장면·카메라·조명·모델을 다룸. 이 앱은 0.185 버전
WebGLRendererthree.js가 실제로 화면에 3D를 그리는 렌더러. 이 앱은 고성능 모드·ACESFilmic 톤매핑 사용
OrbitControls마우스로 카메라를 궤도 회전·줌하는 컨트롤. 감쇠(damping)·자동회전·거리 제한 설정됨
레이캐스팅마우스에서 3D로 광선을 쏴 물체와의 교차를 찾는 표준 클릭 판정. 정확하지만 비쌈. 이 앱은 안 씀
화면좌표 픽킹3D 점을 2D 화면에 투영해 "마우스와 가장 가까운 점"을 고르는 방식. pick()이 이걸 함(24px 반경)
스프라이트(Sprite)항상 카메라를 바라보는 2D 평면. 라벨 "점"으로 씀. sizeAttenuation=false라 화면 픽셀 크기 고정
snapToSurface손으로 적은 핫스팟 좌표를 실제 메시 표면에 붙이는 1회 처리. 방향 원뿔로 반대편 관통 방지
render-on-demand"바뀐 게 있을 때만 다시 그리기". dirty 플래그로 제어. 멈추면 GPU ≈ 0
depth-prepass반투명 페이드 때 "색 없이 깊이만 기록"하는 프록시 메시로, 속이 비쳐 보이는 문제 해결
PMREM반사용 환경맵을 미리 굽는 방식. 이 앱은 HDRI 대신 코드로 만든 작은 그라디언트를 구움
glTF / GLB표준 3D 모델 포맷("3D의 JPEG"). GLB는 단일 바이너리 묶음. 9개 장기가 이 형식
meshopt메시를 작게 압축하는 방식(+ KHR_mesh_quantization). 로더가 MeshoptDecoder로 해제
정규화(fit-normalize)모델마다 다른 크기·위치를 3.8 큐브·원점으로 통일. 핫스팟 좌표계를 모델 독립적으로 만듦
disposeGPU에 올린 지오메트리·텍스처를 명시적으로 해제. 3D 웹 메모리 누수의 핵심 방지책
RSCReact Server Components. 서버가 미리 그려 보내는 컴포넌트. 3D는 클라이언트에서 동적 로드
vinextVite로 Next.js를 Cloudflare Workers에서 돌리는 런타임. 이 앱의 개발·빌드 기반
Cloudflare Workers사용자 근처 "엣지"에서 코드를 실행하는 서버리스 런타임. 페이지 서빙 담당
Drizzle / D1타입세이프 ORM(Drizzle) + Cloudflare의 SQLite DB(D1). 스타터 부속이나 이 앱은 미사용
TSL / WebGPUthree.js의 차세대 셰이더 언어(TSL)·렌더 백엔드(WebGPU). tsl-materials.ts에 미래용 스텁만 존재
site-creator / Sol이 앱을 생성한 OpenAI의 사이트 생성 스타터·에이전트. package 이름·호스트에 흔적이 남음

11참고 링크

원본과 더 깊이 파고들 자료