
최신 JavaScript에서는 모듈 최상위 레벨에서 await를 사용할 수 있지만, 이는 빌드 시 특별한 처리가 필요합니다.
특히 다음과 같은 코드에서 문제가 발생합니다
// services/modelLoader.js
const model = await loadModel('assets/character.glb');
const texture = await loadTexture('assets/skin.png');
export { model, texture };
이러한 코드는 개발 환경에서는 잘 작동하지만, 프로드덕션 빌드 시 다음과 같은 에러가 발생합니다.
ERROR: Top-level await is not available in the configured target environment
실제 프로젝트에서는 다음과 같은 복잡한 상황들이 발생할 수 있습니다.
먼저 vite-plugin-top-level-await를 설치하고 기본 설정을 합니다.
pnpm install vite-plugin-top-level-await --save-dev
// vite.config.js
import { defineConfig } from 'vite';
import topLevelAwait from "vite-plugin-top-level-await";
import compression from 'vite-plugin-compression';
export default defineConfig({
assetsInclude: ['**/*.hdr', '**/*.glb', '**/*.gltf'],
plugins: [
topLevelAwait({
// 특정 파일 패턴만 처리
include: ['src/models/**/*.js', 'src/assets/**/*.js'],
// 특정 파일 제외
exclude: ['node_modules/**']
}),
compression() // 추가적인 최적화
],
build: {
rollupOptions: {
output: {
manualChunks: {
// 벤더 청크 분리
vendor: ['three', '@react-three/fiber'],
// 모델 청크 분리
models: ['src/models/index.js']
}
}
},
chunkSizeWarningLimit: 1000
}
});
실제 프로젝트에서는 다음과 같이 최적화된 모델 로딩 시스템을 구현할 수 있습니다.
// src/utils/modelLoader.js
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { LoadingManager } from 'three';
class ModelLoader {
constructor() {
this.loadingManager = new LoadingManager();
this.dracoLoader = new DRACOLoader(this.loadingManager);
this.gltfLoader = new GLTFLoader(this.loadingManager);
// Draco 디코더 경로 설정
this.dracoLoader.setDecoderPath('/draco/');
this.gltfLoader.setDRACOLoader(this.dracoLoader);
// 로딩 캐시 설정
this.modelCache = new Map();
}
async loadModel(path, options = {}) {
// 캐시 확인
if (this.modelCache.has(path)) {
return this.modelCache.get(path);
}
try {
const model = await new Promise((resolve, reject) => {
this.gltfLoader.load(
path,
(gltf) => {
// 모델 후처리
if (options.scale) {
gltf.scene.scale.multiplyScalar(options.scale);
}
resolve(gltf);
},
(progress) => {
if (options.onProgress) {
options.onProgress(progress);
}
},
reject
);
});
// 캐시에 저장
this.modelCache.set(path, model);
return model;
} catch (error) {
console.error(`Failed to load model: ${path}`, error);
throw error;
}
}
// 메모리 관리
clearCache() {
this.modelCache.forEach((model) => {
model.scene?.traverse((object) => {
if (object.geometry) {
object.geometry.dispose();
}
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(material => material.dispose());
} else {
object.material.dispose();
}
}
});
});
this.modelCache.clear();
}
}
export const modelLoader = new ModelLoader();
// src/components/Scene.jsx
import { Suspense, useEffect, useState } from 'react';
import { modelLoader } from '../utils/modelLoader';
export const Scene = () => {
const [model, setModel] = useState(null);
const [loading, setLoading] = useState(true);
const [progress, setProgress] = useState(0);
useEffect(() => {
const loadModel = async () => {
try {
const gltf = await modelLoader.loadModel('/models/character.glb', {
scale: 1.5,
onProgress: (progress) => {
const percentage = (progress.loaded / progress.total) * 100;
setProgress(Math.round(percentage));
}
});
setModel(gltf.scene);
} catch (error) {
console.error('모델 로딩 실패:', error);
} finally {
setLoading(false);
}
};
loadModel();
// 컴포넌트 언마운트 시 메모리 정리
return () => {
if (model) {
modelLoader.clearCache();
}
};
}, []);
if (loading) {
return <div className="loading">Loading... {progress}%</div>;
}
return (
<Suspense fallback={null}>
{model && (
<primitive object={model} position={[0, 0, 0]} />
)}
</Suspense>
);
};
// vite.config.js 추가 설정
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
// ... 기존 설정 ...
plugins: [
// ... 기존 플러그인 ...
visualizer({
filename: 'stats.html',
open: true,
gzipSize: true
})
]
});
// src/utils/performanceMonitor.js
class PerformanceMonitor {
constructor() {
this.metrics = {
modelLoadTimes: new Map(),
memoryUsage: new Map(),
fps: []
};
}
startModelLoadTimer(modelPath) {
this.metrics.modelLoadTimes.set(modelPath, {
startTime: performance.now(),
endTime: null,
duration: null
});
}
endModelLoadTimer(modelPath) {
const metric = this.metrics.modelLoadTimes.get(modelPath);
if (metric) {
metric.endTime = performance.now();
metric.duration = metric.endTime - metric.startTime;
}
}
trackMemoryUsage() {
if (performance.memory) {
this.metrics.memoryUsage.set(Date.now(), {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize
});
}
}
getMetrics() {
return this.metrics;
}
generateReport() {
const report = {
modelLoadTimes: {},
averageMemoryUsage: null,
averageFPS: null
};
// 모델 로딩 시간 분석
this.metrics.modelLoadTimes.forEach((value, key) => {
report.modelLoadTimes[key] = `${value.duration.toFixed(2)}ms`;
});
// 메모리 사용량 평균 계산
if (this.metrics.memoryUsage.size > 0) {
const totalMemory = Array.from(this.metrics.memoryUsage.values())
.reduce((acc, curr) => acc + curr.usedJSHeapSize, 0);
report.averageMemoryUsage =
`${((totalMemory / this.metrics.memoryUsage.size) / 1024 / 1024).toFixed(2)}MB`;
}
return report;
}
}
export const performanceMonitor = new PerformanceMonitor();
// src/utils/errorBoundary.js
import React from 'react';
class ModelErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// 에러 로깅 서비스로 전송
console.error('Model loading failed:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h2>모델을 불러오는데 실패했습니다</h2>
<button onClick={() => window.location.reload()}>
다시 시도
</button>
</div>
);
}
return this.props.children;
}
}
export default ModelErrorBoundary;
이러한 심화된 구현을 통해 다음과 같은 이점을 얻을 수 있습니다.
특히 대규모 3D 애플리케이션에서는 이러한 최적화가 필수적이며, 사용자 경험을 크게 향상시킬 수 있습니다.