Next.js · GSAP · ScrollTrigger · Lenis

가로 스크롤 패널 전환


Overview

세로 스크롤을 가로 이동으로 변환하는 Horizontal Scroll 섹션을 GSAP ScrollTrigger로 구현한 프로젝트입니다. Sticky 컨테이너 안에서 트랙이 수평 이동하며, 8개의 피처 카드가 뷰포트 진입 시 smoothstep 이징으로 등장합니다.

scrollWidth에서 viewportWidth를 빼서 정확한 이동량을 계산하고, 배경 Orb 드리프트 · FloatingShapes · clip-path 프로그레스 바까지 스크롤 진행률에 연동하여 풍부한 시각적 레이어를 구성합니다.

Anatomy

UI 해부도

가로 스크롤 섹션은 Sticky 뷰포트 안에 수평 트랙이 이동하는 구조입니다. 배경 Orb · FloatingShapes · 프로그레스 바가 스크롤 진행률에 연동되어 시각적 깊이감을 만듭니다.

Sticky Container
h-svh 고정 뷰포트
HzTitle
섹션 타이틀 · 좌상단 고정
Horizontal Track
fit-content 너비 · x 트랜슬레이트
Feature Card ×8
아이콘 · 제목 · 설명 · 그라디언트 배경
Progress Bar
clip-path 기반 진행 표시
FloatingShapes
18개 부유 도형 · 스크롤 연동 이동

Structure

프로젝트 아키텍처

SectionHorizontal이 루트 컨테이너로서 스크롤 계산과 ScrollTrigger를 관리하고, 카드 · 이펙트 · 데이터가 폴더별로 분리되어 관심사가 명확합니다.

components/Horizontal
section/1
SectionHorizontal.tsx
루트 섹션 · 스크롤 계산 · ScrollTrigger
cards/2
Card.jsx
피처 카드 · 아이콘 + 그라디언트 배경
HzTitle.jsx
섹션 타이틀 · 좌상단 배치
effects/2
FloatingShapes.tsx
랜덤 부유 도형 · 시드 기반 생성
BgOrbs.jsx
프리셋 배경 그라디언트 블롭
data/1
horizontalFeatures.json
8개 피처 카드 데이터 (아이콘 · 색상)

Data Flow

컴포넌트 데이터 플로우

데이터는 단방향으로 흐릅니다. 마운트 시 스크롤 이동량을 계산하고, ScrollTrigger의 scrub이 매 프레임 진행률을 갱신하면 트랙 이동 · 카드 입장 · Orb 드리프트가 동시에 렌더됩니다.

MOUNT
SectionHorizontalsetupScrollAmount()section.height 계산
SCROLL
ScrollTrigger scrubx: -amt × progress카드 입장 계산orb 드리프트
RENDER
Card ×8FloatingShapesProgress Bar

ANIMATION OUTPUT

Track
가로 이동
x: -scrollAmount × progress
Card
입장 애니메이션
opacity + scale + y easing
ProgressBar
진행 표시
clip-path inset 0→100%
BgOrbs
배경 드리프트
x/y/scale 스크롤 연동

Timeline

애니메이션 타임라인

스크롤 진행률 0%→100% 동안 5개의 애니메이션 레이어가 동시에 동작합니다. ScrollTrigger scrub 기반이므로 시간이 아닌 스크롤 위치에 연동됩니다.

0%20%40%60%80%100%
Track X
Card Enter
Orb Drift
FloatingShapes
Progress Bar
전체 트랙 x 이동 (scrollWidth - viewportWidth)
뷰포트 진입 시 opacity·scale·y 전환 (smoothstep)
배경 Orb x/y/scale 드리프트
18개 도형 대각선 이동 + 스크롤 연동
clip-path inset(0 100%→0% 0 0)

Interaction

인터랙션 플로우

스크롤 이벤트가 발생하면 ScrollTrigger onUpdate가 진행률을 계산하고, 트랙 이동과 카드 입장이 동시에 분기됩니다. 카드는 뷰포트 교차 여부에 따라 smoothstep 입장 또는 대기 상태로 분기합니다.

SCROLL EVENT
ScrollTrigger onUpdateprogress 0→1
Track TranslationSCRUB
  1. 1

    scrollAmount 계산

    track.scrollWidth - window.innerWidth
  2. 2

    트랙 x 이동 적용

    gsap.set(track, { x: -amt * progress })
  3. 3

    Orb x/y/scale 드리프트 동시 갱신

Card EntranceVIEWPORT
  1. 1

    카드 좌측 좌표 계산

    cardLeft = card.offsetLeft + trackX
  2. 2

    뷰포트 교차 판정

    cardLeft < viewportWidth ?
  3. 3

    smoothstep 이징 적용

    eased = p * p * (3 - 2 * p)
cardLeft < viewport width?
smoothstep 입장0.9→1.0 scale · opacity 0→1 · y offset 해소
대기 상태opacity 0 · 뷰포트 밖 대기

Patterns

핵심 기술 패턴

단순히 가로로 움직이는 것이 아니라, 프로덕션에서 안정적으로 동작하는 구조로 설계했습니다. 스크롤량 계산 · smoothstep 이징 · 리사이즈 대응까지 — 실무에서 바로 적용할 수 있는 패턴입니다.

Horizontal Scroll
가로 스크롤 변환
세로 스크롤을 가로 이동으로 변환 — scrollWidth에서 viewportWidth를 빼서 정확한 이동량 계산
scrollAmount = track.scrollWidth - window.innerWidth section.style.height = scrollAmount + vh
Smoothstep Easing
커스텀 이징 함수
카드 입장에 smoothstep(p²(3-2p)) 적용하여 선형 보간보다 자연스러운 가속-감속 전환 구현
eased = p * p * (3 - 2 * p)
Sticky Container
고정 뷰포트 패턴
섹션 높이를 scrollAmount + 100vh로 설정 — sticky top:0 컨테이너가 뷰포트에 고정되며 내부만 이동
section.height = scrollAmount + innerHeight
Floating Shapes
시드 기반 도형 생성
seed 값으로 일관된 랜덤 배치 생성 — 18개 도형의 크기·위치·속도·투명도를 제어
generateShapes(18, { seed: 77 })
Resize Handling
리사이즈 대응
ScrollTrigger refreshInit 이벤트에 scrollAmount 재계산 바인딩하여 뷰포트 변경 시 자동 보정
ScrollTrigger.addEventListener('refreshInit', recalc)
Gradient Cards
그라디언트 카드 배경
각 카드에 개별 bg·shadow 값 지정 — 그라디언트 배경 + 박스 쉐도우로 시각적 깊이감 표현
bg: 'linear-gradient(...)' shadow: '0 8px 32px ...'

Responsive

반응형 전략

데스크탑 퍼스트로 설계하고 max-xl · max-md 2단계 브레이크포인트만 사용합니다. 가로 스크롤 자체는 모든 디바이스에서 동작하며, 간격 · Orb 드리프트 등 세부 조정만 변경됩니다.

Desktop1280px+
  • 가로 스크롤 풀 동작
  • 8개 카드 + gap-8 간격
  • FloatingShapes 18개 표시
  • BgOrb 드리프트 활성
  • 하단 프로그레스 바
Tabletmax-xl~1280px
  • 카드 크기 유지
  • 간격·패딩 축소
  • FloatingShapes 유지
  • 프로그레스 바 유지
  • 타이틀 위치 유지
Mobilemax-md~768px
  • 카드 gap-4로 축소
  • 트랙 좌측 패딩 축소
  • 프로그레스 바 위치 조정
  • Orb 드리프트 비활성
  • 터치 스크롤 대응