본문 바로가기
React

[React] Three Fiber 로 3D 효과 만들기

by teamnova 2025. 7. 16.
728x90

 

안녕하세요 오늘은 WebGL 기반 3D 그래픽 라이브러리인 three 를 사용해 3D 효과를 구현해보도록 하겠습니다. 

 

먼저 three 는 WebGL 기반의 3D 그래픽 라이브러리 입니다. 

브라우저에서 3D 그래픽을 구현할 수 있도록 도와주는 자바스크립트 라이브러리로,

기본적으로 WebGL API를 추상화하여 훨씬 쉽게 3D 씬, 모델, 애니메이션 등을 구현할 수 있게 해줍니다.

 

Thress.js 를 리액트 환경에서 사용하기 위해서는 React 랜더러인 "React Three Fiber" 를 설치해주어야 합니다. 

react-three-fiber 는 React의 컴포넌트 기반 철학을 유지하면서 3D 그래픽을 구성할 수 있도록 해주는 React 렌더러입니다.

 

 

 

1. react-three-fiber 설치하기 

아래 사이트를 확인해 본인의 개발환경에 맞는 install 설치 명령어를 확인합니다. 

 

Installation - React Three Fiber

Learn how to install react-three-fiber

r3f.docs.pmnd.rs

 

현재 리액트 vite 프로젝트이므로 아래 명령어를 사용하겠습니다.

cd my-app #my-app 에는 본인의 react 프로젝트명을 입력합니다. (프로젝트 루트 경로) 
npm install three @react-three/fiber

 

 

 

 

2. 이외에 필요한 라이브러리 설치 

npm install three @react-three/fiber @react-three/drei

 

three: 핵심 3D 라이브러리
@react-three/fiber: React 렌더러
@react-three/drei: 유용한 헬퍼 컴포넌트들 (OrbitControls, Text 등)

 

 

 

3. 기존 프로젝트에 3d 효과를 추가합니다. 

기존 프로젝트는 아래 링크에서 확인하실 수 있습니다. 

https://stickode.tistory.com/1562

 

[React] react-scroll을 활용한 부드러운 앵커 스크롤 (Anchor Scroll) 구현하기

안녕하세요 오늘은 React 환경에서 react-scroll 라이브러리를 활용해 클릭 시 부드럽게 섹션으로 이동하는 스크롤 내비게이션을 만들어보겠습니다. 1. 먼저 생성돼 있는 react 프로젝트로 이동합니다

stickode.tistory.com

 

추가된 파일 내용입니다. 

 

1) ThreeDScene.jsx

import React from 'react'
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import { OrbitControls, Text, Box, Sphere, Cylinder } from '@react-three/drei'
import { useRef, useState } from 'react'
import * as THREE from 'three'

// 회전하는 카드 컴포넌트
function RotatingCard({ position, color, text, speed = 1 }) {
  const meshRef = useRef()
  const [hovered, setHover] = useState(false)
  const [clicked, setClicked] = useState(false)

  useFrame((state) => {
    if (meshRef.current) {
      meshRef.current.rotation.y += 0.01 * speed
      meshRef.current.rotation.x = Math.sin(state.clock.elapsedTime * 0.5) * 0.1
    }
  })

  return (
    <group position={position}>
      <Box
        ref={meshRef}
        args={[2, 3, 0.1]}
        onClick={() => setClicked(!clicked)}
        onPointerOver={() => setHover(true)}
        onPointerOut={() => setHover(false)}
        scale={clicked ? 1.2 : hovered ? 1.1 : 1}
      >
        <meshStandardMaterial 
          color={hovered ? '#ff6b6b' : color} 
          metalness={0.3}
          roughness={0.4}
        />
      </Box>
      <Text
        position={[0, 0, 0.06]}
        fontSize={0.3}
        color="white"
        anchorX="center"
        anchorY="middle"
        font="/fonts/Inter-Bold.woff"
      >
        {text}
      </Text>
    </group>
  )
}

// 떠다니는 구체들
function FloatingSpheres() {
  const spheres = useRef([])

  useFrame((state) => {
    spheres.current.forEach((sphere, i) => {
      if (sphere) {
        sphere.position.y = Math.sin(state.clock.elapsedTime + i) * 2 + 5
        sphere.rotation.x += 0.01
        sphere.rotation.y += 0.01
      }
    })
  })

  return (
    <>
      {[...Array(5)].map((_, i) => (
        <Sphere
          key={i}
          ref={(el) => (spheres.current[i] = el)}
          args={[0.3, 16, 16]}
          position={[
            (i - 2) * 4,
            Math.sin(i) * 2 + 5,
            Math.cos(i) * 2
          ]}
        >
          <meshStandardMaterial 
            color={`hsl(${i * 60}, 70%, 60%)`}
            transparent
            opacity={0.8}
          />
        </Sphere>
      ))}
    </>
  )
}

