클라이언트 요청 - 3D 제품 뷰어 제작 후기
지인의 소개로 가구 쇼핑몰 3D 제품 뷰어 제작을 의뢰받았습니다. "아마존처럼 제품을 360도로 볼 수 있게 해주세요"라는 요청이었죠. 2주 일정에 작업비 협의까지 마치고 시작했는데, 생각보다 험난한 여정이었습니다.
요구사항 정리 #
첫 미팅에서 받은 요구사항:
- 제품을 360도 회전하며 볼 수 있어야 함
- 확대/축소 가능
- 색상 옵션 선택 시 실시간 변경
- 모바일에서도 작동
- 기존 쇼핑몰에 iframe으로 삽입
간단해 보였습니다. 하지만 숨은 복잡성이 있었죠.
첫 번째 난관: 3D 모델 #
클라이언트가 제공한 것은 가구 설계도 PDF였습니다. 3D 모델이 없었습니다.
선택지:
- 3D 모델러에게 외주 (비용 + 시간 추가)
- 직접 모델링 (Blender 초보 실력으로...)
- 사진 기반 360도 뷰어로 대체 제안
클라이언트와 협의 끝에 간단한 가구(의자)는 제가 직접 모델링하고, 복잡한 것(소파, 침대)은 외주를 주기로 했습니다.
Blender로 의자를 만드는 데 이틀이 걸렸습니다. 3D 모델링은 처음이라 유튜브 튜토리얼을 보면서 했습니다.
의자 모델링 삽질 기록:
- 1일차: 기본 형태 만들기 (6시간)
- 2일차: UV 매핑이 뭔지 검색 (2시간)
- 2일차: UV 매핑 적용 (4시간)
- 2일차: Three.js용 GLTF 내보내기 (2시간)
GLTF 최적화 #
내보낸 GLTF 파일이 15MB였습니다. 이미지 없이 메시만 그 정도였습니다. 뭔가 잘못됐다고 느꼈죠.
알고 보니 불필요한 메시가 많았고, 폴리곤 수도 과했습니다.
최적화 전: 15MB, 150,000 폴리곤
최적화 후: 800KB, 8,000 폴리곤
적용한 최적화:
- Decimate modifier: 폴리곤 수 감소
- 불필요한 내부 면 삭제: 안 보이는 면 제거
- Draco 압축: GLTF 내보내기 시 적용
색상 변경 기능 #
제품 색상 옵션(블랙, 화이트, 우드)을 실시간으로 바꿔야 했습니다.
처음에는 각 색상마다 별도 모델을 로드하려 했습니다.
// 초기 아이디어 (비효율적)
async function changeColor(color) {
const model = await loadModel(`chair_${color}.glb`);
scene.remove(currentModel);
scene.add(model);
currentModel = model;
}
하지만 이러면 색상 바꿀 때마다 로딩이 생깁니다. 대신 런타임에 material을 변경하기로 했습니다.
// 개선된 방법
const colorPresets = {
black: { color: 0x1a1a1a, roughness: 0.3 },
white: { color: 0xf5f5f5, roughness: 0.4 },
wood: {
map: woodTexture,
roughness: 0.8,
normalMap: woodNormalMap
}
};
function changeColor(colorName) {
const preset = colorPresets[colorName];
model.traverse(child => {
if (child.isMesh && child.name.includes('seat')) {
Object.assign(child.material, preset);
child.material.needsUpdate = true;
}
});
}
나무 재질은 텍스처가 필요해서 초기 로딩 때 미리 불러왔습니다.
모바일 최적화 #
데스크톱에서는 잘 돌아갔지만, 클라이언트의 아이폰에서는 버벅거렸습니다.
모바일용 최적화:
- 픽셀 비율 제한:
setPixelRatio(Math.min(devicePixelRatio, 2)) - 그림자 비활성화: 모바일에서는 그림자 off
- 환경맵 해상도 낮춤: 1024 → 256
- 안티앨리어싱 off: 모바일에서 antialias false
const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
const renderer = new THREE.WebGLRenderer({
antialias: !isMobile,
powerPreference: isMobile ? 'low-power' : 'high-performance'
});
renderer.shadowMap.enabled = !isMobile;
iframe 통신 #
쇼핑몰 페이지에 iframe으로 삽입되므로, 부모 페이지와 통신이 필요했습니다.
부모 페이지에서 색상 선택 버튼을 누르면, 뷰어의 색상이 바뀌어야 했죠.
// 뷰어 (iframe 내부)
window.addEventListener('message', (event) => {
// 출처 검증
if (event.origin !== 'https://client-shop.com') return;
const { type, payload } = event.data;
switch (type) {
case 'CHANGE_COLOR':
changeColor(payload.color);
break;
case 'CHANGE_PRODUCT':
loadProduct(payload.productId);
break;
}
});
// 로딩 완료 알림
parent.postMessage({ type: 'VIEWER_READY' }, 'https://client-shop.com');
// 부모 페이지 (쇼핑몰)
const viewer = document.querySelector('#product-viewer');
colorButtons.forEach(btn => {
btn.onclick = () => {
viewer.contentWindow.postMessage(
{ type: 'CHANGE_COLOR', payload: { color: btn.dataset.color } },
'https://viewer.example.com'
);
};
});
예상 못한 요청들 #
개발 중에 추가 요청이 들어왔습니다.
"치수 표시 기능 넣어주세요"
3D 공간에 치수 라인과 텍스트를 표시해야 했습니다.
function createDimensionLine(start, end, text) {
// 라인
const geometry = new THREE.BufferGeometry().setFromPoints([start, end]);
const material = new THREE.LineBasicMaterial({ color: 0x333333 });
const line = new THREE.Line(geometry, material);
// 텍스트 (CSS2DRenderer 사용)
const div = document.createElement('div');
div.className = 'dimension-label';
div.textContent = text;
const label = new CSS2DObject(div);
label.position.copy(start.clone().add(end).multiplyScalar(0.5));
return { line, label };
}
CSS2DRenderer를 사용해서 HTML 요소를 3D 공간에 배치했습니다. 순수 3D 텍스트보다 스타일링이 쉽고 선명합니다.
"스크린샷 저장 기능요"
고객이 제품 구성을 정하고 스크린샷을 저장할 수 있어야 했습니다.
function captureScreenshot() {
renderer.render(scene, camera);
const dataURL = renderer.domElement.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'product-preview.png';
link.href = dataURL;
link.click();
}
단, preserveDrawingBuffer를 true로 설정해야 합니다. 기본값은 false라서 toDataURL이 빈 이미지를 반환합니다.
const renderer = new THREE.WebGLRenderer({
preserveDrawingBuffer: true // 스크린샷용
});
납품과 피드백 #
2주 예정이 3주로 늘어났습니다. 추가 요청들 때문이었죠.
클라이언트 피드백:
- "조작이 직관적이에요" (OrbitControls 덕분)
- "모바일에서 좀 느린 것 같아요" → 저사양 폴백 추가
- "색상 바뀔 때 애니메이션이 있으면 좋겠어요" → GSAP으로 부드러운 전환 추가
최종 결과물에 만족해하셨고, 추가 제품 뷰어 제작도 의뢰받았습니다.
배운 점 #
1. 요구사항은 반드시 문서화
"360도로 볼 수 있게"가 정확히 무슨 뜻인지 명확히 해야 합니다. 자동 회전인지, 드래그 회전인지, 회전 축은 어떤 건지.
2. 3D 모델 확보가 선행
모델이 없으면 개발을 시작하기 어렵습니다. 모델 제작/확보 일정을 먼저 잡아야 합니다.
3. 모바일 테스트는 일찍
"마지막에 모바일 테스트하면 되겠지"는 위험합니다. 중간중간 테스트해야 합니다.
4. 버퍼 두기
2주 일정이면 10일 개발, 4일 버퍼로 잡으세요. 추가 요청과 버그 수정에 시간이 필요합니다.
3D 제품 뷰어는 이커머스에서 점점 더 중요해지고 있습니다. 기술적으로 어렵지 않지만, 디테일(로딩 속도, 모바일 대응, 사용성)에서 차이가 납니다.