Parallax는 스크롤에 따라 배경과 콘텐츠의 이동 속도를 다르게 하여 깊이감을 주는 시각 효과입니다. React에서 성능을 고려해 구현하면 랜딩 페이지, 히어로 섹션, 갤러리 등에서 높은 몰입감을 제공할 수 있습니다.
1. 핵심 개념과 설계
기본 원리는 스크롤 위치에 비례한 오프셋을 계산해 요소의 transform 또는 backgroundPosition을 업데이트하는 것입니다. 성능을 위해 transform: translate3d와 requestAnimationFrame, passive 스크롤 리스너를 사용합니다. iOS에서 background-attachment: fixed가 제한적이므로 엘리먼트 기반 Parallax가 안전합니다.
2. CSS 배경 Parallax (간단하지만 모바일 제한)
배경 이미지를 빠르게 적용하는 방법입니다. 모바일(iOS Safari)에서 제한이 있으므로 대체 구현을 고려합니다.
import React from 'react';
export function HeroFixedBg() {
return (
<section
style={{
position: 'relative',
minHeight: '60vh',
backgroundImage: 'url(/images/hero.jpg)',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundAttachment: 'fixed', // iOS에서 불안정
}}
>
<div style={{ position: 'relative', zIndex: 1, padding: '4rem 2rem', color: '#fff' }}>
<h1>Parallax Hero</h1>
<p>스크롤에 깊이를 더합니다.</p>
</div>
</section>
);
}
3. React 훅으로 엘리먼트 Parallax 구현
요소의 getBoundingClientRect().top을 기반으로 오프셋을 계산하고 transform을 적용합니다. requestAnimationFrame으로 페인트 타이밍에 맞춰 업데이트합니다.
import React, { useEffect, useRef, useState } from 'react';
function useParallax(speed = 0.3) {
const ref = useRef(null);
const [offset, setOffset] = useState(0);
useEffect(() => {
if (typeof window === 'undefined') return;
let rafId = 0;
const update = () => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
// 화면 상단 기준 요소의 상대 위치를 속도에 맞게 변환
const next = rect.top * speed;
setOffset(next);
};
const onScroll = () => {
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(update);
};
// 초기 계산
update();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
return () => {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
cancelAnimationFrame(rafId);
};
}, [speed]);
const style = {
transform: `translate3d(0, ${offset}px, 0)`,
willChange: 'transform',
};
return { ref, style };
}
export function Parallax({ children, speed = 0.3, className, style }) {
const { ref, style: parallaxStyle } = useParallax(speed);
return (
<div ref={ref} className={className} style={{ ...style, ...parallaxStyle }}>
{children}
</div>
);
}
// 사용 예시
export function ExampleSection() {
return (
<section style={{ padding: '6rem 2rem' }}>
<Parallax speed={0.2}>
<img src="/images/mountain.png" alt="산 풍경" style={{ width: '100%', display: 'block' }} />
</Parallax>
<Parallax speed={-0.1}>
<h2 style={{ fontSize: '3rem' }}>앞쪽 텍스트가 더 빠르게 이동합니다</h2>
</Parallax>
</section>
);
}
4. 성능 최적화 체크리스트
- transform 사용: top/left 변경 대신 translate3d로 레이아웃 재계산을 피합니다. - requestAnimationFrame: 스크롤 이벤트에서 직접 setState하지 말고 rAF로 배치합니다. - passive 리스너: 스크롤 성능을 향상합니다. - will-change: transform 힌트를 제공하되 필요한 요소에만 사용합니다. - 이미지 최적화: WebP/AVIF, 적절한 크기, lazy loading(loading="lazy"). - 뷰포트 내에서만 업데이트: 화면 밖이면 계산을 건너뜁니다.
// 뷰포트 체크 추가 (useParallax 내부 개선)
const update = () => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const inView = rect.bottom >= 0 && rect.top <= window.innerHeight;
if (!inView) return; // 화면 밖이면 스킵
setOffset(rect.top * speed);
};
5. Framer Motion으로 빠르게 구현
Framer Motion의 useScroll/useTransform을 사용하면 매핑 로직을 간단히 처리할 수 있습니다.
import { motion, useScroll, useTransform } from 'framer-motion';
import React, { useRef } from 'react';
export function MotionParallax() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({ target: ref, offset: ['start end', 'end start'] });
// 스크롤 진행도(0~1)를 -50px~50px으로 매핑
const y = useTransform(scrollYProgress, [0, 1], [-50, 50]);
return (
<section ref={ref} style={{ height: '120vh', position: 'relative' }}>
<motion.img
src="/images/clouds.png"
alt="구름 레이어"
style={{ position: 'absolute', top: '20%', left: 0, right: 0, margin: '0 auto', y }}
/>
<motion.h2 style={{ y: useTransform(scrollYProgress, [0, 1], [30, -30]) }}>
자연스러운 Parallax</motion.h2>
</section>
);
}
6. 모바일/이미지 최적화
- iOS에서 background-attachment: fixed 대신 엘리먼트 Parallax를 사용합니다. - 큰 이미지 레이어는 속도를 낮추고 스크롤 구간을 제한합니다. - CSS contain: paint로 독립 페인트 영역을 만들면 교차 영향이 줄어듭니다.
// 큰 레이어에 페인트 contain 적용
const layerStyle = {
contain: 'paint',
willChange: 'transform',
};
7. 접근성과 SEO 고려
- prefers-reduced-motion 존중: 모션을 최소화합니다. - 의미 있는 콘텐츠는 Parallax에 묶지 말고, 시맨틱 구조(h1~h3, p)와 대체 텍스트를 제공합니다. - CLS 방지: 이미지에 고정 크기 또는 aspect-ratio를 지정합니다.
// 사용자의 모션 선호도에 따라 속도 조정
function useReducedMotionSpeed(base = 0.3) {
const [speed, setSpeed] = React.useState(base);
React.useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
const apply = () => setSpeed(mq.matches ? 0 : base);
apply();
mq.addEventListener('change', apply);
return () => mq.removeEventListener('change', apply);
}, [base]);
return speed;
}
8. 흔한 문제와 해결
- 요소가 튀는 현상: 레이아웃이 바뀌지 않도록 transform만 변경합니다. - Nested 스크롤 컨테이너: window가 아닌 컨테이너에 스크롤 리스너를 달고 rect/top 기준을 컨테이너로 보정합니다. - 초기 깜빡임: 마운트 시 한 번 update를 호출해 초기 오프셋을 설정합니다.
9. 재사용 가능한 Parallax 컴포넌트 패턴
속도(speed), 방향(axis), 범위(clamp)를 prop으로 받아 다양한 레이어에 재사용합니다.
import React, { useEffect, useRef, useState } from 'react';
function useParallaxAdvanced({ speed = 0.3, axis = 'y', clamp = [-80, 80] }) {
const ref = useRef(null);
const [offset, setOffset] = useState(0);
useEffect(() => {
let rafId = 0;
const update = () => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const base = axis === 'y' ? rect.top : rect.left;
const next = Math.max(clamp[0], Math.min(clamp[1], base * speed));
setOffset(next);
};
const onScroll = () => {
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(update);
};
update();
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
return () => {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
cancelAnimationFrame(rafId);
};
}, [speed, axis, clamp]);
const style =
axis === 'y'
? { transform: `translate3d(0, ${offset}px, 0)`, willChange: 'transform' }
: { transform: `translate3d(${offset}px, 0, 0)`, willChange: 'transform' };
return { ref, style };
}
export function ParallaxLayer({ children, options }) {
const { ref, style } = useParallaxAdvanced(options || {});
return (
<div ref={ref} style={style}>{children}</div>
);
}
위 패턴으로 이미지, 텍스트, 버튼 등 다양한 레이어를 쉽게 구성할 수 있습니다.
10. 마무리
React에서 스크롤 기반 Parallax를 구현할 때는 rAF, transform, passive 리스너로 성능을 챙기고, 모바일/접근성 고려를 더하면 실무에서도 안정적으로 적용할 수 있습니다. Framer Motion을 사용하면 복잡한 맵핑을 간결하게 처리할 수 있어 팀 생산성도 높아집니다.
'React' 카테고리의 다른 글
| React에서 클라이언트 사이드 데이터 암호화/복호화 처리하기 (0) | 2026.06.19 |
|---|---|
| React 앱에서 Device Orientation API 활용하기 (0) | 2026.06.18 |
| React 앱에서 OAuth 1.0 인증 처리하기 (0) | 2026.06.17 |
| React에서 이미지 압축 기능 구현하기 (0) | 2026.06.17 |
| React 앱에서 사용자 세션 타이머 및 자동 로그아웃 구현하기 (1) | 2026.06.17 |