// 배경 원통들
function BackgroundCylinders() {
  const cylinders = useRef([])

  useFrame((state) => {
    cylinders.current.forEach((cylinder, i) => {
      if (cylinder) {
        cylinder.rotation.y += 0.005 * (i + 1)
        cylinder.position.y = Math.sin(state.clock.elapsedTime * 0.5 + i) * 0.5
      }
    })
  })

  return (
    <>
      {[...Array(3)].map((_, i) => (
        <Cylinder
          key={i}
          ref={(el) => (cylinders.current[i] = el)}
          args={[0.5, 0.5, 8, 8]}
          position={[
            (i - 1) * 8,
            0,
            -10
          ]}
        >
          <meshStandardMaterial 
            color={`hsl(${i * 120}, 50%, 40%)`}
            transparent
            opacity={0.3}
            wireframe
          />
        </Cylinder>
      ))}
    </>
  )
}

// 메인 3D 씬 컴포넌트
function ThreeDScene() {
  const { camera } = useThree()

  // 카메라 초기 위치 설정
  React.useEffect(() => {
    camera.position.set(0, 5, 10)
    camera.lookAt(0, 0, 0)
  }, [camera])

  return (
    <div className="w-full h-screen">
      <Canvas
        camera={{ position: [0, 5, 10], fov: 75 }}
        style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}
      >
        {/* 조명 */}
        <ambientLight intensity={0.4} />
        <pointLight position={[10, 10, 10]} intensity={1} />
        <pointLight position={[-10, -10, -10]} intensity={0.5} color="#ff6b6b" />

        {/* 카드들 */}
        <RotatingCard 
          position={[-4, 0, 0]} 
          color="#4ecdc4" 
          text="React" 
          speed={1}
        />
        <RotatingCard 
          position={[0, 0, 0]} 
          color="#45b7d1" 
          text="Three.js" 
          speed={1.2}
        />
        <RotatingCard 
          position={[4, 0, 0]} 
          color="#96ceb4" 
          text="WebGL" 
          speed={0.8}
        />

        {/* 떠다니는 구체들 */}
        <FloatingSpheres />

        {/* 배경 원통들 */}
        <BackgroundCylinders />

        {/* 컨트롤 */}
        <OrbitControls 
          enablePan={true}
          enableZoom={true}
          enableRotate={true}
          maxDistance={20}
          minDistance={5}
        />
      </Canvas>
    </div>
  )
}

export default ThreeDScene

 

주요 기능입니다.

- 자동회전, 마우스 호버시 색상 변경, 클릭하면 크기가 커지는 등 3d 형태로 구현된 모습입니다. 

- FloatingSpheres 컴포넌트에서는 떠다니는 구체 3개에 대한 회전 방향, 투명도 효과, 움직임 방향등을 설정합니다. 

 

 

2) ThreeDPortfolio.jsx

import React from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { OrbitControls, Box, Sphere, Text3D } from '@react-three/drei'
import { useRef, useState } from 'react'

// 회전하는 기술 스택 박스
function TechBox({ position, color, tech, speed = 1 }) {
  const meshRef = useRef()
  const [hovered, setHover] = useState(false)
  const [clicked, setClicked] = useState(false)

  useFrame((state) => {
    if (meshRef.current) {
      meshRef.current.rotation.y += 0.01 * speed
      meshRef.current.rotation.x = Math.sin(state.clock.elapsedTime * 0.5) * 0.1
    }
  })

  return (
    <Box
      ref={meshRef}
      args={[1.5, 1.5, 1.5]}
      position={position}
      onClick={() => setClicked(!clicked)}
      onPointerOver={() => setHover(true)}
      onPointerOut={() => setHover(false)}
      scale={clicked ? 1.3 : hovered ? 1.1 : 1}
    >
      <meshStandardMaterial 
        color={hovered ? '#ff6b6b' : color} 
        metalness={0.5}
        roughness={0.2}
        transparent
        opacity={0.9}
      />
    </Box>
  )
}

// 떠다니는 구체들
function FloatingSpheres() {
  const spheres = useRef([])

  useFrame((state) => {
    spheres.current.forEach((sphere, i) => {
      if (sphere) {
        sphere.position.y = Math.sin(state.clock.elapsedTime + i) * 1.5 + 3
        sphere.rotation.x += 0.01
        sphere.rotation.y += 0.01
      }
    })
  })

  return (
    <>
      {[...Array(3)].map((_, i) => (
        <Sphere
          key={i}
          ref={(el) => (spheres.current[i] = el)}
          args={[0.2, 16, 16]}
          position={[
            (i - 1) * 3,
            Math.sin(i) * 1.5 + 3,
            Math.cos(i) * 1.5
          ]}
        >
          <meshStandardMaterial 
            color={`hsl(${i * 120}, 70%, 60%)`}
            transparent
            opacity={0.7}
          />
        </Sphere>
      ))}
    </>
  )
}

