Next.js에서 ThreeJS로 간단한 3D 정육면체 애니메이션 만들기

이상준 (LeeSangJun)·2024년 8월 21일
0
post-thumbnail

요즘 들어 많은 해외 사이트들을 둘러보다 보니, 화면에 단순한 텍스트나 이미지가 아닌 더 인터랙티브하고 시각적3D 요소들을 많이 사용하는 것이 점점 트렌드화되고 있는 것을 느낄 수 있었습니다.

단순한 정보 전달을 넘어, 사용자와의 상호작용을 통해 더 깊은 인상을 남기고, 몰입감을 제공하는 웹사이트를 만들 수 있겠다는 생각에 웹에서 3D 그래픽을 구현해 보고자 했습니다.

처음에는 게임 엔진이나 복잡한 그래픽 도구를 사용해야 한다고 생각했지만, Three.js라는 라이브러리를 알게 되면서 비교적 간단하게 3D 콘텐츠를 웹에 통합할 수 있다는 것을 알 수 있었습니다.

이 포스팅에서는 Next.js와 Three.js를 이용해 간단한 3D 정육면체 애니메이션을 만드는 과정을 공유하려고 합니다.


1. Three.js란?

Three.js는 3D 그래픽을 웹에서 쉽게 구현할 수 있도록 도와주는 자바스크립트 라이브러리입니다.
이 라이브러리는 WebGL을 기반으로 하며, 복잡한 3D 그래픽을 비교적 간단하게 표현할 수 있게 해줍니다. Three.js를 사용하면 게임, 애니메이션, 데이터 시각화 등 다양한 3D 콘텐츠를 웹에서 구현할 수 있습니다.


2. Three.js의 기본 개념

2-1. Geometry (모양) + Material (재질) = Mesh (각각의 객체)

Three.js에서는 3D 객체를 만들 때, 먼저 객체의 모양을 정의하는 Geometry와 객체의 표면을 정의하는 Material을 결합하여 Mesh를 만듭니다. Mesh는 실제로 화면에 렌더링되는 객체를 의미합니다.

// Geometry: 정육면체 모양
const geometry = new THREE.BoxGeometry();

// Material: 각 면에 다른 색상을 적용한 투명한 재질
const materials = [
  new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.5, side: THREE.BackSide }),
  new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.5, side: THREE.BackSide }),
  new THREE.MeshBasicMaterial({ color: 0x0000ff, transparent: true, opacity: 0.5, side: THREE.BackSide }),
  new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.5, side: THREE.BackSide }),
  new THREE.MeshBasicMaterial({ color: 0xff00ff, transparent: true, opacity: 0.5, side: THREE.BackSide }),
  new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.5, side: THREE.BackSide })
];

// Geometry와 Material을 결합하여 Mesh 생성
const cube = new THREE.Mesh(geometry, materials);

2-2. Camera, Light, Scene, Renderer

  • Camera (카메라) : 장면을 어느 시점에서 볼 것인지를 결정합니다.
  • Light (조명) : 장면에 빛을 추가하여, 객체가 어떻게 보일지를 결정합니다.
  • Scene (장면) : 3D 객체들이 존재하는 공간입니다.
  • Renderer (렌더러) : Scene과 Camera 정보를 바탕으로 최종 이미지를 화면에 그려줍니다.
// Scene 생성
const scene = new THREE.Scene();

// Camera 설정
const camera = new THREE.PerspectiveCamera(75, mount.clientWidth / mount.clientHeight, 0.1, 1000);
camera.position.z = 8;

// Renderer 생성
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(mount.clientWidth, mount.clientHeight);
renderer.setClearColor(0xffffff); // 배경색 설정
mount.appendChild(renderer.domElement);

2-3. x, y, z 축

Three.js에서는 3D 공간을 x, y, z 세 개의 축으로 나눕니다. x축은 좌우, y축은 상하, z축은 전후 방향을 나타냅니다. 각 객체는 이 축을 기준으로 위치와 회전을 설정할 수 있습니다.

// 정육면체의 회전
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;

3. 마우스를 따라다니는 정육면체 만들어보기

이제 threejs의 기본개념을 알았으니 마우스 움직임에 따라 회전하는 정육면체를 만들어보겠습니다.
이 정육면체는 기본적으로 회전하고 있다 마우스가 움직일 때마다 그 방향에 맞춰 회전합니다.

