React Motion Gallery 소개
출처: Dev.to
React Motion Gallery는 복잡한 레이아웃, 모달, 포인터·터치 이벤트, 모션 및 데이터 패턴을 위한 원시 요소들의 모음입니다. 이 라이브러리는 두 가지 원칙—부드러운 애니메이션과 레이아웃 이동 제로—에 맞춰 설계되었습니다.
2022년, Next.js용 전자상거래 템플릿을 만들면서 이 프로젝트가 시작되었습니다.
대부분의 전자상거래 사이트가 UX 면에서 너무 낮은 수준에 머물러 있다는 것을 깨달았고, 개발자들이 빠르고 부드러우며 안정적이고 사용자 친화적인 사이트를 만들 수 있도록 도구를 제공하고자 했습니다.
그때 npm 라이브러리를 처음 다뤄보았지만, 하나만으로는 만족할 수 없었습니다. 언제나 뭔가 부족하거나, 기존 기능을 바꾸려면 직접 포크해서 수정해야 했고, 이는 유지보수가 어려운 상황을 만들었습니다. 이런 상황은 제 강박증을 자극했죠.
제 강박증이 심해지면서, 저는 캐러셀에 집착하게 되었습니다. 좋은 전자상거래 사이트라면 필수적인 요소이기 때문이죠. 소스 코드를 분석하고, 모든 캐러셀을 사용해 보았으며, 직접 몇 개를 처음부터 만들기도 했습니다. 하지만 바퀴를 다시 발명하고 싶지는 않았습니다. 그래서 수천 명의 개발자가 이미 검증한, 잘 알려진 라이브러리 중 하나를 선택해야 했습니다.
- 한 라이브러리는 API가 매우 풍부했지만 슬라이드 애니메이션이 끔찍하게 끊겼습니다.
- 또 다른 라이브러리는 부드러웠지만 기능이 부족했고, 부드러움이 고정된 60 fps에 한정되었습니다.
- 또 다른 라이브러리는 캐러셀 + 라이트박스 조합을 제공했지만 역시 기능이 부족하고 가장 부드러운 편은 아니었습니다.
결국 저는 알파 보간을 사용해 모든 기기와 주사율에서 일관된 속도를 보장하는 최고의 애니메이션 엔진을 가진 라이브러리를 찾았습니다. 이를 React 환경에 맞게 크게 수정한 것이 바로 현재의 React Motion Gallery의 기반이 된 캐러셀·라이트박스입니다.
템플릿을 계속 개발하면서 가장 어렵고 시간이 많이 드는 부분이 복잡한 레이아웃·애니메이션을 포함한 컴포넌트이며, 이를 서버와 클라이언트 사이에서 안정적으로 유지하는 일이라는 것을 깨달았습니다. 그래서 웹사이트 자체보다 컴포넌트에 집중하기 시작했고, 작은 프론트엔드 문제들을 해결하는 데 전념했습니다.
캐러셀 + 라이트박스 조합은 시작에 불과했습니다. 저는 빌딩 블록과 원시 요소를 생각하기 시작했고, 사용하기 쉬우면서도 풍부한 기능을 제공하고, 모듈화와 경량성을 갖춘 API를 설계했습니다. 초기에는 단일 파일 형태였지만, 수많은 리팩터링과 반복 작업을 거쳐 필요한 것만 가져오는 기능 기반 아키텍처로 진화했습니다.
- 제품 페이지 → 캐러셀 + 썸네일 + 라이트박스 컴포넌트
- 카테고리·컬렉션 페이지 → Grid 컴포넌트
- Masonry → 홈·카테고리 페이지 모두에 적용 가능한 자연스러운 확장
- 고객 리뷰 → Entries 컴포넌트 (레코드 기반, 하나의 엔트리가 여러 이미지·비디오 등을 포함하고, 슬라이더·그리드·Masonry 중 선택 가능)
하지만 컴포넌트만으로는 충분하지 않습니다. 제품 카드가 가득한 그리드가 이미지 로딩 중에 흔들리거나, 서버에서 데이터가 돌아올 때마다 레이아웃이 튀어오른다면 의미가 없죠. 여기서 나머지 라이브러리가 역할을 합니다.
레이아웃 이동 제로 → Skeleton
대부분의 전자상거래 사이트는 페이지가 실시간으로 조립되는 모습을 보여줍니다—이미지가 튀어나오고, 가격이 나타나며, 매번 몇 픽셀씩 레이아웃이 움직이죠. Skeleton은 미리 공간을 차지하게 함으로써 첫 번째 페인트부터 레이아웃을 고정합니다. 움직이는 것이 아니라 부드럽게 페이드 인될 뿐입니다.
모션 → Reveal
제품 카드가 깜빡이며 나타나는 대신, 화면에 스크롤될 때 부드럽게 등장합니다. 작은 차이지만, 페이지가 살아있게 느껴지는지 스프레드시트처럼 딱딱하게 느껴지는지를 가르는 요소입니다.
데이터 패턴
수백·수천 개의 제품을 어떻게 로드할까요? 그래서 Pagination, Infinite Scroll, Load‑more, Virtualization을 모두 기본 데이터 패턴으로 제공했습니다.
- Pagination·Load‑more → 제어와 예측 가능성
- Infinite Scroll → 끝없는 탐색 느낌
- Virtualization → 화면에 보이는 부분만 렌더링해 10 000개 제품도 10개 제품과 동일하게 부드럽게 스크롤
기본 슬라이더 데모 (전체 코드)
"use client";
import { GalleryCore } from "react-motion-gallery/core";
import { toMediaItems } from "react-motion-gallery/media";
import { Slider } from "react-motion-gallery/slider";
import { useSliderReady } from "react-motion-gallery/slider/ready";
import { useFullscreenController } from "react-motion-gallery/fullscreen";
import { SliderSkeleton } from "react-motion-gallery/skeleton/slider";
import { fullscreenSlider } from "react-motion-gallery/fullscreen/slider";
import { fullscreenZoomPan } from "react-motion-gallery/fullscreen/zoom-pan";
import { sliderFullscreen } from "react-motion-gallery/slider/fullscreen";
import { sliderArrows } from "react-motion-gallery/slider/arrows";
import { sliderDots } from "react-motion-gallery/slider/dots";
import { sliderRipple } from "react-motion-gallery/slider/ripple";
import styles from "./slider-default-demo.module.css";
const URLS = [
"https://picsum.photos/id/10/1600/900",
"https://picsum.photos/id/11/1600/900",
"https://picsum.photos/id/12/1600/900",
"https://picsum.photos/id/13/1600/900",
"https://picsum.photos/id/14/1600/900",
"https://picsum.photos/id/15/1600/900",
];
const FS_URLS = [
"https://picsum.photos/id/10/2400/1350",
"https://picsum.photos/id/11/2400/1350",
"https://picsum.photos/id/12/2400/1350",
"https://picsum.photos/id/13/2400/1350",
"https://picsum.photos/id/14/2400/1350",
"https://picsum.photos/id/15/2400/1350",
];
function Slide({ src, i }: { src: string; i: number }) {
return ;
}
function FullscreenAddon() {
const { fullscreenNode } = useFullscreenController({
plugins: [fullscreenSlider(), fullscreenZoomPan()],
fullscreen: {
enabled: true,
slider: {
gap: {
0: 40,
768: 60,
},
},
},
});
return <>{fullscreenNode};
}
export function SliderDefaultDemo() {
const media = toMediaItems(URLS);
const fullscreenMedia = toMediaItems(FS_URLS);
const { ref: sliderRef, ready: sliderReady } = useSliderReady();
return (
{media.map((item, i) => (
))}
);
}
위 코드를 위에서 아래로 읽어보면, 전체 글을 코드로 옮긴 것과 같습니다. GalleryCore는 전체 풀스크린 상태를 관리하고, SliderSkeleton은 이미지가 디코딩될 때까지 정확한 레이아웃을 유지합니다(레아웃 이동 제로). Reveal은 슬라이드가 나타날 때 순차적으로 애니메이션을 적용하고(모션), 화살표·점·리플·풀스크린·줌·팬 등 모든 추가 기능은 플러그인 형태로 선택적으로 포함됩니다. 사용하지 않는 코드는 번들에 포함되지 않죠.
마무리
이 이야기를 다 읽어주셨다면, 가장 좋은 방법은 직접 시도해 보는 것입니다. 가능하면 깨뜨려 보세요—이상한 데이터를 넣어보거나, 드래그 중에 창 크기를 바꾸고, 버튼을 연속으로 눌러보고, 느린 폰에서 스크롤해 보는 식으로요. 뭔가 어색하거나 끊긴다면 언제든 알려 주세요. 저는 이 작은 디테일들이 매우 중요하다고 생각하고, 앞으로도 계속 개선하고 확장해 나갈 계획입니다. 여러분의 피드백 하나하나가 다음 방향을 결정하는 데 큰 도움이 됩니다.