5. 우리만의 변별력: 사용자 경험의 끝 && 성능 최적화
2026년 2월 11일
수상을 목표로 하는 것과 별개로, 이 아이디어, 3D뷰어 시스템을 웹에 올리는 이 소프트웨어를 만들고 이것을 가지고 mvp및 스타트업을 한다고 했을때, 가장 중요한점은 무엇인지 생각해 보았을 때
1.디테일한 사용자 경험(경험에 편리하고, 자유도가 높으며 안정적인)
2.성능최적화(프레임이 끊기지 않는)
이 두가지를 모두 챙기는 것이 가장 중요하다고 생각했습니다.
이 두가지를 모두 챙기는 것에서, 보통의 사이트들 보다도 프론트엔드의 역할이 훨씬 중요합니다. 렌더링과 3d 환경의 모든 기능은 프론트에서 구현을 해야 하니까요.
3D 환경의 모든 구현을 담당한 저는 많으 노력을 했지만, 특히나 아래 세가지 경험에서 큰 성과를 가져왔습니다.
1. 부품별 기즈모와 렌더 모드 — 모델은 하나, 환경만 바꾸기
1-1. 부품별 기즈모
각 부품마다 이동·회전·스케일을 위한 기즈모(TransformControls)를 붙였습니다.
사용자가 부품을 선택하면 해당 객체에만 기즈모가 표시되고, 드래그로 위치·각도·크기를 조정할 수 있습니다.
1-2. 세 가지 렌더 모드
- lit: 밝은 조명 (단축키 1)

- dim: 어두운 조명 (단축키 2)

- wireframe: 선만 보이는 와이어프레임 (단축키 3)

저는 기본적으로 5가지 조명세팅을 하였습니다. 기본 전체 조명, 반구 조명(위아래 밝기 구분을 위한), 메인 조명, 서브 조명(그림자 처리), 그리고 환경맵까지.
재질의 특성을 살리며 실제 같은 렌더링 환경을 위해 이와 같이 제공했습니다.
다만 밝은 조명 하나 만으로는, 광택이 나는 재질이 너무 뿌옇게 보일 수 있어, 어두운 조명 버전을 제공하였습니다.
1-3. 성능을 위한 선택: 모델 복제 대신 환경 변경
처음에는 lit / dim / wireframe마다 모델을 따로 로드하는 방식을 고민했습니다.
하지만 이렇게 하면:
- 모델을 3번 다운로드
- 메모리에 3벌 보관
- 전환 시 추가 로딩
대신 모델은 하나만 두고, 환경만 바꾸는 방식으로 구현했습니다.
- 조명: lit / dim에 따라 Ambient, Hemisphere, Directional Light 강도만 조절
- 와이어프레임: 모든 Mesh를 순회하며
material.wireframe = true/false토글
모델을 추가로 받지 않고, 조명과 머티리얼 옵션만 바꿔서 성능적으로 이점을 챙길 수 있었습니다.
2. 조립·분해 + 위치 저장 — 매트릭스와 분해도를 분리해서 저장

