밝을희 클태

react-three/drei Environment 태그 렌더링 블로킹 본문

KEYNUT 프로젝트

react-three/drei Environment 태그 렌더링 블로킹

huipark 2024. 11. 29. 14:49

Blender로 만든 3D 모델을 GLB 파일로 export하여 프로젝트의 메인 페이지에 넣으려고 했다.

 

그러나 GLB 파일의 크기가 큰 편이라, 네트워크가 조금이라도 느린 상황에서는 빈 화면이 오래 노출될 수 있었다. 이를 방지하기 위해, GLB 파일 다운로드 상태를 실시간으로 표시하는 기능을 구현했다. 진행 상황을 1%부터 100%까지 표현하려고 했다.

전체 코드

'use client';

import React, { useState, useEffect } from 'react';
import { Canvas } from '@react-three/fiber';
import { Environment, OrbitControls, useGLTF } from '@react-three/drei';

function Model({ url }: { url: string }) {
  const { scene } = useGLTF(url);

  return <primitive object={scene} />;
}

export default function ModelViewer() {
  const [modelUrl, setModelUrl] = useState<string | null>(null);
  const [progress, setProgress] = useState<number>(0);

  useEffect(() => {
    const downloadModel = async () => {
      try {
        const response = await fetch(`${process.env.NEXT_PUBLIC_IMAGE_DOMAIN}/case_cherry.glb`);
        const reader = response.body?.getReader();
        const contentLength = response.headers.get('Content-Length');
        const total = contentLength ? Number(contentLength) : null;

        if (!reader || !total) {
          console.error('ReadableStream not supported or Content-Length unavailable.');
          return;
        }

        let received = 0;
        const chunks: Uint8Array[] = [];

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          if (value) {
            chunks.push(value);
            received += value.length;
            setProgress(Math.round((received / total) * 100));
          }
        }

        const blob = new Blob(chunks, { type: 'application/octet-stream' });
        const blobUrl = URL.createObjectURL(blob);
        setModelUrl(blobUrl);
      } catch (error) {
        console.error('Failed to download model:', error);
      }
    };

    downloadModel();
  }, []);

  return (
    <div className="w-full h-full relative flex justify-center items-center">
      <Canvas camera={{ position: [-40, 25, 50], fov: 7 }} shadows>
        <Environment files="/textures/shanghai_bund_1k.hdr" />
        {modelUrl && <Model url={modelUrl} />}
        {modelUrl && <OrbitControls autoRotate enableZoom={false} />}
      </Canvas>
      <div className={`absolute transition-opacity ${modelUrl ? 'opacity-0' : 'opacity-100'}`}>
        <progress value={progress} max="100" aria-valuenow={progress} aria-valuemin={0} aria-valuemax={100}></progress>
      </div>
    </div>
  );
}

문제

GLB 다운로드 진행 상황이 1%부터 매끄럽게 표시되지 않고, 중간부터 진행 상황이 표시되는 문제가 발생했다.

원인

<Environment> 태그에서 HDR 파일을 다운로드하는 동안 UI 리렌더링이 블로킹되는 것이 원인이었다.

React의 상태(setProgress)가 업데이트되어도, Environment의 비동기 작업이 진행 중일 때 UI가 즉시 반영되지 않았다.

해결

React의 <Suspense> 태그로 <Environment>를 감싸주어 해결했다.

<Suspense>
  <Environment files="/textures/shanghai_bund_1k.hdr" />
</Suspense>

Suspense가 해결한 이유 (예측)

<Environment> 태그가 HDR 파일을 다운로드하는 동안, 는 React에게 해당 작업이 비동기적임을 알려서 UI 업데이트 블로킹을 해결하는 거 같다?.