3-1. Next.js에서 Three.js 라이브러리 설치 방법

Three.js를 사용하기 위해서는 먼저 프로젝트에 이 라이브러리를 설치해야 합니다. 아래의 명령어를 사용하여 npm을 통해 Three.js를 설치할 수 있습니다:

npm install three

3-2. 구현

이제 실제로 마우스 움직임에 따라 회전하는 정육면체를 구현해 보겠습니다. 먼저, 3D 정육면체를 렌더링할 컴포넌트를 만들고, 그 컴포넌트를 Next.js의 페이지에서 사용해 보겠습니다.

먼저 ThreeDCube라는 컴포넌트를 생성하여 정육면체를 구현합니다.

//ThreeDCube.tsx
'use client';

import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';

function RotatingCube() {
  const mountRef = useRef<HTMLDivElement>(null); // 렌더링할 DOM 요소를 참조하기 위한 ref를 생성
  const [isMouseActive, setIsMouseActive] = useState(false); // 마우스 움직임 여부를 추적하는 상태값 설정

  useEffect(() => {
    const mount = mountRef.current; // ref로 참조한 DOM 요소를 변수에 할당
    if (!mount) return; // DOM 요소가 존재하지 않으면 함수를 종료

    // Three.js의 Scene(장면) 생성
    const scene = new THREE.Scene();

    // Three.js의 Camera(카메라) 생성
    const camera = new THREE.PerspectiveCamera(75, mount.clientWidth / mount.clientHeight, 0.1, 1000);
    camera.position.z = 8; // 카메라를 z축으로 8만큼 이동시켜, 정육면체를 보기 위한 위치 설정

    // Three.js의 Renderer(렌더러) 생성
    const renderer = new THREE.WebGLRenderer({ antialias: true }); // 부드러운 가장자리를 위해 antialias 옵션 활성화
    renderer.setSize(mount.clientWidth, mount.clientHeight); // 렌더러의 크기를 DOM 요소의 크기에 맞게 설정
    renderer.setClearColor(0xffffff); // 배경색을 흰색으로 설정
    mount.appendChild(renderer.domElement); // 생성된 렌더러를 DOM 요소에 추가

    // 정육면체의 Geometry(형상) 및 Material(재질) 생성
    const geometry = new THREE.BoxGeometry(); // 정육면체의 형상을 정의
    const materials = [
      new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.5, side: THREE.BackSide }), // 빨간색 재질
      new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.5, side: THREE.BackSide }), // 녹색 재질
      new THREE.MeshBasicMaterial({ color: 0x0000ff, transparent: true, opacity: 0.5, side: THREE.BackSide }), // 파란색 재질
      new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.5, side: THREE.BackSide }), // 노란색 재질
      new THREE.MeshBasicMaterial({ color: 0xff00ff, transparent: true, opacity: 0.5, side: THREE.BackSide }), // 마젠타색 재질
      new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.5, side: THREE.BackSide })  // 시안색 재질
    ];
    const cube = new THREE.Mesh(geometry, materials); // 형상과 재질을 결합해 정육면체(mesh) 생성
    scene.add(cube); // 장면에 정육면체를 추가

    // 정육면체의 테두리를 추가
    const edgesGeometry = new THREE.EdgesGeometry(geometry); // 정육면체의 모서리를 정의하는 형상 생성
    const edgesMaterial = new THREE.LineBasicMaterial({ color: 0xe999999 }); // 모서리의 색상을 설정
    const edges = new THREE.LineSegments(edgesGeometry, edgesMaterial); // 모서리와 재질을 결합해 선(segment) 생성
    cube.add(edges); // 정육면체에 모서리를 추가

    let mouseX = 0; // 마우스 X축 위치값 초기화
    let mouseY = 0; // 마우스 Y축 위치값 초기화

    // 창 크기 변경 시 카메라 및 렌더러의 크기 재설정
    const onResize = () => {
      camera.aspect = mount.clientWidth / mount.clientHeight; // 카메라의 종횡비를 새 창 크기에 맞게 조정
      camera.updateProjectionMatrix(); // 카메라의 투영 매트릭스를 업데이트
      renderer.setSize(mount.clientWidth, mount.clientHeight); // 렌더러의 크기를 새 창 크기에 맞게 조정
    };
    window.addEventListener('resize', onResize); // 창 크기가 변경될 때 onResize 함수 실행

    // 마우스 움직임에 따라 정육면체 회전을 제어하는 함수
    const onMouseMove = (event: MouseEvent) => {
      const { clientX, clientY } = event; // 마우스의 현재 위치를 가져옴
      const { innerWidth, innerHeight } = window; // 창의 크기를 가져옴

      mouseX = (clientX / innerWidth) * 2 - 1; // 마우스의 X 위치를 -1 ~ 1 범위로 변환
      mouseY = -(clientY / innerHeight) * 2 + 1; // 마우스의 Y 위치를 -1 ~ 1 범위로 변환
      setIsMouseActive(true); // 마우스가 움직이는 상태로 변경
    };
    window.addEventListener('mousemove', onMouseMove); // 마우스가 움직일 때 onMouseMove 함수 실행

    // 마우스가 화면을 벗어났을 때 호출되는 함수
    const onMouseLeave = () => {
      setIsMouseActive(false); // 마우스가 움직이지 않는 상태로 변경
    };
    window.addEventListener('mouseleave', onMouseLeave); // 마우스가 화면을 벗어날 때 onMouseLeave 함수 실행

    // 애니메이션 함수
    const animate = () => {
      requestAnimationFrame(animate); // 애니메이션 프레임을 요청

      if (isMouseActive) {
        cube.rotation.x += (mouseY - cube.rotation.x) * 0.1; // 마우스의 Y축 움직임에 따라 정육면체의 X축 회전값 조정
        cube.rotation.y += (mouseX - cube.rotation.y) * 0.1; // 마우스의 X축 움직임에 따라 정육면체의 Y축 회전값 조정
      } else {
        cube.rotation.x += 0.01; // 마우스가 움직이지 않으면 X축으로 천천히 회전
        cube.rotation.y += 0.01; // 마우스가 움직이지 않으면 Y축으로 천천히 회전
      }

      renderer.render(scene, camera); // 장면을 렌더링하여 화면에 표시
    };
    animate(); // 애니메이션 함수 실행

    // 컴포넌트가 언마운트될 때 이벤트 리스너 및 DOM 요소 정리
    return () => {
      window.removeEventListener('resize', onResize); // 리스너 제거
      window.removeEventListener('mousemove', onMouseMove); // 리스너 제거
      window.removeEventListener('mouseleave', onMouseLeave); // 리스너 제거
      mount.removeChild(renderer.domElement); // DOM에서 렌더러 요소 제거
    };
  }, [isMouseActive]); // isMouseActive 상태값이 변경될 때 useEffect 재실행

  return <div ref={mountRef} style={{ width: '100vw', height: '100vh' }} />; // 렌더링할 요소를 반환
}

