Dev

3D 파일을 브라우저에서 열게 된 이야기 — Three.js 뷰어 개발기

toolzip 2026. 10. 8. 19:00

OBJ, FBX, STL, PLY... 8가지 3D 파일 포맷을 브라우저에서 열 수 있는 뷰어를 만들었습니다. 생각보다 쉽지 않았습니다.


왜 만들었나요?

3D 파일을 확인하려면 보통 Blender나 MeshLab 같은 무거운 프로그램을 설치해야 합니다. 단순히 "이 파일이 어떻게 생겼지?" 확인하는 것뿐인데 수백MB짜리 프로그램을 설치하는 건 과합니다.

브라우저에서 바로 열 수 있으면 어떨까 생각했습니다. 파일이 서버로 전송되지 않으면 더 좋고요.


Three.js를 선택한 이유

브라우저에서 3D를 렌더링하는 방법은 WebGL입니다. 하지만 WebGL을 직접 쓰는 건 C++로 게임 엔진을 만드는 것처럼 복잡합니다.

Three.js는 WebGL 위에서 동작하는 라이브러리입니다. 복잡한 WebGL 코드를 추상화해서 JavaScript로 쉽게 3D를 다룰 수 있게 해줍니다.


첫 번째 문제 — CDN vs npm

처음에는 CDN으로 Three.js를 불러오는 방식을 썼습니다.

<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js">

그런데 이 방식은 타이밍 문제가 있었습니다. 스크립트가 로드되기 전에 컴포넌트가 초기화되면 window.THREE is undefined 에러가 납니다.

npm으로 설치하고 import하는 방식으로 바꿨습니다. 이 방식은 모듈이 사용되기 전에 반드시 로드됩니다.


두 번째 문제 — 포맷마다 다른 로더

OBJ, FBX, STL, PLY 각각의 포맷마다 다른 로더가 필요합니다. 그런데 각 로더마다 반환하는 데이터 구조가 조금씩 달랐습니다.

OBJ 로더: THREE.Group을 반환합니다.
FBX 로더: THREE.Group을 반환하지만 애니메이션이 포함됩니다.
STL 로더: THREE.BufferGeometry를 직접 반환합니다. 직접 Mesh를 만들어야 합니다.
PLY 로더: THREE.BufferGeometry를 반환합니다. 컬러 정보가 포함될 수 있습니다.

각 로더의 특성에 맞게 처리하는 공통 함수를 만드는 데 시간이 걸렸습니다.


세 번째 문제 — GLB의 Draco 압축

GLB 파일 중 일부는 Draco 압축을 사용합니다. 이런 파일은 Draco 디코더 없이는 열 수 없습니다.

Draco 디코더는 WebAssembly 파일로 약 150KB입니다. Google CDN에서 불러올 수 있습니다.

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
gltfLoader.setDRACOLoader(dracoLoader);

이 설정 없이는 Draco 압축된 GLB 파일이 조용히 실패합니다. 에러 메시지도 명확하지 않아서 원인을 찾는 데 시간이 걸렸습니다.


네 번째 문제 — BVH 파일의 좌표 스케일

BVH는 모션 캡처 데이터 포맷입니다. 뼈대(Skeleton)와 애니메이션 데이터만 있고 메시가 없습니다.

BVH 파일의 좌표는 센티미터 단위입니다. 그냥 렌더링하면 화면에 아무것도 보이지 않거나 너무 크게 나옵니다. 뼈대의 실제 크기를 계산해서 적절히 스케일을 조정해야 합니다.

const bonePositions = skeleton.bones.map(b => b.position);
const maxDim = Math.max(
  Math.max(...bonePositions.map(p => p.x)) - Math.min(...bonePositions.map(p => p.x)),
  Math.max(...bonePositions.map(p => p.y)) - Math.min(...bonePositions.map(p => p.y))
);
group.scale.setScalar(2.5 / maxDim);

다섯 번째 문제 — React의 stale closure

자동 회전 기능을 만들 때 버그가 있었습니다. autoRotate 상태를 requestAnimationFrame 루프 안에서 읽으면, 항상 초기값만 읽히는 문제입니다.

React의 stale closure 문제입니다. 해결책은 ref를 사용하는 것입니다.

const stateRef = useRef({ autoRotate: true });

useEffect(() => {
  stateRef.current.autoRotate = autoRotate;
}, [autoRotate]);

// 애니메이션 루프 안에서:
if (stateRef.current.autoRotate) rotY += 0.2;

결과

OBJ, GLB, GLTF, FBX, STL, DAE, BVH, PLY 8가지 포맷을 지원하는 뷰어가 완성됐습니다. 드래그 앤 드롭으로 파일을 올리면 바로 3D 모델을 볼 수 있습니다. 파일이 서버로 전송되지 않습니다.

생각보다 많은 문제가 있었지만, 하나씩 해결하는 과정이 재미있었습니다.