스크롤 애니메이션 3D 랜딩페이지 - 첫 상업 프로젝트 회고
포트폴리오에 3D 작업물을 올렸더니 연락이 왔습니다. 핀테크 스타트업에서 신규 서비스 랜딩페이지를 의뢰했습니다. "애플 에어팟 페이지처럼 스크롤하면 제품이 움직이는 걸 원해요."
요구사항 #
클라이언트가 레퍼런스로 보낸 사이트들:
- Apple AirPods Pro 페이지 - 스크롤하면 에어팟이 회전하며 기능 설명
- Linear 웹사이트 - 부드러운 스크롤 기반 애니메이션
- Stripe 홈페이지 - 3D 요소와 그라데이션 배경
요구사항 정리:
- 메인 히어로에 3D 신용카드 모델
- 스크롤하면 카드가 회전하며 뒤집어짐
- 각 섹션에서 카드의 다른 기능 강조
- 모바일 지원 필수
- 2주 마감
기술 선택 #
Three.js + GSAP ScrollTrigger #
스크롤 기반 애니메이션에는 GSAP ScrollTrigger가 표준입니다. Three.js와 조합하면 강력합니다.
import * as THREE from 'three';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
카드 모델 제작 #
실제 신용카드 비율(85.6mm × 53.98mm)로 만들었습니다.
const cardGeometry = new THREE.BoxGeometry(8.56, 5.4, 0.1);
// 앞면, 뒷면 텍스처
const frontTexture = textureLoader.load('/card-front.png');
const backTexture = textureLoader.load('/card-back.png');
const materials = [
new THREE.MeshStandardMaterial({ color: 0x1a1a1a }), // 오른쪽
new THREE.MeshStandardMaterial({ color: 0x1a1a1a }), // 왼쪽
new THREE.MeshStandardMaterial({ color: 0x1a1a1a }), // 위
new THREE.MeshStandardMaterial({ color: 0x1a1a1a }), // 아래
new THREE.MeshStandardMaterial({ map: frontTexture }), // 앞면
new THREE.MeshStandardMaterial({ map: backTexture }) // 뒷면
];
const card = new THREE.Mesh(cardGeometry, materials);
스크롤 애니메이션 구현 #
섹션별 애니메이션 #
// 섹션 1: 카드 등장
gsap.from(card.position, {
y: -10,
duration: 1,
scrollTrigger: {
trigger: '#section-1',
start: 'top center',
end: 'bottom center',
scrub: 1
}
});
// 섹션 2: 카드 뒤집기 (뒷면 보여주기)
gsap.to(card.rotation, {
y: Math.PI,
scrollTrigger: {
trigger: '#section-2',
start: 'top center',
end: 'bottom center',
scrub: 1
}
});
// 섹션 3: 카드 기울이기 (칩 강조)
gsap.to(card.rotation, {
x: 0.3,
z: -0.2,
scrollTrigger: {
trigger: '#section-3',
start: 'top center',
end: 'bottom center',
scrub: 1
}
});
문제 1: 애니메이션 꼬임 #
섹션을 빠르게 스크롤하면 애니메이션이 꼬였습니다. 섹션 2에서 뒤집고, 섹션 3에서 기울이는데, 빠르게 넘기면 rotation 값이 충돌했습니다.
해결책: 타임라인을 사용해서 순차적으로 관리
const tl = gsap.timeline({
scrollTrigger: {
trigger: '#scroll-container',
start: 'top top',
end: 'bottom bottom',
scrub: 1
}
});
tl.to(card.position, { y: 0, duration: 1 }, 0)
.to(card.rotation, { y: Math.PI, duration: 1 }, 1)
.to(card.rotation, { x: 0.3, z: -0.2, duration: 1 }, 2)
.to(card.position, { z: -5, duration: 1 }, 3);
문제 2: 모바일에서 스크롤 버벅임 #
터치 스크롤 시 3D 렌더링과 스크롤이 동시에 일어나면서 버벅였습니다.
해결책: 스크롤 이벤트 스로틀링 + 렌더링 최적화
let scrollPosition = 0;
let targetScrollPosition = 0;
window.addEventListener('scroll', () => {
targetScrollPosition = window.scrollY;
}, { passive: true });
function animate() {
// 부드러운 보간
scrollPosition += (targetScrollPosition - scrollPosition) * 0.1;
// 스크롤 위치에 따른 업데이트
updateCardAnimation(scrollPosition);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
시각 효과 추가 #
반사 효과 #
카드에 반사 효과를 넣었습니다.
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
const rgbeLoader = new RGBELoader();
rgbeLoader.load('/studio.hdr', (envMap) => {
envMap.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = envMap;
card.material.forEach(mat => {
mat.envMapIntensity = 0.5;
mat.metalness = 0.8;
mat.roughness = 0.2;
});
});
빛나는 효과 #
카드 테두리가 빛나는 효과를 위해 Bloom을 추가했습니다.
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
0.5, // 강도
0.4, // 반경
0.85 // 임계값
);
composer.addPass(bloomPass);
클라이언트 피드백 대응 #
"카드가 더 반짝였으면 좋겠어요" #
환경맵 강도와 metalness를 조절했습니다.
"스크롤 속도를 조절할 수 있나요?" #
scrub 값을 조절하면 됩니다.
// scrub: 1 → 1초 딜레이로 부드럽게
// scrub: 0.5 → 0.5초 딜레이
// scrub: true → 즉시 반응 (버벅일 수 있음)
"로딩이 좀 길어요" #
초기 로딩 최적화를 진행했습니다.
// 1. 텍스처 압축
// 2. 환경맵 해상도 낮춤 (1K로 충분)
// 3. 지연 로딩
// 스크롤 위치에 따라 추가 에셋 로드
ScrollTrigger.create({
trigger: '#section-3',
start: 'top bottom',
onEnter: () => loadSection3Assets()
});
납품과 결과 #
2주 일정에서 3일 초과했습니다. 모바일 최적화가 예상보다 오래 걸렸습니다.
최종 성과:
- 랜딩페이지 방문자 평균 체류 시간: 2분 45초 (기존 45초)
- 전환율: 3.2% (기존 1.8%)
- 클라이언트 피드백: "기대 이상이에요"
배운 점 #
1. 모바일 예산을 넉넉히
"3D는 데스크톱에서만"이라는 생각은 오래전 이야기입니다. 모바일 최적화에 전체 일정의 40%를 잡으세요.
2. 레퍼런스는 목표가 아니라 방향
애플 수준의 퀄리티는 그만큼의 시간과 리소스가 필요합니다. 클라이언트와 현실적인 기대치를 조율하세요.
3. GSAP ScrollTrigger는 필수
바닐라 JS로 스크롤 애니메이션을 구현하면 크로스 브라우저 이슈로 고생합니다. ScrollTrigger가 다 해결해줍니다.
4. 피드백 사이클을 짧게
완성 후 보여주지 말고, 중간중간 공유하세요. "생각했던 것과 다른데요"를 마감 직전에 듣고 싶지 않다면요.
이 프로젝트 이후로 스크롤 기반 3D 애니메이션 의뢰가 여러 번 들어왔습니다. 첫 상업 프로젝트의 경험이 큰 자산이 되었습니다.