Skip to main content
민트라

스크롤 애니메이션 3D 랜딩페이지 - 첫 상업 프로젝트 회고

포트폴리오에 3D 작업물을 올렸더니 연락이 왔습니다. 핀테크 스타트업에서 신규 서비스 랜딩페이지를 의뢰했습니다. "애플 에어팟 페이지처럼 스크롤하면 제품이 움직이는 걸 원해요."

요구사항 #

클라이언트가 레퍼런스로 보낸 사이트들:

  1. Apple AirPods Pro 페이지 - 스크롤하면 에어팟이 회전하며 기능 설명
  2. Linear 웹사이트 - 부드러운 스크롤 기반 애니메이션
  3. Stripe 홈페이지 - 3D 요소와 그라데이션 배경

요구사항 정리:

기술 선택 #

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일 초과했습니다. 모바일 최적화가 예상보다 오래 걸렸습니다.

최종 성과:

배운 점 #

1. 모바일 예산을 넉넉히

"3D는 데스크톱에서만"이라는 생각은 오래전 이야기입니다. 모바일 최적화에 전체 일정의 40%를 잡으세요.

2. 레퍼런스는 목표가 아니라 방향

애플 수준의 퀄리티는 그만큼의 시간과 리소스가 필요합니다. 클라이언트와 현실적인 기대치를 조율하세요.

3. GSAP ScrollTrigger는 필수

바닐라 JS로 스크롤 애니메이션을 구현하면 크로스 브라우저 이슈로 고생합니다. ScrollTrigger가 다 해결해줍니다.

4. 피드백 사이클을 짧게

완성 후 보여주지 말고, 중간중간 공유하세요. "생각했던 것과 다른데요"를 마감 직전에 듣고 싶지 않다면요.

이 프로젝트 이후로 스크롤 기반 3D 애니메이션 의뢰가 여러 번 들어왔습니다. 첫 상업 프로젝트의 경험이 큰 자산이 되었습니다.