웹 개발에서 3D 그래픽과 애니메이션을 활용하면 사용자 경험을 크게 향상시킬 수 있습니다. 이번 포스팅에서는 Three.js와 React를 결합하여 인터랙티브한 3D 마법책 애니메이션을 만드는 방법을 알아보겠습니다. 단순히 코드를 설명하는 것을 넘어, 웹 개발자로서 배울 수 있는 주요 개념과 기술적 포인트를 중점적으로 다루겠습니다.
프로젝트 개요
![마법책 애니메이션 이미지(예시)]
이 프로젝트는 다음과 같은 특징을 가진 3D 마법책을 구현합니다:
- 클릭/터치로 책이 열리고 닫히는 애니메이션
- 책이 열리면 주변에 마법 파티클이 생성
- 페이지 넘기기 기능
- 반응형 디자인(모바일 지원)
주요 기술 스택
- React: UI 컴포넌트 관리
- Three.js: 3D 그래픽 구현
- Tailwind CSS: 스타일링
- Babel: JSX 변환
프로젝트 구조와 핵심 코드 분석
1. React 컴포넌트 설정과 상태 관리
const EnhancedMagicBook = () => {
const containerRef = useRef(null);
const [isLoading, setIsLoading] = useState(true);
const [bookState, setBookState] = useState('closed');
const [currentPage, setCurrentPage] = useState(0);
const pageCount = 5; // 컴포넌트 스코프에서 정의
useEffect(() => {
// Three.js 초기화 및 애니메이션 코드
// ...
}, []);
return (
<div className="relative w-full h-screen">
<div ref={containerRef} className="absolute inset-0"></div>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50">
<p className="text-white text-2xl">Loading...</p>
</div>
)}
{bookState !== 'closed' && (
<div className="absolute bottom-10 left-1/2 transform -translate-x-1/2 flex space-x-4">
<button
onClick={() => turnPage('prev')}
className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 disabled:opacity-50"
disabled={currentPage === 0}
aria-label="Go to previous page"
>
Previous Page
</button>
<button
onClick={() => turnPage('next')}
className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 disabled:opacity-50"
disabled={currentPage === pageCount - 1}
aria-label="Go to next page"
>
Next Page
</button>
</div>
)}
</div>
);
};
개발자 팁:
useRef
를 사용해 Three.js 렌더링을 위한 DOM 요소를 참조합니다. React와 Three.js의 전형적인 통합 방식입니다.- 책의 상태(
closed
,opening
,open
,closing
,turning
)를 명확히 구분하여 애니메이션 로직을 쉽게 관리합니다. - 조건부 렌더링(
bookState !== 'closed'
)을 활용해 상황에 맞는 UI를 제공합니다. - 접근성을 위해
aria-label
속성을 추가한 것에 주목하세요.
2. Three.js 초기화 및 장면 설정
// 기본 THREE.js 설정
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1a);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 7);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
containerRef.current.appendChild(renderer.domElement);
// 조명 설정
const ambientLight = new THREE.AmbientLight(0x333366, 0.5);
scene.add(ambientLight);
const centerLight = new THREE.PointLight(0xb38bff, 1, 20);
centerLight.position.set(0, 0, 5);
centerLight.castShadow = true;
centerLight.shadow.mapSize.width = 1024;
centerLight.shadow.mapSize.height = 1024;
scene.add(centerLight);
개발자 팁:
{ antialias: true }
로 렌더러를 초기화하여 가장자리를 부드럽게 처리합니다.setPixelRatio(window.devicePixelRatio)
는 고해상도 디스플레이에서 선명한 출력을 보장합니다.- 여러 조명을 조합하여 깊이감 있는 장면을 연출합니다. 특히 컬러 조명을 활용하여 마법 책 분위기를 조성합니다.
- 그림자 매핑을 활성화하여 현실감 있는 렌더링을 구현합니다.
3. 캔버스를 활용한 동적 텍스처 생성
// 책 표지 텍스처 만들기
const createCoverTexture = () => {
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 1024;
const ctx = canvas.getContext('2d');
// 표지 배경 그라데이션
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#3a0ca3');
gradient.addColorStop(1, '#4361ee');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 가장자리 테두리
ctx.strokeStyle = '#7209b7';
ctx.lineWidth = 30;
ctx.strokeRect(40, 40, canvas.width - 80, canvas.height - 80);
// 중앙 원형 장식과 별 문양
// ...
return new THREE.CanvasTexture(canvas);
};
개발자 팁:
- HTML Canvas API와 Three.js를 결합하여 동적으로 텍스처를 생성하는 강력한 패턴입니다.
- 이 접근 방식을 사용하면 외부 이미지 파일 없이도 복잡한 텍스처를 프로그래밍 방식으로 생성할 수 있습니다.
- 그라데이션, 텍스트, 도형을 조합하여 정교한 디자인을 만들 수 있습니다.
- 텍스트 줄바꿈 함수(
wrapText
)는 Canvas에서 텍스트 작업 시 매우 유용합니다.
4. 파티클 시스템 구현
// 파티클 시스템
const particleCount = 200;
const particlesGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
const scales = new Float32Array(particleCount);
const velocities = new Float32Array(particleCount * 3);
const colorOptions = [
new THREE.Color(0xff53d0),
new THREE.Color(0x4cc9f0),
new THREE.Color(0xffbe0b),
new THREE.Color(0x4361ee)
];
// 파티클 속성 초기화
for (let i = 0; i < particleCount; i++) {
// 위치, 속도, 색상, 크기 설정
// ...
}
// 버퍼 속성 설정
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
particlesGeometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));
// 파티클 애니메이션
if (particles.visible) {
const positions = particles.geometry.attributes.position.array;
for (let i = 0; i < particleCount; i++) {
positions[i * 3] += velocities[i * 3] * delta;
positions[i * 3 + 1] += velocities[i * 3 + 1] * delta;
positions[i * 3 + 2] += velocities[i * 3 + 2] * delta;
// 파티클이 너무 멀어지면 다시 생성
const distance = Math.sqrt(
positions[i * 3] ** 2 +
positions[i * 3 + 1] ** 2 +
positions[i * 3 + 2] ** 2
);
if (distance > 10) {
// 새 위치로 재설정
// ...
}
}
particles.geometry.attributes.position.needsUpdate = true;
}
개발자 팁:
BufferGeometry
와BufferAttribute
를 사용하여 효율적인 파티클 시스템을 구현합니다.velocities
배열을 별도로 관리하여 각 파티클의 움직임을 제어합니다.needsUpdate = true
로 설정하여 변경된 위치 데이터를 GPU에 업데이트합니다.- 파티클이 일정 범위를 벗어나면 재생성하는 로직으로 시각적 연속성을 유지합니다.
5. 애니메이션 및 트랜지션 구현
// 애니메이션 상태
let animationTime = 0;
const openDuration = 3.0;
const closeDuration = 2.0;
const pageTurnDuration = 1.0;
// 애니메이션 루프
function animate() {
const delta = clock.getDelta();
animationTime += delta;
// 책 애니메이션
if (bookState === 'opening') {
const progress = Math.min(animationTime / openDuration, 1.0);
const easedProgress = 1 - Math.pow(1 - progress, 3); // cubic ease out
book.position.y = easedProgress * 0.5;
book.rotation.x = easedProgress * -0.2;
frontCover.rotation.y = easedProgress * Math.PI * 0.5;
// ...
if (progress >= 1.0) {
setBookState('open');
}
} else if (bookState === 'turning') {
const progress = Math.min(animationTime / pageTurnDuration, 1.0);
const easedProgress = 1 - Math.pow(1 - progress, 3);
pages.forEach((page, index) => {
if (index <= currentPage) {
page.rotation.y = easedProgress * Math.PI;
} else {
page.rotation.y = 0;
}
});
// ...
}
// 렌더링 & 다음 프레임 요청
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
개발자 팁:
requestAnimationFrame
루프를 사용하여 부드러운 애니메이션을 구현합니다.clock.getDelta()
로 프레임 간 시간을 측정하여 애니메이션 속도를 일정하게 유지합니다.- 진행 상태(
progress
)를 0~1 사이로 정규화하여 애니메이션을 제어합니다. - 이징 함수(
1 - Math.pow(1 - progress, 3)
)를 사용하여 부드럽고 자연스러운 움직임을 만듭니다. - 상태 기반 애니메이션 접근법은 복잡한 시각 효과를 관리하는 데 매우 효과적입니다.
6. 커스텀 애니메이션 함수 구현 (GSAP 대체)
// GSAP 비슷한 애니메이션 함수
function gsapLike(object, targetValues, duration, delay = 0) {
const startValues = {};
const changeValues = {};
const startTime = Date.now() + delay * 1000;
for (const key in targetValues) {
startValues[key] = object[key];
changeValues[key] = targetValues[key] - startValues[key];
}
function animate() {
const now = Date.now();
const elapsedTime = (now - startTime) / 1000;
if (elapsedTime < 0) {
requestAnimationFrame(animate);
return;
}
if (elapsedTime < duration) {
const progress = 1 - Math.pow(1 - elapsedTime / duration, 3);
for (const key in targetValues) {
object[key] = startValues[key] + changeValues[key] * progress;
}
requestAnimationFrame(animate);
} else {
for (const key in targetValues) {
object[key] = targetValues[key];
}
}
}
animate();
}
개발자 팁:
- GSAP(GreenSock Animation Platform)와 유사한 기능을 자체 구현한 좋은 예입니다.
- 외부 라이브러리 없이도 선언적 애니메이션 API를 만들 수 있습니다.
- 이징 함수를 통해 자연스러운 움직임을 구현합니다.
- 지연(
delay
) 파라미터를 통해 시차 애니메이션을 쉽게 만들 수 있습니다. - 객체의 여러 속성을 동시에 애니메이션화할 수 있습니다.
7. 터치 이벤트 처리 및 모바일 최적화
// 이벤트 리스너
const handleClick = () => {
if (bookState === 'closed') {
openBook();
} else if (bookState === 'open') {
closeBook();
}
};
window.addEventListener('click', handleClick);
// 터치 이벤트 추가 (패시브 모드 비활성화)
const handleTouchStart = (event) => {
event.preventDefault();
if (bookState === 'closed') {
openBook();
} else if (bookState === 'open') {
closeBook();
}
};
window.addEventListener('touchstart', handleTouchStart, { passive: false });
// 정리
return () => {
window.removeEventListener('resize', onWindowResize);
window.removeEventListener('click', handleClick);
window.removeEventListener('touchstart', handleTouchStart);
if (containerRef.current && renderer.domElement) {
containerRef.current.removeChild(renderer.domElement);
}
};
개발자 팁:
{ passive: false }
옵션은 모바일 브라우저에서 매우 중요합니다. 이를 통해 기본 터치 동작(예: 스크롤)을 방지하고 커스텀 동작을 구현할 수 있습니다.event.preventDefault()
를 사용하여 기본 브라우저 동작을 방지합니다.useEffect
클린업 함수에서 모든 이벤트 리스너를 제거하여 메모리 누수를 방지합니다.- null 체크(
containerRef.current && renderer.domElement
)로 컴포넌트 언마운트 시 발생할 수 있는 오류를 방지합니다.
8. 창 크기 변경 처리와 반응형 디자인
// 창 크기 변경 처리
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onWindowResize);
개발자 팁:
camera.aspect
조정과updateProjectionMatrix()
호출은 Three.js에서 반응형 디자인을 구현하는 핵심 단계입니다.- 창 크기가 변경될 때마다 렌더러 크기를 조정하여 항상 전체 화면을 채울 수 있습니다.
- React의 컴포넌트 라이프사이클과 Three.js의 이벤트를 적절히 연결하는 패턴을 보여줍니다.
핵심 개발 포인트 및 배울 점
- React와 Three.js 통합:
- React의 선언적 UI 패러다임과 Three.js의 명령형 그래픽 API를 효과적으로 결합하는 패턴을 배울 수 있습니다.
useRef
와useEffect
를 활용하여 DOM 조작과 3D 렌더링을 연결하는 방법을 보여줍니다.
- 동적 텍스처 생성:
- Canvas API를 활용하여 프로그래밍 방식으로 텍스처를 생성하는 강력한 기법을 익힐 수 있습니다.
- 이미지 파일 없이도 복잡한 그래픽을 생성할 수 있어 로딩 시간과 리소스 사용을 최적화할 수 있습니다.
- 상태 기반 애니메이션:
- 애니메이션을 여러 상태로 나누어 관리하는 패턴은 복잡한 인터랙션을 구현할 때 매우 유용합니다.
- 진행률(progress)과 이징 함수를 사용하여 자연스러운 움직임을 만드는 방법을 배울 수 있습니다.
- 성능 최적화:
BufferGeometry
와BufferAttribute
를 사용한 효율적인 파티클 시스템 구현.- 파티클 수 제한 및 최적화된 렌더링 기법을 통한 성능 관리.
- 접근성과 사용자 경험:
- 모바일 터치 이벤트 처리와 접근성 속성(
aria-label
) 추가. - 로딩 상태 표시 및 조건부 UI 렌더링을 통한 사용자 경험 개선.
- 모바일 터치 이벤트 처리와 접근성 속성(
- 클린 코드 관행:
- 컴포넌트 언마운트 시 메모리 누수를 방지하는 철저한 클린업.
- null 체크를 통한 안전한 DOM 조작.
- 명확한 함수 이름과 구조화된 코드 조직.
결론
이 프로젝트는 현대 웹 개발의 여러 핵심 개념을 종합적으로 보여줍니다. React의 컴포넌트 기반 아키텍처와 Three.js의 강력한 3D 렌더링 기능을 결합하여 인상적인 사용자 경험을 만들 수 있음을 보여줍니다. 특히 Canvas API를 활용한 동적 텍스처 생성, 커스텀 애니메이션 시스템, 파티클 효과는 웹 개발자가 숙달하면 다양한 프로젝트에 응용할 수 있는 가치 있는 기술입니다.
이 코드를 기반으로 더 다양한 책 디자인, 페이지 전환 효과, 인터랙션을 추가해보세요. Three.js의 다양한 머티리얼, 셰이더, 물리 효과를 실험해보며 더 놀라운 시각적 경험을 만들어보는 것도 좋은 학습 방향이 될 것입니다.
이 포스팅이 여러분의 웹 개발 여정에 도움이 되길 바랍니다. 질문이나 피드백이 있으시면 댓글로 남겨주세요!
답글 남기기