요즘 들어 많은 해외 사이트들을 둘러보다 보니, 화면에 단순한 텍스트나 이미지가 아닌 더 인터랙티브하고 시각적인 3D 요소들을 많이 사용하는 것이 점점 트렌드화되고 있는 것을 느낄 수 있었습니다.
단순한 정보 전달을 넘어, 사용자와의 상호작용을 통해 더 깊은 인상을 남기고, 몰입감을 제공하는 웹사이트를 만들 수 있겠다는 생각에 웹에서 3D 그래픽을 구현해 보고자 했습니다.
처음에는 게임 엔진이나 복잡한 그래픽 도구를 사용해야 한다고 생각했지만, Three.js라는 라이브러리를 알게 되면서 비교적 간단하게 3D 콘텐츠를 웹에 통합할 수 있다는 것을 알 수 있었습니다.
이 포스팅에서는 Next.js와 Three.js를 이용해 간단한 3D 정육면체 애니메이션을 만드는 과정을 공유하려고 합니다.
Three.js는 3D 그래픽을 웹에서 쉽게 구현할 수 있도록 도와주는 자바스크립트 라이브러리입니다.
이 라이브러리는 WebGL을 기반으로 하며, 복잡한 3D 그래픽을 비교적 간단하게 표현할 수 있게 해줍니다. Three.js를 사용하면 게임, 애니메이션, 데이터 시각화 등 다양한 3D 콘텐츠를 웹에서 구현할 수 있습니다.
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);
// 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);
Three.js에서는 3D 공간을 x, y, z 세 개의 축으로 나눕니다. x축은 좌우, y축은 상하, z축은 전후 방향을 나타냅니다. 각 객체는 이 축을 기준으로 위치와 회전을 설정할 수 있습니다.
// 정육면체의 회전
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
이제 threejs의 기본개념을 알았으니 마우스 움직임에 따라 회전하는 정육면체를 만들어보겠습니다.
이 정육면체는 기본적으로 회전하고 있다 마우스가 움직일 때마다 그 방향에 맞춰 회전합니다.
Three.js를 사용하기 위해서는 먼저 프로젝트에 이 라이브러리를 설치해야 합니다. 아래의 명령어를 사용하여 npm을 통해 Three.js를 설치할 수 있습니다:
npm install three
이제 실제로 마우스 움직임에 따라 회전하는 정육면체를 구현해 보겠습니다. 먼저, 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; // 컴포넌트를 기본으로 내보냄
//page.tsx
import ThreeDCube from '@/src/components/ThreeDCube';

const Home = () => {
return (
<div>
<main>
<ThreeDCube />
</main>
</div>
);
};
export default Home;
위의 코드를 실행하면, 아래와 같이 화면에 마우스 움직임에 따라 회전하는 정육면체가 나타납니다.
이처럼 Three.js와 Next.js를 결합하여 웹에서 인터랙티브한 3D 요소를 쉽게 구현할 수 있습니다.
이번 포스팅을 통해 Three.js를 사용해 정육면체를 만들어보는 과정은 저에게 꽤 재미있는 경험이었습니다. 3D 그래픽을 웹에 구현하는 게 이렇게 흥미롭고, 동시에 쉽게 접근할 수 있다는 걸 새삼 느꼈습니다. 앞으로는 이걸 더 발전시켜서 조명이나 텍스처 같은 요소를 추가해보거나, 더 복잡한 3D 모델을 다뤄보고 싶다는 생각이 듭니다. 이 작은 시작이 더 큰 프로젝트로 이어질 수 있을 것 같아 기대가 됩니다.