2-1. 사용자가 원하는 저장
- 조립/분해 슬라이더 값 유지
- 각 부품의 위치 유지
두 가지가 함께 저장·복원되어야 합니다.
2-2. 문제: 실제 위치를 그대로 저장했을 때
처음에는 현재 화면에 보이는 위치를 그대로 저장했습니다.
그 결과:
- 조립/분해 애니메이션에 따른 위치와, 사용자가 이동시킨 위치가 같은 값에 섞여 저장됨
- 조립/분해 레벨과 위치가 중복되어, 복원 시 위치가 어긋나거나 깨지는 현상 발생
2-3. 해결: 조립 기준 매트릭스 + 분해도 분리 저장
저장 구조를 바꿨습니다.
- 매트릭스: 조립 상태(0%)를 기준으로 한 부품별 변환 행렬만 저장
→ 사용자가 이동·회전·스케일한 “변위”만 저장 - 분해도: 조립/분해 슬라이더 값(0–100)을 별도 필드로 저장
복원 시에는:
- 서버에서 매트릭스와 분해도를 각각 가져옴
- 프론트에서 조립 기준 매트릭스를 먼저 적용
- 그 위에 분해도에 따른 애니메이션을 적용
이렇게 하면 조립/분해와 사용자 변형이 겹치지 않고, 복원 시에도 위치가 정확하게 맞습니다. 이를 통해, 사용자가 기대하는 저장,복원을 구현할 수 있었습니다.
3. 다양한 최적화 시도 — DPR 제한
3-1. forwardRef로 불필요한 상태 줄이기
부모에서 3D 씬을 제어할 때, zoom, 포커스, 스냅샷 등 여러 동작이 필요했습니다.
이걸 콜백 props로 넘기면:
- 부모가 리렌더될 때마다 새 함수 참조 생성
- 자식도 함께 리렌더될 가능성 증가
대신 forwardRef + useImperativeHandle로:
- 부모는
scene3DRef.current.zoomIn()처럼 명령형 호출만 함 - zoom, 포커스 등에 대한 추가 state/props를 만들지 않음
- 3D 씬 쪽으로 전달되는 props를 줄여, 이후
React.memo적용 시 이득을 볼 수 있게 함
3-2. 메모리 명시적 해제
- Blob URL:
URL.createObjectURL로 만든 URL은URL.revokeObjectURL로 해제 - Geometry / Material: 선택 아웃라인 제거 시
dispose()호출 - RenderTarget: 스냅샷 후
dispose()호출 - unmount 시:
modelUrls.revoke()로 Blob URL 일괄 해제
자동으로 해제되지 않는 리소스를 명시적으로 정리해 메모리 누수를 막았습니다.
3-3. 불필요한 업데이트 줄이기
- useObjectInfo: 이전 값과 비교해 실제로 변경된 경우에만 콜백 호출
- useSelectionSync: 이전 배열과 비교해 변경 시에만
setState
드래그·선택 시 React 리렌더 빈도를 줄였습니다.
3-4. 생각지도 못한 가장 효과적이었던 것 : DPR 2로 제한
여러 최적화를 시도했지만, 그럼에도 불구하고 어쨌든 gpu를 사용하고 무거울 수 밖에 없어, 창을 여러개 띄우거나 다른 무거운 프로그램을 돌리면서 사용하면 프레임이 끊기는 현상이 생기기도 했습니다.
이에 더 발전시킬 수 있는 것이 무엇이 있을까 계속 고민했고,
가장 눈에 띄게 효과가 있었던 것은 DPR(Device Pixel Ratio)을 2로 제한한 것이었습니다.
- 맥북프로 등 고해상도 디스플레이에서는 DPR이 3까지 올라감
- DPR이 높을수록 렌더링할 픽셀 수가 급격히 증가
- 예: 800×600 캔버스에서 DPR 3 → 2400×1800 = 432만 픽셀
R3F Canvas에 dpr={[1, 2]}를 설정해:
- DPR 3 디스플레이에서도 최대 2까지만 사용
- 렌더 비용을 크게 줄이면서, 화질 저하는 크지 않음
복잡한 로직보다 단순한 DPR 제한이 성능에 가장 큰 영향을 줬습니다.
| 구분 | 내용 |
|---|---|
| 1. 기능·성능 | 부품별 기즈모 + lit/dim/wireframe 3모드, 모델 1개만 로드하고 환경만 변경 |
| 2. 저장 구조 | 조립 기준 매트릭스 + 분해도 분리 저장, 프론트에서 조합해 렌더링 |
| 3. 최적화 | forwardRef, 메모리 해제, 불필요한 업데이트 감소, DPR 2 제한이 가장 효과적 |
프론트엔드 개발자로서, 사용자 환경을 항상 우선시 하고 깊이있게 고민한다는 것은 이러한 태도를 말하는 것입니다.
특히나 이 주제에 대해서, 다른 많은 참가자들이 단순 구현에 목매고 AI를 활용해 코드를 어떻게든 짜 올 것입니다.
하지만 3D뷰어 웹사이트 구현에는, 단순 구현이 목적이 되면 안된다고 생각합니다. 사용성의 끝을 바라보고, 코드를 이해하고 뜯어보며 문제가 있지 않을까, 성능적으로 병목이 있지 않을까, 더 좋은 기능과 더 좋은 환경을 발전시킬 수 있지 않을까 고민해야합니다.
이 프로젝트 뿐 아니라 앞으로 모든 작업에서 이와같은 태도를 지속해서 유지해야 합니다.