Claude Code와 Three.js로 실무형 3D Web UI 만들기
Claude Code와 Three.js로 3D 상품 뷰어를 만들며 resize, dispose, 리뷰 프롬프트와 실무 함정을 정리합니다.
Three.js는 WebGL을 직접 다루는 부담을 줄여 주지만, 프로덕션에서 쓸 수 있는 3D UI는 단순히 돌아가는 오브젝트가 아닙니다. 카메라, 조명, canvas 크기, 모바일 GPU 부하, 페이지 이동 후 리소스 해제, 모델 로딩 실패 시 대체 화면까지 관리해야 합니다.
Claude Code는 이런 반복적인 기반 코드를 빠르게 만들 수 있게 해 줍니다. 다만 “멋진 3D로 만들어줘”처럼 요청하면 보기 좋은 데모는 나오지만 resize가 빠지거나, unmount 후에도 renderer가 살아 있거나, 스마트폰에서 과하게 무거운 코드가 생기기 쉽습니다.
이 글은 Vite + React + Three.js로 복사해서 실행할 수 있는 3D 상품 뷰어를 만들고, Claude Code에게 어떤 기준으로 리뷰를 맡길지 정리합니다.
먼저3D의 목적을 정한다
코드를 쓰기 전에 3D가 사용자에게 어떤 정보를 제공해야 하는지 정해야 합니다. 전자상거래 상품 뷰어라면 색상, 재질, 뒷면, 크기감을 확인하는 것이 목적입니다. 데이터 시각화라면 군집, 이상치, 시간 변화가 보여야 합니다. 교육용 장면이라면 사용자가 자유롭게 돌리는 것보다 중요한 부품으로 시선을 유도하는 것이 더 중요할 수 있습니다.
Claude Code에는 스타일이 아니라 조건을 줘야 합니다.
Vite + React + TypeScript + three로 3D 상품 뷰어를 만들어 주세요.
조건:
- canvas는 document.body가 아니라 부모 요소 안에 렌더링
- 부모 요소 크기에 맞춰 resize
- OrbitControls로 회전과 줌 지원
- unmount 시 geometry, material, renderer, controls dispose
- 모바일 GPU를 위해 devicePixelRatio는 최대 2
- src/App.tsx에 그대로 붙여 넣어 실행 가능한 코드
이 조건은 harness, 즉 에이전트가 안전하게 작업하기 위한 발판입니다. 발판이 있어야 흰 화면, 찌그러진 렌더링, 페이지 전환 후 성능 저하, 모바일 발열을 줄일 수 있습니다. API 세부 사항은 Three.js 공식 문서와 WebGLRenderer 문서를 함께 확인하세요.
Vite/React 최소 구성
처음부터 GLB 모델, HDR 환경, 포스트프로세싱을 넣지 마세요. 먼저 단순한 mesh 하나로 카메라, 조명, 조작, resize, dispose가 제대로 동작하는지 확인해야 합니다.
npm create vite@latest three-claude-demo -- --template react-ts
cd three-claude-demo
npm i three
npm run dev
flowchart LR
A["React component"] --> B["mount div"]
B --> C["WebGLRenderer canvas"]
C --> D["Scene"]
D --> E["Camera and lights"]
D --> F["Mesh and material"]
C --> G["OrbitControls"]
G --> H["resize and dispose"]
복사해서 실행하는3D 뷰어
다음 코드를 src/App.tsx에 붙여 넣으면 간단한 상품 형태의 3D 뷰어가 동작합니다. 핵심은 부모 요소의 clientWidth와 clientHeight를 기준으로 렌더러 크기를 맞추고, cleanup에서 Three.js 리소스를 직접 해제하는 것입니다.
import { useEffect, useRef } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import "./App.css";
export default function App() {
const mountRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const mount = mountRef.current;
if (!mount) return;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf6f7fb);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(3.5, 2.2, 4.5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;
mount.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.minDistance = 2.5;
controls.maxDistance = 8;
controls.target.set(0, 0.4, 0);
scene.add(new THREE.HemisphereLight(0xffffff, 0x7c8594, 1.6));
const keyLight = new THREE.DirectionalLight(0xffffff, 2.4);
keyLight.position.set(3, 5, 4);
keyLight.castShadow = true;
scene.add(keyLight);
const productGeometry = new THREE.BoxGeometry(1.8, 1.2, 1.1, 4, 4, 4);
const productMaterial = new THREE.MeshStandardMaterial({
color: 0x2f6f73,
roughness: 0.42,
metalness: 0.08,
});
const product = new THREE.Mesh(productGeometry, productMaterial);
product.castShadow = true;
product.position.y = 0.75;
scene.add(product);
const floorGeometry = new THREE.CircleGeometry(2.2, 64);
const floorMaterial = new THREE.MeshStandardMaterial({
color: 0xd9dee8,
roughness: 0.7,
});
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
const resize = () => {
const width = mount.clientWidth;
const height = mount.clientHeight;
if (width === 0 || height === 0) return;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
};
let frameId = 0;
const clock = new THREE.Clock();
const animate = () => {
const elapsed = clock.getElapsedTime();
product.rotation.y = elapsed * 0.45;
product.rotation.x = Math.sin(elapsed * 0.8) * 0.08;
controls.update();
renderer.render(scene, camera);
frameId = window.requestAnimationFrame(animate);
};
resize();
animate();
window.addEventListener("resize", resize);
return () => {
window.removeEventListener("resize", resize);
window.cancelAnimationFrame(frameId);
controls.dispose();
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
object.geometry.dispose();
const materials = Array.isArray(object.material)
? object.material
: [object.material];
materials.forEach((material) => material.dispose());
}
});
renderer.dispose();
renderer.domElement.remove();
};
}, []);
return (
<main className="viewerShell">
<div className="copy">
<p className="eyebrow">Three.js + Claude Code</p>
<h1>3D product viewer</h1>
<p>
Drag to rotate, scroll to zoom, and resize the window to verify that
the canvas follows its container.
</p>
</div>
<div ref={mountRef} className="viewerStage" />
</main>
);
}
src/App.css도 추가합니다. 부모 요소에 높이가 없으면 renderer가 정상이어도 화면은 비어 보입니다.
body {
margin: 0;
font-family: Inter, system-ui, sans-serif;
background: #eef2f7;
color: #17202a;
}
.viewerShell {
min-height: 100vh;
display: grid;
grid-template-columns: minmax(260px, 0.8fr) minmax(320px, 1.2fr);
gap: 32px;
align-items: center;
padding: 40px;
box-sizing: border-box;
}
.copy {
max-width: 520px;
}
.viewerStage {
height: min(62vh, 560px);
min-height: 360px;
border: 1px solid #ccd5df;
background: #f6f7fb;
}
.viewerStage canvas {
display: block;
}
@media (max-width: 760px) {
.viewerShell {
grid-template-columns: 1fr;
padding: 24px;
}
.viewerStage {
min-height: 300px;
}
}
Claude Code 리뷰 프롬프트
첫 구현이 끝나면 Claude Code에게 다음 관점으로 리뷰를 맡깁니다.
이 Three.js React 구현을 리뷰해 주세요.
확인할 것:
1. canvas가 부모 요소 크기를 따라가는가
2. geometry, material, renderer, controls가 unmount 때 dispose되는가
3. requestAnimationFrame이 멈추는가
4. 고해상도 모바일 화면에서 devicePixelRatio가 과하지 않은가
5. Next.js나 Astro 같은 SSR 환경에서 window/document 접근이 문제되지 않는가
6. 카메라, 조명, 조작 방식이 상품 확인에 적합한가
수정 diff와 수동 테스트 절차도 제안해 주세요.
이 프롬프트는 Claude Code를 코드 생성기에서 리뷰어로 바꿉니다. 특히 dispose 누락은 한 번 화면을 봐서는 알기 어렵기 때문에, 페이지 이동을 반복하면서 DevTools의 Memory와 Performance를 확인해야 합니다.
실무 유스케이스3가지
| 유스케이스 | 3D를 쓰는 이유 | Claude Code에 맡길 작업 |
|---|---|---|
| 3D 상품 뷰어 | 색상, 재질, 깊이, 뒷면을 구매 전에 확인 | OrbitControls, 색상 변경, 조명 프리셋, 모바일 성능 점검 |
| 데이터 시각화 | 2D 그래프에서 놓치는 군집이나 시간 변화를 표현 | 포인트 클라우드, 3D 막대, 카메라 전환, 범례 UI |
| 포트폴리오/교육 장면 | 회전 가능한 모델로 구조와 부품을 설명 | 주석 라벨, 부품 하이라이트, 단계별 카메라 위치 |
상품 뷰어는 “멋있다”보다 “불안을 줄인다”가 중요합니다. 데이터 시각화는 3D가 비교를 방해하면 2D 뷰를 함께 제공해야 합니다. 교육 장면은 자유 조작만 두기보다 설명 순서에 맞춘 카메라 이동과 자동 회전 정지가 필요합니다.
자주 나는 실패와 해결
| 실패 | 원인 | 해결 |
|---|---|---|
| canvas가 하얗다 | 부모 높이 0, 카메라 방향 오류, 조명 없음 | CSS 높이, camera position, controls target 확인 |
| resize 후 화면이 찌그러진다 | camera.aspect와 renderer size를 같이 갱신하지 않음 | resize 함수에서 camera.updateProjectionMatrix()와 renderer.setSize() 호출 |
| 페이지 이동 후 느려진다 | geometry/material/renderer/controls dispose 누락 | unmount에서 scene을 순회하며 dispose |
| 스마트폰이 뜨거워진다 | 높은 pixelRatio, 무거운 그림자, 과한 세그먼트 | pixelRatio 제한, 그림자와 지오메트리 단순화 |
| SSR에서 깨진다 | 서버 렌더링 중 window/document 접근 | useEffect 또는 client-only 컴포넌트에서 초기화 |
흰 화면을 디버깅할 때는 복잡한 모델과 후처리를 모두 끄고, 밝은 배경, 큐브 하나, 조명 하나만 남기는 것이 빠릅니다.
출시 전 확인과 상담
출시 전에는 목표 스마트폰에서 프레임레이트, 발열, 첫 로드 용량, WebGL 실패 시 대체 이미지를 확인합니다. Canvas 전반의 설계는 Claude Code Canvas 개발 가이드를, 움직임 조정은 Claude Code 애니메이션 구현 가이드를 함께 보면 좋습니다.
Claude Code Lab에서는 3D 상품 뷰어, WebGL 데이터 시각화, 교육용 인터랙티브 장면의 설계 리뷰와 팀 교육을 도와드릴 수 있습니다. 상담할 때는 대상 기기, 프레임워크, 모델 형식, 성능 예산, 비즈니스 목표를 함께 준비하는 것이 좋습니다.
실제로 시험한 결과
이 글의 코드를 Vite React TypeScript 프로젝트에 붙여 넣고 데스크톱 폭과 모바일 폭에서 resize를 확인했습니다. .viewerStage에 높이를 명시하면 흔한 흰 화면 문제를 피할 수 있었고, devicePixelRatio를 2로 제한하면 고해상도 화면에서 GPU 부하가 과하게 올라가는 것을 막기 쉬웠습니다. Claude Code에 dispose 경로를 리뷰시키면 페이지 전환 후 리소스 누수를 더 빨리 찾을 수 있었습니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.