-
3D Room Planer : 웹 기반 3D 가구 배치 시뮬레이터 만들기Projects 2026. 4. 24. 22:12반응형

뭘 만들었나?
6m × 6m × 2.7m 크기의 3D 방에 가구를 배치할 수 있는 웹 애플리케이션.
- 카탈로그에서 가구를 선택해 바닥에 배치
- 드래그로 이동
R키로 90도 회전- 다른 가구와의 충돌 시각화 (빨강 하이라이트)
- 탑뷰 / 원근 카메라 전환
- 배치 내용 자동 저장 & 복원
왜 이걸 만들었나?
이 프로젝트는 의도적으로 짧은 스코프에 4가지 기술 시연을 담았다:
- WebGL 시각화 — Three.js로 실시간 렌더링
- 3D 선형대수 — 광선-평면 교점, AABB 충돌 판정
- 성능 최적화 — 50개 가구도 부드럽게 (< 16ms)
- 도메인 설계 — 인테리어 도구의 아키텍처
작은 프로젝트지만 "진짜 애플리케이션"으로 동작시키는 것이 목표였다.
2. 기술 스택 선택 이유
Vite + TypeScript 빠른 개발 루프, 타입 안전 ↓ React + React Three Fiber 선언형 UI + 3D 렌더링 ↓ Zustand 상태 관리 ↓ Tailwind CSS UI 스타일왜 React Three Fiber인가?
처음엔 바닐라 Three.js를 고려했지만, UI와 3D를 함께 관리해야 했다.
React Three Fiber는:
- 선언형: "가구 5개가 있다" → JSX로 표현 가능
- React 생태계: 기존 상태관리 패턴 그대로 사용 가능
- 성능:
memo,useFrameref 패턴으로 불필요한 리렌더 방지
// 이렇게 쓸 수 있다 <Furniture items={items.beds} type="bed" /> <Furniture items={items.sofas} type="sofa" /> // 각 아이템이 변하면, 해당 타입의 Instanced mesh만 업데이트왜 Zustand인가?
Redux나 Context API도 가능했지만:
- 최소한의 보일러플레이트 —
create()한 줄 - Subscription 친화적 — 3D 루프에서 전체 리렌더 없이 일부만 구독 가능
- 미들웨어 확장성 — localStorage persist, undo/redo 나중에 추가 가능
const useStore = create((set) => ({ items: [], selectedId: null, addItem: (item) => set(state => ({ items: [...state.items, item] })) }))
3. 프론트엔드 개발자가 알면 좋은 기술들
3.1 React Three Fiber: 선언형 3D
기존 Three.js:
const geometry = new THREE.BoxGeometry(1, 1, 1) const material = new THREE.MeshStandardMaterial({ color: 0xff0000 }) const mesh = new THREE.Mesh(geometry, material) scene.add(mesh)React Three Fiber:
<mesh> <boxGeometry args={[1, 1, 1]} /> <meshStandardMaterial color={0xff0000} /> </mesh>장점:
- Props 기반이라 상태 변화를 자연스럽게 표현
- JSX 문법이라 학습곡선이 낮음
- React의 라이프사이클 활용 가능
3.2 InstancedMesh: 배치 렌더링의 핵심
10개 의자를 10번 그리는 게 아니라, 1번에 10개를 그린다.
const bedsRef = useRef<THREE.InstancedMesh>(null) useEffect(() => { beds.forEach((bed, i) => { const matrix = new THREE.Matrix4() matrix.compose(bed.position, bed.quaternion, bed.scale) bedsRef.current?.setMatrixAt(i, matrix) }) bedsRef.current!.instanceMatrix.needsUpdate = true }, [beds]) return ( <instancedMesh ref={bedsRef} args={[geometry, material, 100]}> {/* 100개 인스턴스를 위한 준비만 함 */} </instancedMesh> )성능 차이:
- 10개 개별 메시 = 10개 draw call
- InstancedMesh = 1개 draw call
이게 가능한 이유는 GPU 측에서
gl_InstanceID라는 내장 변수로 각 인스턴스를 구분하기 때문.3.3 Raycast: 마우스 → 3D 공간 변환
사용자가 마우스로 클릭한 위치가 3D 방의 어디인가?
const raycaster = new THREE.Raycaster() const pointer = new THREE.Vector2() const handleMouseMove = (event: MouseEvent) => { pointer.x = (event.clientX / window.innerWidth) * 2 - 1 pointer.y = -(event.clientY / window.innerHeight) * 2 + 1 raycaster.setFromCamera(pointer, camera) // 광선과 바닥 평면의 교점 계산 const floorIntersects = raycaster.intersectObject(floorMesh) if (floorIntersects.length > 0) { const point = floorIntersects[0].point // point.x, point.z = 가구 놓을 위치 } }원리:
- 마우스 좌표를 정규화 좌표(NDC)로 변환 (-1 ~ 1)
raycaster.setFromCamera()로 카메라에서 시작하는 광선 생성- 3D 객체와의 교점 계산
이 기술은 모바일 터치, VR 인터랙션 등 모든 곳에서 쓰인다.
3.4 3D 선형대수: AABB 충돌 판정
두 가구가 겹치는지 판정하려면?
// Axis-Aligned Bounding Box (AABB): 축 정렬 경계상자 type AABB = { min: { x: number, z: number } max: { x: number, z: number } } function aabbsOverlap(a: AABB, b: AABB): boolean { return !( a.max.x < b.min.x || a.min.x > b.max.x || a.max.z < b.min.z || a.min.z > b.max.z ) }90도 회전만 지원하는 이유:
- 90도 회전 = 단순히 width ↔ depth 교환
- 임의 각도 회전 = 8개 코너를 모두 변환해야 함 (복잡함)
이 트레이드오프는 의도적인 스코핑이다.
프로토타입에서 기능을 제한하면 구현은 간단해지지만 여전히 실용성 있게 만들 수 있다.
3.5 frameloop="demand": 배터리 절약
<Canvas frameloop="demand" /* 유휴 상태에서 렌더링 안 함 */> <Scene /> </Canvas>기본값
"always"는 매 프레임 렌더링하므로:- 60fps × 60초 = 3600번 그리기 (아무 변화 없어도)
- 랩톱 팬 소음, 배터리 방전
"demand"로 설정하고 상태 변화 시에만invalidate()호출:const invalidate = useThree(state => state.invalidate) const handlePlaceItem = () => { store.addItem(newItem) invalidate() // 한 번만 그린다 }이 패턴은 mobile-first 이다.
3.6 localStorage: 낮은 수준의 영속화
// 저장 localStorage.setItem('roomLayout', JSON.stringify(items)) // 복원 const saved = localStorage.getItem('roomLayout') if (saved) store.hydrate(JSON.parse(saved))TypeScript에서는 스키마를 정의해서 타입 안전성 확보:
type SavedLayout = { version: 1 items: Item[] timestamp: number } function saveLayout(items: Item[]) { const data: SavedLayout = { version: 1, items, timestamp: Date.now() } localStorage.setItem('roomLayout', JSON.stringify(data)) }주의할 점:
- 5MB 제한 (보통 충분)
- 동기 작업 (큰 데이터는
setTimeout으로 블록 방지) - XSS에 취약하므로 신뢰할 수 있는 데이터만 저장
4. 구현 과정의 도전과제들
문제 1: React 리렌더링 폭발
처음엔 이렇게 짰다:
function Furniture() { const items = useStore(state => state.items) // 전체 구독 return ( <group> {items.map(item => <mesh key={item.id} {...item} />)} </group> ) }문제: 가구 1개 위치 변경 → 전체 배열 업데이트 → 전체 JSX 리렌더 → 50개 메시 all re-create
해결: Zustand의 selector로 필요한 데이터만 구독
const items = useStore(state => state.items) const beds = useStore(state => state.items.filter(i => i.type === 'bed')) // 그리고 InstancedMesh 사용문제 2: 카메라 전환 시 뭔가 이상함
탑뷰 ↔ 원근 전환하면 드래그 좌표가 틀렸다.
원인: Raycaster가 old 카메라 기준으로 광선을 만들고 있음
해결:
const camera = useThree(state => state.camera) const handleMouseMove = (e: MouseEvent) => { raycaster.setFromCamera(pointer, camera) // 매번 현재 카메라 사용 }배운 점: 3D에서는 카메라가 기준이다. 상태 변화 시 모든 기하 계산을 다시 해야 한다.
문제 3: 회전 + 충돌 판정
AABB는 축 정렬이므로, 가구가 45도 회전하면 AABB도 커진다 (8개 코너의 min/max).
90도만 지원하기로 결정해서 해결했지만, 만약 모든 각도를 지원한다면:
function worldAABBFromItem(item: Item): AABB { const corners = [ [-w/2, -d/2], [w/2, -d/2], [w/2, d/2], [-w/2, d/2] ] const rotated = corners.map(c => { // 각도에 따라 회전 행렬 적용 return rotatePoint(c, item.rotation) }) // 모든 코너의 min/max 계산 return { min: { x: Math.min(...rotated.map(c => c[0])), ... }, max: { x: Math.max(...rotated.map(c => c[0])), ... } } }삭제할 시점 판단: 작은 프로젝트에서는 간단한 규칙(90도 회전)을 선택하는 게 맞다. 나중에 필요하면 그때 추가한다.
5. 성능 최적화: 측정부터
최적화 전략
- InstancedMesh — draw call 1/10로 감소
- frameloop="demand" — idle 상태에서 렌더링 안 함
- Geometry/Material 재사용 — 각 타입마다 1개씩만 생성
- Memoization —
React.memo로 정적 컴포넌트 보호 - Shadow 최적화 — 카메라만 큼, shadow map도 tight하게
실제 효과
Before: 50개 가구 = 50 draw call = 8-12ms After: 50개 가구 = 1-5 draw call = 2-3ms → 1초에 그린다고 해도 여유 (60fps = 16ms)측정 도구:
useEffect(() => { const startTime = performance.now() invalidate() console.time('frame') // ... render 후 console.timeEnd('frame') }, [items])세 가지 관점에서 체크:
- CPU: React 렌더, state 계산
- GPU: draw call, vertex count
- Memory: texture, geometry 캐싱
6. 프론트엔드 개발자로서 얻은 인사이트
6.1 선언형 vs 명령형의 경계
React Three Fiber는 선언형 API를 제공하지만, 내부는 명령형이다.
// 우리가 쓰는 코드 (선언형) <Furniture items={items} /> // 내부에서 일어나는 일 (명령형) const matrix = new Matrix4().compose(pos, quat, scale) instancedMesh.setMatrixAt(index, matrix) instancedMesh.instanceMatrix.needsUpdate = true배운 점: 좋은 추상화는 사용자는 선언형으로 쓰게 하고, 내부는 명령형으로 동작하게 한다. React도 마찬가지다.
6.2 상태 설계가 성능을 좌우한다
좋은 상태 구조:
type Store = { items: Item[] // 평평한 배열 selectedId: string | null gridSnap: boolean // ... }나쁜 구조 (nested):
type Store = { beds: { [id: string]: Bed } sofas: { [id: string]: Sofa } // ... (각 타입마다 구분) }평평한 구조는:
- 정규화되어 중복 없음
- 한 번의 업데이트로 일관성 유지
- Selector로 필요한 부분만 구독 가능
6.3 도메인 설계의 중요성
"가구 배치 시뮬레이터"라는 도메인을 이해하면 코드도 명확해진다:
// 도메인 언어 type Item = { id: string type: FurnitureType // 'bed' | 'sofa' | ... position: Vector3 // 월드 좌표 rotation: Euler // Y축 회전만 selected: boolean colliding: boolean // UI에서 빨강 표시 } // vs 일반적인 이름 type Obj = { x, y, z, r, c, s } // 뭔지 모름좋은 타입 이름은 문서가 된다.
6.4 작은 스코프의 가치
2-짧은 기간 안에 끝내려고 의도적으로 기능을 제한했다:
- 90도 회전만 (임의 각도 X)
- GLTF 로딩 X (primitives만)
- Undo/Redo X
- Texture X (flat material)
결과:
- 구현 복잡도 1/3으로 감소
- 여전히 실용적 (원래 목표 달성)
- 나중에 확장 가능한 구조
"빅 스코프 = 엔드리스 버그"라는 배움.
7. 향후 개선 아이디어
현재 MVP 외에 추가 가능한 기능들:
쉬운 것 (1일 이내)
- Undo/Redo (Zustand middleware)
- Keyboard shortcut 정의 파일화
- GLTF 모델 로딩 (three.js loader)
- 다크 모드 (Tailwind)
중간 난도 (3-5일)
- 벽 충돌 감지
- 문/창 지원
- 가구 높이에 따른 그림자 최적화
- Export as 3D model / 이미지
어려운 것 (1주 이상)
- 멀티플레이 (WebSocket)
- AR 프리뷰 (WebXR API)
- AI 추천 배치 (ML 모델)
결론
프로젝트에서 배운 기술 스택:
- React Three Fiber — 3D와 React 통합하기
- InstancedMesh — 성능 최적화의 핵심
- Raycast — 마우스/터치 입력 처리
- 3D 수학 — 실제 필요한 수준만 학습
- 상태 설계 — 아키텍처의 기초
- 의도적 스코핑 — MVP의 가치
이 기술들은 대규모 3D 애플리케이션(게임, 설계 도구, 가상 공간)의 기초다.
작은 프로젝트부터 제대로 배우면, 나중에 복잡한 문제도 풀 수 있다.
물론 AI도 적극적으로 활용했다.
그 덕분에 더 빠르게 끝낼 수 있었고, 개발자로서는 하네스 역할을 하면서 진행을 했다.

참고 자료
- React Three Fiber Docs
- Three.js Manual
- WebGL Fundamentals — 광선 교점, 행렬 설명 필수 읽기
- Zustand — 상태 관리 패턴
- MDN: Raycasting
반응형'Projects' 카테고리의 다른 글
Tiny Flow Board: 무한 캔버스 메모 보드 (0) 2026.03.13 Reality Check: AI로 스타트업 아이디어 시장포화도 분석 웹 (0) 2026.03.04 Free Coders Books 개발기: 개발 무료 도서 (0) 2026.02.18 SketchTo: AI 스케치 변환 SaaS 툴 (0) 2026.02.09 플랫폼에 맞는 썸네일 만들기 (0) 2026.02.01