export default RotatingCube; // 컴포넌트를 기본으로 내보냄

이제 이 ThreeDCube 컴포넌트를 Next.js의 페이지에 추가하여 렌더링합니다. 다음과 같이 app/page.tsx 파일을 작성합니다.
//page.tsx
import ThreeDCube from '@/src/components/ThreeDCube';
![](https://velog.velcdn.com/images/vlck1111/post/088c2fe7-5e5d-4a3d-9f32-bcfaf45d9219/image.mp4)

const Home = () => {
  return (
    <div>
      <main>
        <ThreeDCube />
      </main>
    </div>
  );
};

export default Home;

위의 코드를 실행하면, 아래와 같이 화면에 마우스 움직임에 따라 회전하는 정육면체가 나타납니다.
이처럼 Three.js와 Next.js를 결합하여 웹에서 인터랙티브한 3D 요소를 쉽게 구현할 수 있습니다.

4. 마치며

이번 포스팅을 통해 Three.js를 사용해 정육면체를 만들어보는 과정은 저에게 꽤 재미있는 경험이었습니다. 3D 그래픽을 웹에 구현하는 게 이렇게 흥미롭고, 동시에 쉽게 접근할 수 있다는 걸 새삼 느꼈습니다. 앞으로는 이걸 더 발전시켜서 조명이나 텍스처 같은 요소를 추가해보거나, 더 복잡한 3D 모델을 다뤄보고 싶다는 생각이 듭니다. 이 작은 시작이 더 큰 프로젝트로 이어질 수 있을 것 같아 기대가 됩니다.

0개의 댓글