thebuggeddev/anatomy · 라이선스 없음(명시 안 됨 = 기본 저작권) · 언어 TypeScript · 3D 엔진 three.js 0.185 · 프레임워크 Next.js 16 + React 19 · 최신 커밋 2026-08-02 · 제작 thebuggeddev · 장기 모델 9개(~29MB) · TrendShift Daily #14)
anatomy(Anatomy Atelier)는 "9개 인체 장기의 3D 모델을 브라우저에서 자유롭게 돌려보고, 표면의 라벨 점을 클릭하면 그 부위의 이름·기능·관련 질환이 뜨는, 설치가 필요 없는 웹 해부학 학습 앱"이다. 한마디로 "손으로 만지며 배우는 3D 인체 도감"이다. 의료 기록을 다루거나 진단을 하는 도구가 아니라, 순수하게 "보고 돌려보고 클릭하며 익히는" 교육용 시각화가 목적이자 결과물이다.
그리고 한 겹 더 있다. 이 앱의 package.json 이름은 site-creator-vinext-starter이고 호스트는 anatomy-atelier.openai.site다. 즉 이건 OpenAI의 코딩 에이전트가 "해부학 학습 사이트 만들어 줘"라는 주문을 받아 생성한 결과물이라는 뜻이다. 그래서 이 레포는 두 가지로 읽힌다 — (1) 잘 만든 3D 해부학 교보재이자, (2) "요즘 AI가 앱을 만들면 이 정도 나온다"의 실물 샘플.
과학관에 가면 유리 진열대 안에 인체 장기 모형이 떠 있고, 손잡이를 돌리면 모형이 빙글 돈다. 표면의 작은 버튼을 누르면 "여기는 좌심실, 온몸으로 피를 내보내는 펌프"라고 설명 카드가 튀어나온다. Anatomy Atelier가 웹에서 하는 일이 정확히 이것이다.
그런데 진짜 재미있는 건 진열대 뒤편의 기계 장치다. 보통은 "손님이 어느 버튼을 눌렀나"를 알아내려고 모형 속으로 광선을 쏴서 맞히는 방식(레이캐스팅)을 쓴다 — 정확하지만 비싸다. 이 앱은 대신 "화면에 찍힌 버튼들 중 손가락과 가장 가까운 걸 고른다"는 훨씬 싼 방법을 쓴다. 손님은 차이를 못 느끼지만, 기계는 훨씬 덜 뜨겁다. 이 "티 안 나게 영리한 지름길"들이 이 레포의 진짜 볼거리다.
왜 이 앱이 "가벼운데 부드럽다"는 인상을 주는지, 숫자로 감을 잡아 보자. 3D 웹앱이 무거워지는 두 지점 — 매 프레임 그리기와 클릭 판정 — 을 이 앱이 어떻게 비껴갔는지가 요약돼 있다.
이 세 줄의 "지름길"이 이 레포를 공부할 값어치로 만든다. 화려한 결과물(예쁜 3D 장기) 뒤에 "어떻게 하면 3D를 싸게 돌릴까"라는 실전 기법이 층층이 깔려 있기 때문이다. 아래에서 하나씩 뜯어본다.
첫째, "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가 생성한 앱인데도 성능·접근성·자원 정리(GPU 메모리 해제)까지 챙긴 구석이 많다. "AI 생성물이라 대충일 것"이라는 편견을 열어 보며 깨는 재미, 그리고 그 안에서 순수 three.js 최적화 기법을 통째로 배우는 실속. 이 둘의 조합이 트렌딩 상위에 오른 이유다.
냉정하게 봐야 할 것들이 있다. ① README가 앱과 무관하다 — 스타터 보일러플레이트("vinext-starter", ChatGPT 로그인, D1 DB) 그대로라서 해부학 얘기가 한 줄도 없다. ② 라이선스가 없다(LICENSE 파일 부재 = 기본적으로 모든 권리 보유, 자유 사용 불가). ③ 3D 장기 모델의 출처·저작권 표기가 전혀 없다 — 어디서 온 모델인지 불명이라, 그대로 가져다 쓰기엔 위험하다. ④ 화면의 일부 기능은 껍데기다: "Layers(레이어)"는 사실 와이어프레임 토글일 뿐 골격계·근육계 같은 진짜 해부 계통 전환이 아니고, Compare·Quiz·Animation·Lessons 상당수가 정적 모달(내용 없는 UI 틀)이다. ⑤ 상단 "검색"은 장기 목록 필터이지 부위 이름 검색이 아니다. 즉 "배우는 교보재"보다 "구경하고 코드 뜯는 샘플"로 접근하는 게 정확하다.
이 앱의 스택은 거의 모든 부품이 최신 버전이라는 점에서 그 자체로 "2026년 프론트엔드 지형도"다. AI 생성기가 최신 스타터를 썼기 때문에, 지금 시점의 "권장 조합"이 그대로 박제돼 있다. 크게 세 층 — 화면(프론트), 빌드, 인프라 — 으로 나눠 본다.
| 영역 | 무엇을 쓰나 | 역할 |
|---|---|---|
| 프레임워크 | Next.js 16.2 (App Router + RSC) | 페이지·서버 렌더링 뼈대. 첫 화면은 서버가 그려 보내고 3D는 클라이언트에서 |
| UI 런타임 | React 19.2 | 화면 상태·컴포넌트. 3D는 React 밖(명령형)에서 돌리고 얇게만 연결 |
| 3D 엔진 | three.js 0.185 | WebGL 위의 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 | 본문 산세리프 + 제목 세리프의 "공방" 무드 |
import("../lib/three/viewer")). 이렇게 하면 무거운 three.js가 첫 번들에 안 실려 초기 로딩이 가벼워진다. "서버가 할 일과 브라우저가 할 일을 가른다"의 좋은 예.| 영역 | 무엇을 쓰나 | 역할 |
|---|---|---|
| 번들러 | Vite 8 + vinext 0.0.50 | vinext = 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.json → next build) | 두 곳 모두에 배포 가능하게 설정됨 |
| 런타임 요구 | Node ≥ 22.13 | 개발·빌드 환경 기준 |
상태 관리에 주목할 점 하나 — Redux도 Zustand도 없다. 3D의 모든 상태는 AnatomyApp.tsx 안의 평범한 React useState/useRef로 관리된다. 복잡한 상태 라이브러리를 끌어오는 대신, "React는 UI만, 3D 내부 상태는 three.js 쪽 객체가 직접"이라는 역할 분리로 단순함을 유지한 것이다.
리액트로 three.js를 다룰 때 요즘은 react-three-fiber(r3f)로 3D를 JSX처럼 선언적으로 쓰는 게 유행이다. 그런데 이 앱은 일부러 r3f 없이 three.js를 명령형 클래스로 직접 다룬다. 덕분에 "React 리렌더가 3D 성능을 갉아먹는" 흔한 함정을 원천 차단하고, 렌더 루프·자원 정리를 손끝까지 제어한다. r3f의 편리함 대신 성능·제어권을 택한 셈 — 어느 쪽이 옳다기보다, "프레임워크에 3D를 맡기지 않고 직접 쥐는 법"을 통째로 보여 주는 귀한 표본이다.
이 앱의 뼈대는 "두꺼운 명령형 3D 엔진을, 얇은 React 다리로 연결"하는 구조다. React는 버튼·패널 같은 UI만 그리고, 실제 3D는 AnatomyViewer라는 클래스가 React 바깥에서 독립적으로 돌린다. 둘 사이는 OrganViewer.tsx라는 얇은 다리(bridge) 하나로만 이어진다.
이 그림의 핵심은 화살표가 한 곳에만 있다는 것이다 — React와 three.js가 OrganViewer.tsx에서만 만난다. 나머지는 서로를 모른다. 그래서 3D 내부가 아무리 바빠도 React는 리렌더되지 않고, 반대로 React UI가 바뀌어도 3D 루프는 방해받지 않는다. 이제 이 엔진을 빠르게 만든 네 개의 결정을 하나씩 본다.
가장 독특한 선택이자 이 앱의 상징이다. 3D에서 "사용자가 어느 부위를 클릭했나"를 알아내는 표준 방법은 레이캐스팅 — 마우스 위치에서 3D 공간으로 광선을 쏘고, 그 광선이 장기 표면의 어느 삼각형과 부딪히는지 계산하는 것이다. 정확하지만, 수만 개 삼각형과의 충돌을 매번 따져야 해서 비싸다.
hotspots.ts 속 pick() 함수는 후자다 — 각 라벨 점을 화면에 투영해 24픽셀 반경 안에서 가장 가까운 것을 집는다. 삼각형 충돌 계산이 아예 없다. 라벨(점)만 고르면 되지 표면의 임의 지점을 맞힐 필요가 없으니, 이 앱엔 이게 충분하고 훨씬 싸다.여기에 두 가지 영리한 보조 장치가 붙는다. 첫째 snapToSurface() — 데이터에 손으로 적어 둔 라벨 좌표를 앱 시작 때 실제 장기 표면(메시 정점)에 착 붙인다. 단, "방향 원뿔(direction cone)"로 걸러서 점이 장기를 뚫고 반대편에 붙는 사고를 막는다. 둘째 가림 처리(occlusion) — 매 프레임 "이 점이 카메라를 향하고 있나(바깥쪽 법선 vs 카메라)"를 재서, 뒤통수로 돌아간 점은 서서히 투명해진다. 이 역시 레이캐스팅 없이 방향 계산만으로 해낸다.
보통 three.js 튜토리얼은 requestAnimationFrame으로 초당 60번 무조건 다시 그린다. 화면이 안 변해도 계속. 이건 노트북 배터리와 폰 발열의 주범이다. 이 앱의 animate() 루프는 다르다 — dirty(더러움) 플래그가 켜졌거나, 애니메이션 타이머가 도는 동안에만 실제로 renderer.render()를 호출한다. 사용자가 손을 떼고 장면이 멈추면 그리기 비용이 사실상 0이 된다.
dirty=true로 표시하고, 루프는 그때만 그린 뒤 다시 잠든다. 여기에 IntersectionObserver(요소가 화면에 보이는지 감시)를 더해 캔버스가 스크롤 밖으로 나가면 루프를 멈추고, visibilitychange로 탭이 숨겨지면 정지, ResizeObserver로 크기 변화에만 반응한다. "필요할 때만 일한다"는 원칙을 네 겹으로 구현한 셈. 3D가 아니어도 무거운 캔버스·애니메이션 전반에 그대로 쓰이는 패턴이다.장기가 스르륵 나타나거나 사라질 때 반투명해진다. 그런데 반투명 3D 물체는 고질적 문제가 있다 — 앞면 너머로 뒷면·내부가 비쳐 보여 지저분해진다. 이 앱은 setDepthPrepass로 이를 푼다: 페이드 중에는 "색은 안 칠하고 깊이만 기록하는" 투명 프록시 메시를 각 메시에 붙여, 반투명 상태에서도 가장 가까운 한 면만 남기고 뒤는 가린다. 코드에 그 이유가 친절히 주석으로 달려 있다.
3D를 사실적으로 보이게 하려면 보통 무거운 HDRI 환경맵(수 MB짜리 360도 사진)을 내려받아 반사에 쓴다. 이 앱은 그걸 안 받는다. 대신 16×32짜리 초소형 "따뜻함→차가움" 그라디언트를 코드로 만들어 PMREM(반사용 환경맵)으로 한 번 구워 쓴다. 그림자도 진짜 그림자 연산(무겁다) 대신, 캔버스에 방사형 그라디언트로 "가짜 접지 그림자"를 그려 바닥에 깔았다. 조명 리그는 은은한 키·필·림 라이트 + 장기별 색이 입혀지는 organ-glow 포인트 라이트로 구성된다.
사진관의 조명 세팅. 진짜 야외 햇빛(HDRI)을 스튜디오로 끌어오는 대신, 노련한 사진사는 작은 반사판과 은은한 조명 몇 개로 "야외 같은" 빛을 만든다. 바닥 그림자도 진짜 그림자를 기다리는 대신 검은 천을 슬쩍 깔아 그림자처럼 보이게 한다. 결과 사진은 손님이 차이를 못 느끼지만, 준비는 훨씬 가볍고 빠르다. 이 앱의 조명·그림자가 정확히 이 방식이다 — "진짜"가 아니라 "충분히 그럴듯한 가짜"를 싸게.
KHR_mesh_quantization 사용)라서 용량이 작다(1.98~5.79MB). 로더는 각 모델을 불러온 뒤 크기를 재서 3.8 단위 큐브에 맞추고 중심을 원점으로 옮긴다(정규화). 덕분에 모델마다 크기·위치가 제각각이어도 핫스팟 좌표계가 일정해져, 라벨 데이터를 모델 독립적으로 쓸 수 있다. 로더엔 LRU 캐시(최근 3개 유지)·중복 요청 합치기·마우스 올리면 미리 받기(prefetch)·철저한 GPU 자원 해제(dispose)까지 붙어 있다.이 앱의 진짜 교훈은 특정 트릭이 아니라 경계 설계다. React와 three.js가 딱 한 파일(OrganViewer.tsx)에서만 만나고, 콜아웃 위치 같은 잦은 변화는 React 상태를 거치지 않고 DOM을 직접 움직인다. "자주 바뀌고 성능 민감한 것"과 "가끔 바뀌고 선언적으로 두면 편한 것"을 의도적으로 다른 세계에 배치한 것이다. 이 분리 감각은 3D뿐 아니라 지도·차트·에디터 같은 모든 고성능 인터랙티브 UI에 그대로 적용된다.
이 구조가 알려주는 힌트 셋. 첫째, 앱의 알맹이는 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가 스타터에서 앱을 뽑으면 "켰지만 안 쓴 기능"이 잔뜩 남는다. 이 레포의 Draco/Basis 디코더(~1.7MB)는 코드 어디서도 참조되지 않고(로더는 Meshopt만 씀), D1 데이터베이스·ChatGPT 로그인도 배선만 되고 미사용이다. 초보자가 이걸 "다 중요한가 보다" 하고 따라 읽으면 길을 잃는다. import를 실제로 타고 들어가며 "진짜 쓰이는 코드"만 추리는 습관이 AI 생성 코드베이스를 읽는 핵심 기술이 된다. 역설적으로, 이 레포는 그 훈련용으로도 좋은 교보재다.
react-three-fiber 없이 순수 three.js를 클래스로 쥐고, React와는 OrganViewer.tsx 다리 하나로만 잇는 패턴. "3D 렌더 루프는 React 밖에서, UI만 React 안에서"라는 경계 감각을 실제 코드로 익힐 수 있다. 이건 지도·차트·게임 캔버스 등 React와 명령형 라이브러리를 결혼시키는 모든 상황에 그대로 쓰인다.
hotspots.ts의 pick()을 정독하면, "3D 상호작용 = 레이캐스팅"이라는 고정관념이 깨진다. "라벨만 고르면 될 땐 화면에 투영해 2D 거리로"가 훨씬 싸다는 것, 그리고 snapToSurface(방향 원뿔로 표면에 붙이기)·가림 처리(법선 vs 카메라)까지 레이캐스팅 없이 기하로 푸는 발상을 배운다.
dirty 플래그 + IntersectionObserver + visibilitychange + ResizeObserver로 "필요할 때만 렌더"를 구현하는 법. 이 하나만 익혀도 당신이 만드는 모든 캔버스/애니메이션의 배터리·발열이 극적으로 준다. three.js를 안 써도 통하는, 웹 성능의 근본 기법이다.
반투명 물체가 왜 "속이 비쳐" 지저분해지는지, setDepthPrepass가 "색은 안 칠하고 깊이만 기록하는 프록시"로 이를 어떻게 푸는지. 알파 블렌딩·깊이 버퍼의 원리를 실전 코드로 만지는 드문 기회다.
무거운 HDRI를 내려받지 않고 코드로 만든 작은 그라디언트를 PMREM으로 굽는 법, 진짜 그림자 연산 대신 캔버스 그라디언트로 접지 그림자를 흉내내는 법. PBR 재질·환경맵·톤매핑(ACESFilmic)의 실용 감각을 얻는다.
meshopt 압축 해제 → 크기 정규화 → 재질 손질(거칠기·이방성) → LRU 캐시 → 마우스오버 prefetch → 철저한 dispose(GPU 메모리 해제). "3D 에셋을 실무에서 다루는 처음부터 끝까지"가 loaders.ts·dispose.ts에 압축돼 있다. 특히 dispose는 3D 웹 메모리 누수의 단골 원인이라 꼭 볼 값어치가 있다.
3D 캔버스는 접근성의 사각지대인데, 이 앱은 aria-label·키보드 조작(←→ 회전, +/- 줌, Esc 해제)·스크린리더용 숨은 라벨 목록(hotspot-index)까지 챙긴다. "보이는 것을 못 보는 사용자에게 어떻게 전달하나"의 실전 예.
Cloudflare Workers(vinext)로 엣지에서 Next를 돌리는 구조, 그리고 스타터 잔재를 걸러 내며 진짜 코드만 추리는 독해력. 후자는 앞으로 AI 생성 코드베이스가 흔해질 시대에 점점 더 중요한 기술이다.
pick() 하나만 읽어도 절반은 이해한 것이 레포의 정신을 가장 빨리 맛보는 길: app/lib/three/hotspots.ts를 열어 pick()과 snapToSurface() 두 함수만 정독하라. "3D 클릭을 2D 거리로 푼다"가 실제 코드로 어떻게 생겼는지 보이는 순간, 이 앱의 설계 철학 절반을 잡은 것이다. 그다음 viewer.ts의 animate()에서 "언제 render()를 부르고 언제 건너뛰는지"만 확인하면 나머지 절반이다.
| 항목 | 필요한 것 |
|---|---|
| 브라우저 | 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 — 접속 가능 여부는 그때그때 확인 필요) |
git clone 후 npm install && npm run dev로 앱을 띄운다. 장기를 돌려 보고 점을 클릭해 본 뒤, 브라우저 개발자도구의 성능/렌더링 탭에서 FPS 미터를 켜라. 그리고 마우스를 떼고 가만히 두면 GPU 사용률이 뚝 떨어지는 것을 관찰하라(render-on-demand). 탭을 숨겼다 다시 열어도 멈췄다 재개되는지 확인. "안 그릴 때 안 그린다"를 눈으로 보는 게 목표다.
app/lib/three/hotspots.ts의 pick()·snapToSurface()를 줄 단위로 읽어라. 그리고 three.js 공식 예제의 Raycaster 방식과 비교해, "이 앱은 왜 삼각형 충돌 계산이 없는가", "24px 반경은 무슨 의미인가", "방향 원뿔([0.94, 0.82, 0.6, -1.1])이 없으면 무슨 사고가 나는가"를 스스로 설명해 보라. "라벨만 고르면 될 땐 2D로 충분하다"를 이해하면 성공.
app/lib/anatomy-data.ts의 Organ 타입을 그대로 따라 새 장기 레코드를 하나 추가하고(이름·기능·질환·핫스팟 좌표), 대응하는 public/models/<id>.glb를 넣어라(무료 CC0 모델을 구하거나, 기존 장기 하나를 복제해 실험). 단일 진실원 설계 덕에 이 두 곳만 손대면 라이브러리에 뜨는지 확인하라. 안 뜬다면 "정규화·핫스팟 스냅"이 어디서 막혔는지 추적하는 것 자체가 좋은 디버깅 훈련이다.
viewer.ts의 animate()에서 dirty 플래그 검사를 무력화해(매 프레임 무조건 render()) 원래대로 되돌려 놓고, 두 경우의 GPU 사용률·노트북 발열·배터리 소모를 나란히 재라. IntersectionObserver 정지도 꺼서 스크롤 밖에서도 도는지 확인. "이 최적화가 실제로 몇 %를 아끼는가"를 숫자로 말할 수 있게 되는 게 목표다.
같은 장면을 react-three-fiber(선언적)로 재구현해, "명령형 vs 선언적"의 실제 차이를 몸으로 겪어 보라 — 어디서 편해지고 어디서 리렌더 성능이 새는지. 여력이 안 되면, 화면좌표 픽킹 + render-on-demand 패턴만 뽑아 완전히 다른 3D 앱(예: 지도 위 마커, 별자리 뷰어)에 이식하라. 이 두 패턴이 특정 앱을 넘어 재사용된다는 걸 손으로 확인하면, 이 레포의 진짜 교훈을 가진 것이다.
| 주차 | 주제 | 무엇을 · 어떻게 |
|---|---|---|
| 1주 | three.js 기초 | Scene·Camera·Mesh·Light·렌더 루프의 골격. three.js 공식 매뉴얼 + 이 레포 viewer.ts의 buildEnvironment·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 생성 스타터에서 진짜 코드만 추리기" 복기. |
| 용어 | 뜻 |
|---|---|
| three.js | 브라우저 3D 라이브러리. WebGL을 감싸 장면·카메라·조명·모델을 다룸. 이 앱은 0.185 버전 |
| WebGLRenderer | three.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 큐브·원점으로 통일. 핫스팟 좌표계를 모델 독립적으로 만듦 |
| dispose | GPU에 올린 지오메트리·텍스처를 명시적으로 해제. 3D 웹 메모리 누수의 핵심 방지책 |
| RSC | React Server Components. 서버가 미리 그려 보내는 컴포넌트. 3D는 클라이언트에서 동적 로드 |
| vinext | Vite로 Next.js를 Cloudflare Workers에서 돌리는 런타임. 이 앱의 개발·빌드 기반 |
| Cloudflare Workers | 사용자 근처 "엣지"에서 코드를 실행하는 서버리스 런타임. 페이지 서빙 담당 |
| Drizzle / D1 | 타입세이프 ORM(Drizzle) + Cloudflare의 SQLite DB(D1). 스타터 부속이나 이 앱은 미사용 |
| TSL / WebGPU | three.js의 차세대 셰이더 언어(TSL)·렌더 백엔드(WebGPU). tsl-materials.ts에 미래용 스텁만 존재 |
| site-creator / Sol | 이 앱을 생성한 OpenAI의 사이트 생성 스타터·에이전트. package 이름·호스트에 흔적이 남음 |
app/lib/three/hotspots.ts(화면좌표 픽킹) → viewer.ts(엔진·렌더 루프) → loaders.ts(glTF 파이프라인) → anatomy-data.ts(데이터 단일 진실원) 순서로 읽으면 앱 전체가 잡힌다anatomy-atelier.openai.site(OpenAI Sites). 접속 가능 여부는 시점에 따라 다를 수 있으니 직접 확인react-three-fiber(리액트용 선언적 three.js, 이 앱이 "안 쓴" 대안) · Sketchfab(범용 3D 뷰어) · BioDigital Human(상용 해부학, 규모 대비)