본문으로 건너뛰기
← 블로그로 돌아가기
개발>웹 개발
Three.jsR3FWebGL

웹에서 3D 구현하기: Three.js와 React Three Fiber

4분 읽기

웹에서 3D를 써야 할 이유가 있느냐고 묻는다면 — White Place 히어로 섹션을 보여주겠습니다. 아이콘 큐브들이 둥둥 떠다니고, 마우스를 따라 살짝 기울어지는 장면. 이걸 CSS만으로는 절대 못 만듭니다.

Three.js와 React Three Fiber의 차이

Three.js는 WebGL을 감싼 JavaScript 라이브러리입니다. 강력하지만 사용법이 명령형(imperative)입니다. 장면을 만들고, 카메라를 놓고, 오브젝트를 추가하고, 렌더 루프를 돌리고 — 단계가 많습니다.

React Three Fiber(R3F)는 이 과정을 React 컴포넌트로 바꿔줍니다.

// Three.js 방식 — 코드가 길어집니다
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: "white" });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// React Three Fiber 방식 — JSX로 선언합니다
<mesh>
  <boxGeometry args={[1, 1, 1]} />
  <meshStandardMaterial color="white" />
</mesh>

R3F는 Three.js의 모든 기능을 그대로 쓸 수 있으면서, React의 상태 관리와 컴포넌트 구조를 활용할 수 있습니다. useFrame 훅으로 매 프레임마다 애니메이션을 실행하고, useLoader로 텍스처를 불러오고, useThree로 카메라와 뷰포트 정보에 접근합니다.

한 가지 주의점은 R3F가 "React처럼 보이지만 성능 특성은 Three.js와 같다"는 것입니다. 컴포넌트를 잘못 설계하면 매 프레임마다 re-render가 일어나서 프레임 드롭이 생깁니다. useMemo, useRef를 적극적으로 써야 합니다.

글래스모피즘 큐브 만들기

White Place의 아이콘 큐브가 유리처럼 보이는 이유는 meshPhysicalMaterial 덕분입니다.

<RoundedBox args={[1, 1, 1]} radius={0.12} smoothness={4}>
  <meshPhysicalMaterial
    color="#ffffff"
    roughness={0.08}       // 낮을수록 반짝임
    metalness={0.02}       // 금속 느낌 최소화
    clearcoat={1}          // 코팅 레이어
    clearcoatRoughness={0.03}
    envMapIntensity={1.5}  // 환경맵 반사 강도
  />
</RoundedBox>

여기에 <Environment preset="studio" />를 추가하면 스튜디오 조명이 반사되어 진짜 유리 같은 느낌이 납니다. @react-three/drei 라이브러리의 RoundedBox를 사용하면 모서리가 둥근 큐브를 간단하게 만들 수 있습니다.

큐브 여러 개를 배치할 때는 Float 컴포넌트로 감싸면 각각 다른 속도로 부유하는 효과를 줄 수 있습니다.

<Float speed={1.2} rotationIntensity={0.05} floatIntensity={0.15}>
  {/* 큐브 컴포넌트 */}
</Float>

speed, rotationIntensity, floatIntensity 세 값만 조절하면 자연스러운 부유 애니메이션이 완성됩니다.

마우스 인터랙션 추가하기

정적인 3D 씬은 이미지와 다를 게 없습니다. 인터랙션이 있어야 3D를 쓰는 의미가 있습니다.

White Place에서는 마우스 위치에 따라 전체 씬이 살짝 기울어집니다. 구현 원리는 단순합니다.

function SceneGroup({ children }) {
  const groupRef = useRef(null);
  const target = useRef({ x: 0, y: 0 });

  useFrame(({ pointer }) => {
    // pointer.x, pointer.y는 -1 ~ 1 범위
    target.current.x += (pointer.y * 0.08 - target.current.x) * 0.03;
    target.current.y += (pointer.x * 0.12 - target.current.y) * 0.03;
    groupRef.current.rotation.x = target.current.x;
    groupRef.current.rotation.y = target.current.y;
  });

  return <group ref={groupRef}>{children}</group>;
}

핵심은 0.03이라는 보간 계수입니다. 마우스를 즉시 따라가는 게 아니라 천천히 따라가면서 부드러운 움직임을 만듭니다. 이 값을 올리면 반응이 빨라지고, 내리면 느려집니다.

성능 최적화 — 이걸 안 하면 모바일에서 버벅입니다

3D 웹은 성능에 민감합니다. 몇 가지 실전 팁을 공유합니다.

DPR 제한. 레티나 디스플레이에서 원본 해상도로 렌더링하면 GPU가 혹사당합니다.

<Canvas dpr={[1, 1.5]}>

dpr={[1, 1.5]}로 최대 DPR을 제한하면 시각적 차이는 거의 없으면서 성능이 크게 개선됩니다.

Dynamic Import. Next.js에서 3D 컴포넌트는 반드시 SSR을 끄고 지연 로드해야 합니다.

const HeroScene = dynamic(() => import("@/components/HeroScene"), {
  ssr: false,
  loading: () => <LoadingSkeleton />,
});

ssr: false를 빼먹으면 서버에서 Three.js를 실행하려다 에러가 납니다. window 객체가 없기 때문입니다.

텍스처 최적화. 아이콘 이미지는 최대 256x256 PNG로 충분합니다. 1024x1024 이미지를 큐브에 붙이면 GPU 메모리만 낭비됩니다.

Suspense 활용. 텍스처 로딩 중에 빈 화면이 보이면 사용자 경험이 나쁩니다.

<Suspense fallback={null}>
  <Scene />
</Suspense>

실제로 만들어보면서 느낀 점

처음에는 Three.js 문서를 보면서 "이걸 웹에서 할 수 있다고?" 싶었습니다. R3F를 쓰면 React 개발 경험만 있어도 3D를 시작할 수 있습니다.

다만 "3D가 멋있으니까 넣자"는 위험한 생각입니다. 의미 있는 곳에만 써야 합니다. White Place에서는 히어로 섹션에만 3D를 넣고 나머지는 CSS로 처리했습니다. 모든 섹션에 3D를 넣으면 사이트가 무거워질 뿐입니다.

첫 3D 프로젝트로는 로고 큐브 하나를 띄워서 마우스를 따라 회전시키는 것을 추천합니다. 거기서부터 하나씩 확장하면 됩니다.

관련 포스트