// 3D 씬 컴포넌트
function ThreeDScene() {
  return (
    <div className="w-full h-96">
      <Canvas
        camera={{ position: [0, 3, 8], fov: 60 }}
        style={{ background: 'transparent' }}
      >
        {/* 조명 */}
        <ambientLight intensity={0.6} />
        <pointLight position={[10, 10, 10]} intensity={1} />
        <pointLight position={[-10, -10, -10]} intensity={0.3} color="#ff6b6b" />

        {/* 기술 스택 박스들 */}
        <TechBox 
          position={[-3, 0, 0]} 
          color="#61dafb" 
          tech="React"
          speed={1}
        />
        <TechBox 
          position={[0, 0, 0]} 
          color="#f7df1e" 
          tech="JavaScript"
          speed={1.2}
        />
        <TechBox 
          position={[3, 0, 0]} 
          color="#38bdf8" 
          tech="Three.js"
          speed={0.8}
        />

        {/* 떠다니는 구체들 */}
        <FloatingSpheres />

        {/* 컨트롤 */}
        <OrbitControls 
          enablePan={false}
          enableZoom={true}
          enableRotate={true}
          maxDistance={15}
          minDistance={5}
          autoRotate
          autoRotateSpeed={0.5}
        />
      </Canvas>
    </div>
  )
}

// 메인 포트폴리오 섹션
function ThreeDPortfolio() {
  return (
    <section className="min-h-screen flex flex-col justify-center items-center bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-900 py-20 px-6">
      <div className="max-w-6xl mx-auto">
        <div className="text-center mb-12">
          <h2 className="text-5xl font-bold text-white mb-6">
            3D Interactive Portfolio
          </h2>
          <p className="text-xl text-gray-300 max-w-3xl mx-auto leading-relaxed">
            Explore my skills and projects in an immersive 3D environment. 
            Click and interact with the elements to discover more about my work.
          </p>
        </div>

        <div className="grid lg:grid-cols-2 gap-12 items-center">
          {/* 3D 씬 */}
          <div className="bg-black/20 rounded-2xl p-8 backdrop-blur-sm border border-white/10">
            <ThreeDScene />
          </div>

          {/* 텍스트 내용 */}
          <div className="space-y-6">
            <div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20">
              <h3 className="text-2xl font-bold text-white mb-4">Frontend Development</h3>
              <p className="text-gray-300 leading-relaxed">
                Specialized in React, Vue.js, and modern JavaScript frameworks. 
                Creating responsive and interactive user interfaces with cutting-edge technologies.
              </p>
            </div>

            <div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20">
              <h3 className="text-2xl font-bold text-white mb-4">3D & WebGL</h3>
              <p className="text-gray-300 leading-relaxed">
                Experienced with Three.js and React Three Fiber for creating 
                immersive 3D web experiences and interactive visualizations.
              </p>
            </div>

            <div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 border border-white/20">
              <h3 className="text-2xl font-bold text-white mb-4">Full-Stack Solutions</h3>
              <p className="text-gray-300 leading-relaxed">
                Building complete web applications from database design to 
                deployment, with expertise in Node.js, Python, and cloud platforms.
              </p>
            </div>
          </div>
        </div>

        {/* 인터랙션 가이드 */}
        <div className="text-center mt-12">
          <div className="inline-flex items-center space-x-4 bg-white/10 backdrop-blur-sm rounded-full px-6 py-3 border border-white/20">
            <span className="text-white text-sm">💡</span>
            <span className="text-gray-300 text-sm">
              Click on the 3D elements to interact • Drag to rotate • Scroll to zoom
            </span>
          </div>
        </div>
      </div>
    </section>
  )
}

export default ThreeDPortfolio

 

 

 

3) App.jsx 

import Header from "./components/Header";
import Hero from "./components/Hero";
import About from "./components/About";
import Contact from "./components/Contact";
import ThreeDPortfolio from "./components/ThreeDPortfolio";

function App() {
  return (
    <>
      <Header />
      <Hero />
      <About />
      <ThreeDPortfolio />
      <Contact />
    </>
  );
}

export default App;

 

 

 

시연 영상 입니다. 

마우스의 움직임에 따라 도형들이 움직이는 것을 확인할 수 있습니다.