릴스형 피드 상하 스와이프 UX 리서치 및 PoC 결과 정리
2026년 6월 10일
PLick 릴스형 피드 상하 스와이프 UX 리서치 및 PoC 결과 정리
기획심의를 통과하고, MVP 범위를 '릴스 구현' 까지 좁히면서, 각자의 포지션에서 리서치와 PoC를 진행하기로 했다.
프론트엔드가 나 혼자인 상황에서, 먼저 메인 기능은 릴스 UX 구현에 대한 리서치와 PoC를 진행했다.
1. 배경
PLick 사용자 서비스의 핵심 화면은 릴스형 피드다.
사용자는 앱에 들어오자마자 여러 프리미어리그 루머 콘텐츠를 상하로 빠르게 넘겨보고, 관심 있는 게시물에서만 상세 정보나 팬반응 패널로 진입한다.
따라서 초기 MVP에서 가장 중요한 UX는 단순한 게시물 목록이 아니라, 콘텐츠를 자연스럽게 넘기는 경험이다.
현재 피드 기본 UX의 최소 기능 범위는 다음과 같다.
상하 스크롤 → 다음 게시물 / 이전 게시물 이동 우측 스크롤 → 현재 게시물의 상세 정보 또는 팬반응 패널 진입 팬반응 버튼 클릭 → 현재 게시물의 팬반응 패널 진입
이 문서는 위 범위 중 첫 번째 주제인 릴스형 피드 상하 스와이프 UX 구현 방식을 비교하고, 실제 PoC 결과를 바탕으로 PLick에 적합한 방향을 정리한 글이다.
2. 릴스형 피드 UX에서 중요한 조건
릴스형 피드는 단순히 리스트를 세로로 나열하는 UI가 아니다.
다음 조건을 만족해야 한다.
1. 한 화면에 하나의 게시물이 중심적으로 보여야 한다. 2. 사용자가 위아래로 스와이프하면 다음/이전 게시물로 이동해야 한다. 3. 이동 후에는 게시물이 화면에 정확히 맞춰져야 한다. 4. 모바일 브라우저에서도 끊김 없이 부드럽게 동작해야 한다. 5. 이후 우측 패널 진입 UX와 충돌하지 않아야 한다.
즉, 구현 방식은 단순 개발 편의성만으로 결정할 수 없다.
다음 요소들을 함께 고려해야 한다.
- 모바일 터치 UX
- 제스처 충돌 가능성
- 렌더링 성능
- 현재 게시물 인덱스 제어
- 이미지 preload / 데이터 prefetch
- 유지보수성
- 접근성
3. 고려한 구현 옵션
릴스형 상하 스와이프 UX 구현을 위해 다음 네 가지 방식을 검토했다.
1. CSS Scroll Snap 2. Swiper.js 3. Embla Carousel 4. Motion 기반 커스텀 제스처
각 방식의 핵심 특성은 다음과 같다.
| 옵션 | 구현 방식 | 핵심 아이디어 | 라이브러리 의존성 | 제스처 제어 수준 | 구현 난이도 | MVP 적합도 |
|---|---|---|---|---|---|---|
| CSS Scroll Snap | CSS 기반 네이티브 스크롤 | 브라우저 기본 스크롤에 snap point 설정 | 없음 | 낮음 | 낮음 | 높음 |
| Swiper.js | 완성형 슬라이더 라이브러리 | 슬라이드 단위로 수직 이동 제어 | 있음 | 높음 | 중간 | 중간 |
| Embla Carousel | 가벼운 캐러셀 엔진 | 캐러셀 동작을 직접 조합 | 있음 | 중간~높음 | 중간~높음 | 중간 |
| Motion custom gesture | 애니메이션·드래그 직접 제어 | drag offset / velocity 기반 화면 전환 | 있음 | 매우 높음 | 높음 | 낮음~중간 |
4. 비교 기준
PLick의 MVP 범위를 기준으로 다음 항목을 중심으로 비교했다.
| 비교 기준 | 설명 |
|---|---|
| 모바일 터치 UX | iOS Safari, Android Chrome에서 자연스럽게 스와이프되는가 |
| 제스처 충돌 가능성 | 추후 우측 패널 진입 UX와 충돌하지 않는가 |
| 성능 | 게시물이 늘어나도 스크롤이 끊기지 않는가 |
| 커스터마이징 가능성 | 애니메이션, 이동 조건, 인덱스 제어가 가능한가 |
| 접근성 | 키보드 이동, 브라우저 기본 스크롤, 스크린리더 대응이 가능한가 |
| 유지보수성 | 팀원이 이해하고 수정하기 쉬운가 |
5. 옵션별 리서치
5.1 CSS Scroll Snap
개요
CSS Scroll Snap은 브라우저의 기본 스크롤 동작을 사용하면서, 특정 위치에 스크롤이 자동으로 맞춰지도록 하는 CSS 기능이다.
릴스형 피드에서는 전체 피드 컨테이너에 세로 스크롤을 만들고, 각 게시물 아이템을 화면 높이만큼 배치한 뒤, 각 아이템을 snap point로 지정한다.
예상 구현 구조는 다음과 같다.
export default function FeedPage() { return ( <main className="feed"> {posts.map((post) => ( <section key={post.id} className="feedItem"> <FeedCard post={post} /> </section> ))} </main> ); }
.feed { height: 100dvh; overflow-y: scroll; scroll-snap-type: y mandatory; } .feedItem { height: 100dvh; scroll-snap-align: start; }
장점
CSS Scroll Snap의 가장 큰 장점은 구현이 단순하다는 점이다.
별도 라이브러리 없이 CSS만으로 기본적인 릴스형 상하 스크롤을 구현할 수 있다.
스크롤 컨테이너 + 화면 높이 아이템 + scroll-snap-type + scroll-snap-align
브라우저 기본 스크롤을 사용하기 때문에 터치, 마우스 휠, 트랙패드 스크롤이 비교적 자연스럽게 처리된다.
또한 외부 라이브러리를 추가하지 않기 때문에 JS 번들 크기가 증가하지 않는다. 이미지와 콘텐츠가 많은 피드에서는 UX 구현을 위한 추가 JS 의존성을 줄이는 것도 장점이다.
접근성 측면에서도 상대적으로 유리하다. 기본 스크롤을 사용하기 때문에 키보드, 터치, 보조 기술과의 충돌이 적다.
단점
반대로 세밀한 전환 제어는 어렵다.
예를 들어 다음과 같은 제어는 CSS Scroll Snap만으로 구현하기 어렵다.
스와이프 거리 기준으로 강제 이동 스와이프 속도 기준으로 다음 게시물 전환 전환 중 애니메이션 커스터마이징 중간 드래그 상태에 따른 UI 변화
또한 유튜브 쇼츠나 인스타그램 릴스처럼 한 장씩 강하게 넘어가는 앱형 전환감을 만들기에는 브라우저 기본 스크롤의 느낌이 남을 수 있다.
추후 댓글, 팬반응, 상세 정보처럼 게시물 내부에 자체 스크롤 영역이 들어가면 부모 피드 스크롤과 자식 영역 스크롤이 충돌할 가능성도 있다.
판단
CSS Scroll Snap은 초기 레이아웃 검증이나 빠른 데모에는 적합하다.
하지만 PLick의 메인 피드 UX가 앱형 전환감을 목표로 한다면 최종 구현 방식으로는 제어권이 부족할 가능성이 높다.
5.2 Swiper.js
개요
Swiper.js는 모바일 터치 슬라이더 구현에 널리 사용되는 라이브러리다.
수평 슬라이더뿐 아니라 수직 방향 슬라이더도 지원한다.
릴스형 피드에서는 direction="vertical"을 설정해 각 게시물을 하나의 slide로 구성할 수 있다.
import { Swiper, SwiperSlide } from "swiper/react"; import "swiper/css"; export default function FeedPage() { return ( <Swiper direction="vertical" className="feedSwiper"> {posts.map((post) => ( <SwiperSlide key={post.id}> <FeedCard post={post} /> </SwiperSlide> ))} </Swiper> ); }
.feedSwiper { height: 100dvh; }
장점
Swiper는 모바일 슬라이드 UX에 필요한 많은 기능을 이미 제공한다.
스와이프 거리, 속도, momentum, 슬라이드 전환 등을 직접 구현하지 않아도 비교적 완성도 있는 슬라이드 UX를 빠르게 만들 수 있다.
현재 인덱스 관리도 쉽다.
<Swiper direction="vertical" onSlideChange={(swiper) => { setCurrentIndex(swiper.activeIndex); }} > ... </Swiper>
이후 다음 피드 prefetch, 현재 게시물 조회 로그, analytics 이벤트 수집과 연결하기 좋다.
단점
Swiper의 가장 큰 단점은 라이브러리 구조에 강하게 의존한다는 점이다.
릴스형 피드의 핵심 UX를 외부 라이브러리 구조에 맞춰 구현해야 하고, 복잡한 커스터마이징이 필요해지면 오히려 코드가 어려워질 수 있다.
또한 PLick은 세로 피드 이동뿐 아니라 우측 스와이프를 통한 상세/팬반응 패널 진입을 고려하고 있다.
이 경우 사용자가 대각선으로 스와이프했을 때 다음 중 어떤 동작으로 해석할지 문제가 생길 수 있다.
세로 이동? 가로 패널 열기? 브라우저 뒤로가기 제스처?
Swiper는 자체 wrapper와 slide 구조를 사용하므로, 피드 아이템 최적화나 복잡한 패널 구조를 붙일 때 구조적 제약이 생길 수 있다.
판단
Swiper는 빠르게 비교용 PoC를 만들기에는 적합하다.
하지만 PLick의 메인 피드처럼 장기적으로 제스처 정책과 렌더링 구조를 직접 소유해야 하는 경우에는 최종 구현으로 신중하게 검토해야 한다.
5.3 Embla Carousel
개요
Embla Carousel은 가볍고 유연한 캐러셀 엔진이다.
Swiper처럼 완성형 UI를 제공하기보다는, 캐러셀 동작을 개발자가 직접 조합할 수 있도록 도와주는 성격이 강하다.
React wrapper를 제공하며, 커스텀 UI와 결합하기 좋다.
import useEmblaCarousel from "embla-carousel-react"; export default function FeedPage() { const [emblaRef] = useEmblaCarousel({ axis: "y", }); return ( <div className="embla" ref={emblaRef}> <div className="emblaContainer"> {posts.map((post) => ( <section className="emblaSlide" key={post.id}> <FeedCard post={post} /> </section> ))} </div> </div> ); }
.embla { overflow: hidden; height: 100dvh; } .emblaContainer { height: 100%; display: flex; flex-direction: column; } .emblaSlide { flex: 0 0 100%; min-height: 0; }
장점
Embla는 Swiper보다 구조 제약이 적고, 커스터마이징 자유도가 높다.
완성형 UI 라이브러리라기보다는 캐러셀 엔진에 가깝기 때문에 DOM 구조와 스타일을 서비스에 맞게 구성하기 좋다.
현재 게시물 인덱스, 스크롤 상태, 이벤트 구독을 React 상태와 연결하기도 쉽다.
PLick처럼 피드 전환, prefetch, 현재 인덱스 제어, 우측 패널 진입을 직접 조합해야 하는 구조에서는 Swiper보다 유연한 대안이 될 수 있다.
단점
Embla는 Swiper보다 직접 구현해야 할 부분이 많다.
예를 들어 다음 기능들은 개발자가 별도로 붙여야 한다.
현재 인덱스 표시 다음/이전 버튼 스냅 상태 관리 일부 제스처 제어
또한 CSS Scroll Snap으로도 충분히 검증 가능한 단계에서 Embla를 먼저 도입하면 초기 구현 복잡도가 올라갈 수 있다.
수직 릴스형 피드와 우측 패널 진입이 함께 있는 구조에서는 실제 모바일 터치 충돌을 반드시 PoC로 검증해야 한다.
판단
Embla는 Motion 기반 직접 구현이 너무 복잡하거나, 안정적인 스냅 동작을 빠르게 확보해야 할 때 좋은 중간 대안이다.
다만 대량 게시물 조건에서는 별도의 windowing 또는 virtualization 전략이 필요하다.
5.4 Motion 기반 커스텀 제스처
개요
Motion 기반 커스텀 제스처 구현은 사용자의 drag 방향, 거리, 속도를 직접 계산해서 현재 게시물 index를 변경하는 방식이다.
브라우저 기본 스크롤이나 carousel 라이브러리에 맡기지 않고, 피드 전환 정책을 직접 설계한다.
예상 구현 구조는 다음과 같다.
import { motion, AnimatePresence } from "motion/react"; import { useState } from "react"; export default function FeedPage() { const [currentIndex, setCurrentIndex] = useState(0); const currentPost = posts[currentIndex]; return ( <div className="feed"> <AnimatePresence mode="wait"> <motion.section key={currentPost.id} className="feedItem" initial={{ y: "100%" }} animate={{ y: 0 }} exit={{ y: "-100%" }} drag="y" onDragEnd={(_, info) => { if (info.offset.y < -80) { setCurrentIndex((prev) => Math.min(prev + 1, posts.length - 1)); } if (info.offset.y > 80) { setCurrentIndex((prev) => Math.max(prev - 1, 0)); } }} > <FeedCard post={currentPost} /> </motion.section> </AnimatePresence> </div> ); }
장점
Motion 기반 구현의 가장 큰 장점은 제스처와 애니메이션을 가장 세밀하게 제어할 수 있다는 점이다.
다음 항목들을 직접 설계할 수 있다.
스와이프 거리 기준 스와이프 속도 기준 다음/이전 게시물 전환 조건 전환 애니메이션 시간 전환 중 easing 대각선 제스처 판별 세로 스와이프와 가로 패널 제스처 분리 현재 게시물, 이전 게시물, 다음 게시물 중심 렌더링
즉, 단순히 “스크롤되는 피드”가 아니라 “한 장씩 넘기는 앱형 피드”를 만들 수 있다.
또한 currentIndex를 React state로 직접 관리하기 때문에 다음 기능들과 연결하기 쉽다.
다음 게시물 preload 상세 정보 prefetch 팬반응 패널 prefetch analytics 이벤트 기록 현재 게시물 기준 URL sync 우측 패널 open / close 제어
단점
반대로 구현 책임은 가장 크다.
기본 스크롤을 버리고 직접 제스처를 처리하는 순간 고려할 것이 크게 늘어난다.
터치 이벤트 마우스 이벤트 트랙패드 스크롤 키보드 접근성 브라우저 뒤로가기 제스처 모바일 Safari 이슈 스크롤 복원 overscroll 처리
또한 세로 드래그와 가로 드래그를 모두 직접 판별해야 하므로, 대각선 스와이프나 짧은 터치, 긴 드래그 등 사용자 입력을 세밀하게 분기해야 한다.
판단
PLick의 릴스형 피드는 서비스의 메인 UX다.
따라서 구현 난이도보다 사용자 경험 완성도를 우선한다면, Motion 기반 커스텀 제스처 구현이 가장 적합한 후보가 될 수 있다.
6. 주요 고려 사항
6.1 세로 스와이프와 가로 패널 진입의 충돌
PLick 피드 UX는 단순 세로 피드가 아니다.
상하 스와이프 → 게시물 이동 우측 스와이프 → 상세 정보 또는 팬반응 패널 진입
따라서 세로 스와이프 구현 방식은 이후 가로 패널 UX와 충돌하지 않아야 한다.
특히 다음 상황을 고려해야 한다.
사용자가 대각선으로 스와이프하는 경우 모바일 브라우저의 좌우 뒤로가기 제스처와 겹치는 경우 게시물 내부 버튼 클릭이 스와이프로 오인되는 경우 팬반응 패널 내부 스크롤이 피드 이동으로 오인되는 경우
Motion 기반 구현에서는 다음과 같은 기준을 둘 수 있다.
세로 이동 조건: abs(offsetY) > abs(offsetX) AND offsetY가 일정 거리 이상 AND velocityY가 일정 기준 이상 가로 패널 진입 조건: abs(offsetX) > abs(offsetY) AND offsetX가 일정 거리 이상 AND 오른쪽 방향 스와이프
이처럼 입력을 직접 해석하면, 대각선 스와이프나 애매한 제스처에서 의도치 않은 전환을 줄일 수 있다.
6.2 현재 게시물 인덱스 추적
릴스형 피드에서는 현재 보고 있는 게시물의 index 또는 postId를 알아야 한다.
이 정보는 다음 기능에 필요하다.
현재 게시물 기준 팬반응 패널 열기 다음 게시물 prefetch 조회 이벤트 기록 공유 링크 처리 현재 게시물 URL 반영
CSS Scroll Snap을 사용할 경우 IntersectionObserver를 사용해서 현재 보이는 게시물을 추적할 수 있다.
const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const postId = entry.target.getAttribute("data-post-id"); setCurrentPostId(postId); } }); }, { threshold: 0.7, } );
Swiper나 Embla를 사용할 경우 라이브러리의 slide change 이벤트를 통해 현재 index를 가져올 수 있다.
Motion 기반 구현에서는 currentIndex를 React state로 직접 관리할 수 있다.
6.3 모바일 viewport height 문제
릴스형 피드는 한 게시물이 화면 전체 높이를 차지해야 한다.
하지만 모바일 브라우저에서는 주소창이 접히고 펼쳐지면서 100vh가 예상과 다르게 동작할 수 있다.
따라서 PoC에서는 다음 단위를 비교해야 한다.
height: 100vh; height: 100dvh; height: 100svh;
MVP에서는 최신 모바일 브라우저 기준으로 100dvh를 우선 검토하되, iOS Safari에서 실제 동작을 반드시 확인해야 한다.
6.4 렌더링 성능
게시물 수가 많아질 경우 모든 게시물을 한 번에 렌더링하면 성능 문제가 발생할 수 있다.
초기 PoC에서는 다음 조건을 테스트했다.
게시물 20개 렌더링 게시물 100개 렌더링 게시물 300개 렌더링 이미지 포함 여부 피드 카드 개수 long task 발생 여부
릴스형 피드는 사용자가 실제로 보는 게시물이 한 번에 하나이므로, 장기적으로는 현재 게시물 주변만 무겁게 렌더링하는 전략이 필요하다.
이전 게시물 현재 게시물 다음 게시물
6.5 이미지 로딩 전략
피드 콘텐츠는 이미지 중심일 가능성이 높다.
다음 게시물로 이동했을 때 이미지가 늦게 뜨면 UX가 크게 나빠진다.
따라서 다음 전략이 필요하다.
현재 게시물 이미지 → 즉시 로드 다음 게시물 이미지 → 미리 로드 나머지 게시물 이미지 → lazy loading
7. 1차 리서치 기준 우선순위
PLick의 릴스형 피드는 단순한 목록 UI가 아니라 사용자 서비스의 핵심 경험이다.
사용자는 첫 화면에서 루머 콘텐츠를 빠르게 넘겨보고, 관심 있는 게시물에서만 상세 정보나 팬반응 패널로 진입한다.
따라서 릴스형 피드는 단순히 “구현 가능한 수준”이 아니라, 유튜브 쇼츠나 인스타그램 릴스처럼 앱에 가까운 전환감과 몰입감을 제공해야 한다.
리서치 단계에서는 다음 기준을 우선순위로 두었다.
1. 앱 같은 스와이프 경험 2. 한 장씩 정확히 전환되는 피드 구조 3. 세로 스와이프와 우측 패널 제스처의 명확한 분리 4. 현재 게시물 index/postId의 안정적인 제어 5. 다음 게시물 preload와 현재 게시물 중심 렌더링 최적화 6. 장기적으로 유지 가능한 구조
이를 기준으로 초기 우선순위는 다음과 같이 정리했다.
| 순위 | 옵션 | 판단 |
|---|---|---|
| 1순위 | Motion 기반 커스텀 제스처 | 가장 앱 같은 경험을 만들 수 있음. PLick의 메인 피드 UX에 가장 적합 |
| 2순위 | Embla Carousel | 직접 구현 복잡도가 너무 높을 때 사용할 수 있는 커스터마이징 가능한 대안 |
| 3순위 | Swiper.js | 빠른 비교 PoC에는 적합하지만 장기 커스터마이징 제약 가능성 있음 |
| 4순위 | CSS Scroll Snap | 초기 레이아웃 검증/fallback용. 최종 메인 UX로는 부족할 가능성 높음 |
8. PoC 테스트 개요
리서치 이후 실제 PoC를 통해 네 가지 구현 방식을 같은 조건에서 비교했다.
테스트 대상
| 구현 방식 | URL | 핵심 아이디어 |
|---|---|---|
| Motion custom gesture | /motion | MotionValue 기반 track 이동, drag end snap, current index 주변 windowing |
| Embla Carousel | /embla | Embla Carousel axis: "y" 기반 vertical carousel |
| Swiper | /swiper | Swiper direction="vertical" 기반 slider |
| CSS Scroll Snap | /scroll-snap | browser native scroll + CSS scroll snap + IntersectionObserver |
테스트 조합
각 구현에 대해 동일한 mock post data와 동일한 feed card UI를 사용했다.
구현 방식: 4개 게시물 개수: 20, 100, 300 총 조합: 12개 각 조합당 다음 게시물 이동: 10회
입력 방식은 Playwright의 mouse wheel 입력을 사용했다.
다만 실제 서비스의 핵심 입력은 모바일 touch swipe이기 때문에, 모바일 touch swipe 자체는 이번 벤치마크에 포함하지 않았다.
테스트 환경
| 항목 | 값 |
|---|---|
| App | apps/reels-feed-swipe-motion |
| Base URL | http://localhost:3000 |
| Viewport | 390 x 844 |
| Step count | 10 |
| Inter-step delay | 560ms |
| Benchmark script | apps/reels-feed-swipe-motion/scripts/benchmark-feed.mjs |
| Raw result JSON | features/01-reels-feed-swipe/results/2026-06-18T02-59-41-720Z-benchmark.json |
| Raw result Markdown | features/01-reels-feed-swipe/results/2026-06-18T02-59-41-720Z-benchmark.md |
9. 측정 지표
9.1 First Card
페이지 진입 후 첫 번째 feed card heading이 visible 상태가 되기까지의 시간이다.
이 값은 라우트 방문 순서, dev server cache, browser warm-up의 영향을 받을 수 있으므로 단독 의사결정 지표로 보기는 어렵다.
9.2 DOM Node Count
현재 페이지에 렌더링된 전체 DOM element 수다.
릴스형 피드는 한 번에 하나의 게시물만 중심적으로 보이는 구조이므로, 게시물 수가 늘어나도 DOM 규모가 선형 증가하지 않는 것이 이상적이다.
9.3 Image Count
현재 DOM에 올라온 img 개수다.
이미지 중심 피드에서는 이 값이 성능과 메모리 압박에 직접적인 영향을 준다.
9.4 Card Count
현재 DOM에 올라온 feed card article 개수다.
Motion 구현은 current index 기준 주변 카드만 렌더링하도록 windowing을 적용했다.
9.5 Avg Step
wheel 입력 후 current index가 다음 게시물로 변경되기까지 걸린 평균 시간이다.
각 구현마다 내부 throttle, animation, scroll snap 정책이 다르기 때문에 이 값은 UX 감각의 일부만 설명한다.
9.6 Success
요청한 10회 이동 중 실제 current index가 다음 index로 변경된 횟수다.
9.7 Long Tasks
PerformanceObserver의 longtask entry를 기준으로 측정했다.
long task는 main thread를 오래 점유하는 작업이다. 모바일 기기에서는 프레임 드랍이나 입력 지연으로 이어질 가능성이 있다.
9.8 Heap Delta
이 테스트 환경에서는 usedJSHeapSize가 모든 조합에서 0.0 MB 변화로 측정되었다.
Playwright Chromium 환경의 메모리 값이 충분히 세밀하지 않게 보고된 것으로 보이며, 이번 의사결정에서는 heap보다 DOM, image, card count와 long task를 더 신뢰했다.
10. 벤치마크 결과
| Implementation | Count | First Card | Initial DOM | Initial Images | Initial Cards | After DOM | After Images | After Cards | Avg Step | Success | Long Tasks |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Motion | 20 | 177ms | 123 | 3 | 3 | 161 | 5 | 5 | 40ms | 10/10 | 0 / 0ms |
| Motion | 100 | 52ms | 123 | 3 | 3 | 161 | 5 | 5 | 30ms | 10/10 | 0 / 0ms |
| Motion | 300 | 35ms | 123 | 3 | 3 | 161 | 5 | 5 | 30ms | 10/10 | 0 / 0ms |
| Embla | 20 | 61ms | 446 | 20 | 20 | 446 | 20 | 20 | 31ms | 10/10 | 0 / 0ms |
| Embla | 100 | 57ms | 1966 | 100 | 100 | 1966 | 100 | 100 | 66ms | 10/10 | 0 / 0ms |
| Embla | 300 | 59ms | 5766 | 300 | 300 | 5766 | 300 | 300 | 84ms | 10/10 | 5 / 381ms |
| Swiper | 20 | 87ms | 447 | 20 | 20 | 447 | 20 | 20 | 37ms | 10/10 | 0 / 0ms |
| Swiper | 100 | 63ms | 1967 | 100 | 100 | 1967 | 100 | 100 | 64ms | 10/10 | 0 / 0ms |
| Swiper | 300 | 56ms | 5767 | 300 | 300 | 5767 | 300 | 300 | 103ms | 10/10 | 5 / 402ms |
| CSS Scroll Snap | 20 | 82ms | 445 | 20 | 20 | 445 | 20 | 20 | 31ms | 10/10 | 0 / 0ms |
| CSS Scroll Snap | 100 | 69ms | 1965 | 100 | 100 | 1965 | 100 | 100 | 56ms | 10/10 | 0 / 0ms |
| CSS Scroll Snap | 300 | 66ms | 5765 | 300 | 300 | 5765 | 300 | 300 | 77ms | 10/10 | 5 / 386ms |
11. 핵심 결론
이번 정량 테스트의 핵심 결론은 다음과 같다.
1. Motion windowing 구현은 300개 게시물 조건에서도 DOM, image, card 수를 거의 일정하게 유지했다. 2. Embla, Swiper, CSS Scroll Snap은 현재 구현 기준으로 전체 게시물을 DOM에 렌더링한다. 3. 300개 조건에서 Embla, Swiper, CSS Scroll Snap은 long task가 발생했지만, Motion은 전환 중 long task가 발생하지 않았다. 4. 모든 구현은 10회 휠 기반 다음 이동에서 10/10 성공했다. 5. 정량 성능만 보면 Motion custom gesture가 가장 유리하지만, 최종 선택 전 모바일 실기기 UX 검증이 필요하다.
12. 결과 분석
12.1 Motion은 게시물 수가 늘어나도 DOM 규모를 안정적으로 유지했다
Motion은 20개, 100개, 300개 조건 모두에서 초기 DOM이 123개로 동일했다.
10회 이동 후에도 DOM은 161개, image는 5개, card는 5개로 제한되었다.
Motion 300 posts Initial: DOM 123 Images 3 Cards 3 After 10 steps: DOM 161 Images 5 Cards 5
이 결과가 나온 이유는 Motion 구현에 current index 주변 windowing이 적용되어 있기 때문이다.
릴스형 피드는 한 번에 모든 게시물을 보여줄 필요가 없다.
현재 보고 있는 게시물, 이전 게시물, 다음 게시물 정도만 안정적으로 렌더링하면 된다.
따라서 게시물 수가 20개에서 300개로 늘어나도 실제 DOM에 올라오는 카드 수가 거의 일정하게 유지되는 Motion 구조는 릴스형 피드에 유리하다.
12.2 Embla, Swiper, CSS Scroll Snap은 현재 구현 기준으로 전체 게시물을 렌더링한다
Embla, Swiper, CSS Scroll Snap은 현재 구현 기준으로 전체 게시물을 DOM에 올린다.
300개 조건에서는 세 방식 모두 DOM이 약 5,700개 수준으로 증가했다.
Embla 300 posts: DOM 5766 Images 300 Cards 300 Swiper 300 posts: DOM 5767 Images 300 Cards 300 CSS Scroll Snap 300 posts: DOM 5765 Images 300 Cards 300
이 구조는 구현이 단순하다는 장점이 있다.
carousel 라이브러리나 브라우저 native scroll 동작을 그대로 활용할 수 있기 때문이다.
하지만 게시물 수가 많아지거나 카드 내부 UI가 더 복잡해질수록 비용이 빠르게 커질 가능성이 높다.
PLick의 피드 카드는 단순 이미지 카드에서 끝나지 않는다.
향후 다음 요소들이 추가될 가능성이 높다.
팀/선수 태그 기자 출처 신뢰도 루머 진행 단계 댓글 수 투표 상태 팬반응 미리보기 상세 패널 진입 버튼
즉, 카드 하나의 DOM 비용이 더 커질 수 있다.
이 상황에서 전체 게시물을 한 번에 렌더링하는 구조는 장기적으로 부담이 될 수 있다.
12.3 Long task는 300개 full-render 구현에서만 발생했다
300개 조건에서 Embla, Swiper, CSS Scroll Snap은 모두 long task가 5개 발생했다.
| Implementation | Count | Long Tasks |
|---|---|---|
| Motion | 300 | 0 / 0ms |
| Embla | 300 | 5 / 381ms |
| Swiper | 300 | 5 / 402ms |
| CSS Scroll Snap | 300 | 5 / 386ms |
long task는 main thread를 오래 점유하는 작업이다.
즉, 브라우저가 사용자의 입력에 바로 반응하지 못하거나, 프레임이 끊기는 원인이 될 수 있다.
특히 모바일 브라우저는 데스크톱 headless Chromium보다 CPU/GPU 여유가 작다.
따라서 데스크톱 테스트에서 long task가 보이는 구조는 실제 모바일 기기에서 체감 버벅임으로 이어질 위험이 있다.
이번 결과에서는 전체 게시물을 DOM에 올린 구현들에서만 long task가 발생했고, windowing을 적용한 Motion에서는 발생하지 않았다.
12.4 모든 구현은 10회 이동에 성공했다
모든 구현은 각 조합에서 10회 이동을 모두 성공했다.
Motion: 10/10 Embla: 10/10 Swiper: 10/10 CSS Scroll Snap: 10/10
즉, 이번 테스트 기준으로는 “다음 게시물로 이동할 수 있는가” 자체는 네 방식 모두 충족했다.
차이는 이동 가능 여부가 아니라 다음 요소에서 발생한다.
렌더링 규모 이미지 개수 카드 개수 long task 발생 여부 제스처 커스터마이징 가능성 모바일 swipe 감각 가로 패널 진입과의 충돌 가능성
따라서 단순히 이동 성공률만 보면 네 방식 모두 충분해 보일 수 있다.
하지만 서비스의 핵심 피드 구현을 선택하기에는 DOM 규모와 long task 차이가 더 중요하다.
12.5 Avg Step만으로 승자를 결정하면 안 된다
Avg Step은 구현별로 다음과 같이 측정되었다.
| Implementation | 20 posts | 100 posts | 300 posts |
|---|---|---|---|
| Motion | 40ms | 30ms | 30ms |
| Embla | 31ms | 66ms | 84ms |
| Swiper | 37ms | 64ms | 103ms |
| CSS Scroll Snap | 31ms | 56ms | 77ms |
Motion은 300개에서도 step time이 안정적이다.
다만 step time은 wheel event가 current index 변경으로 반영되는 시간만 측정한다.
실제 UX 감각은 다음 요소들의 영향을 함께 받는다.
animation duration easing touch drag follow overscroll 모바일 브라우저 주소창 접힘/펼침 손가락을 따라오는 느낌 빠른 연속 swipe에서의 안정성
따라서 step time은 보조 지표로 보는 것이 적절하다.
이번 PoC에서는 DOM 규모와 long task를 더 강한 성능 지표로 해석했다.
13. 구현별 해석
13.1 Motion custom gesture
장점
Motion 구현의 가장 큰 장점은 게시물 수와 무관하게 DOM, image, card 수가 낮게 유지된다는 점이다.
300개 조건에서도 long task가 발생하지 않았다.
또한 current index를 직접 관리하므로 다음 기능들을 연결하기 쉽다.
다음 게시물 preload 상세 정보 prefetch 팬반응 패널 prefetch analytics 이벤트 기록 현재 게시물 기준 URL sync 우측 패널 open / close 제어
세로 전환과 가로 패널 진입을 직접 분기할 수 있다는 점도 PLick에게 중요하다.
PLick은 단순히 위아래로 넘기는 피드가 아니라, 현재 게시물에서 오른쪽으로 밀어 팬반응이나 상세 패널로 진입하는 구조를 고려하고 있다.
이 경우 세로 drag와 가로 drag의 의도를 구분해야 한다.
Motion 기반 custom gesture는 이 분기를 직접 설계할 수 있다는 장점이 있다.
리스크
반대로 구현 책임은 가장 크다.
Embla나 Swiper처럼 이미 완성된 carousel 엔진을 사용하는 것이 아니기 때문에 다음 요소들을 직접 설계해야 한다.
touch gesture wheel 입력 keyboard 접근성 drag threshold snap animation overscroll 처리 접근성 모바일 Safari 대응 Android Chrome 대응
따라서 성능상으로는 가장 유리하지만, 모바일 실기기 검증이 필수다.
판단
성능과 확장성 측면에서는 현재 가장 유리하다.
PLick 피드가 장기적으로 많은 게시물, 이미지, 팬반응 패널을 포함할 가능성이 높다면 Motion 기반 windowing 구조가 가장 설득력 있다.
13.2 Embla Carousel
장점
Embla는 drag/swipe 감각이 자연스럽고 carousel 엔진으로 안정적인 snap을 제공한다.
Swiper보다 구조 제약이 적고 커스터마이징 여지도 있다.
20개 조건에서는 성능상 큰 문제가 없었다.
또한 carousel의 기본 동작을 빠르게 붙일 수 있어 구현 안정성 측면에서 좋은 중간 대안이다.
리스크
현재 구현은 전체 게시물을 렌더링한다.
300개 조건에서 DOM 5766개, image 300개, card 300개가 올라갔다.
또한 300개 조건에서 long task가 발생했다.
즉, Embla 자체가 나쁘다기보다는 현재 구현 방식에서는 대량 게시물 조건에서 부담이 커진다.
Embla를 최종 후보로 가져가려면 별도의 virtualization 또는 windowing 전략을 붙여야 한다.
판단
Embla는 UX 감각이 좋고 안정적인 중간 대안이다.
다만 최종 후보가 되려면 Embla에도 virtualization/windowing 전략을 별도로 붙여야 한다.
13.3 Swiper
장점
Swiper는 vertical slider 기능이 빠르게 동작한다.
slide change 이벤트와 입력 대응도 잘 갖춰져 있다.
비교 PoC를 빠르게 만들기 좋고, 기본적인 슬라이더 기능을 붙이는 속도도 빠르다.
리스크
Swiper는 DOM 구조와 라이브러리 정책에 의존하는 정도가 크다.
현재 구현은 전체 게시물을 렌더링한다.
300개 조건에서 평균 step time이 103ms로 가장 높았고, long task도 발생했다.
PLick처럼 세로 피드와 가로 패널을 정교하게 결합해야 하는 구조에서는 라이브러리의 추상화가 오히려 제약이 될 수 있다.
판단
빠른 프로토타입이나 fallback 비교군으로는 유용하다.
하지만 PLick 메인 피드의 최종 구현으로 가져가기에는 구조 의존성과 대량 렌더링 문제가 있다.
13.4 CSS Scroll Snap
장점
CSS Scroll Snap은 네이티브 스크롤 기반이라 구현이 단순하다.
브라우저의 기본 스크롤 감각을 활용할 수 있고, 기본 접근성 측면에서도 장점이 있다.
10회 이동 성공률도 안정적이었다.
리스크
현재 구현은 전체 게시물을 렌더링한다.
또한 스와이프 거리, 속도, 대각선 제스처, 가로 패널 진입 분기를 세밀하게 제어하기 어렵다.
PLick의 피드는 오른쪽 스와이프 팬반응 패널과 결합될 가능성이 높다.
이 경우 단순 세로 스크롤 기반인 CSS Scroll Snap은 제스처 제어권이 부족할 수 있다.
300개 조건에서 long task도 발생했다.
판단
초기 레이아웃 검증이나 fallback으로는 좋다.
하지만 PLick의 앱형 릴스 피드 경험을 책임지는 메인 구현으로는 제어권이 부족하다.
14. 정량 의사결정 매트릭스
정량 테스트와 현재 구현 구조를 기준으로 1~5점으로 평가했다.
| Criteria | Weight | Motion | Embla | Swiper | CSS Snap |
|---|---|---|---|---|---|
| 300개 DOM 안정성 | 25 | 5 | 2 | 2 | 2 |
| 이미지 렌더링 규모 | 20 | 5 | 2 | 2 | 2 |
| Long task 안정성 | 20 | 5 | 2 | 2 | 2 |
| 이동 성공률 | 10 | 5 | 5 | 5 | 5 |
| 제스처 커스터마이징 | 15 | 5 | 4 | 3 | 2 |
| 구현 단순성 | 10 | 2 | 3 | 4 | 5 |
가중 점수는 다음과 같다.
| Implementation | Weighted Score |
|---|---|
| Motion | 470 / 500 |
| Embla | 285 / 500 |
| Swiper | 280 / 500 |
| CSS Scroll Snap | 275 / 500 |
주의할 점은 이 점수표가 “현재 구현 상태” 기준이라는 점이다.
Embla, Swiper, CSS Scroll Snap에 virtualization을 추가하면 성능 점수는 달라질 수 있다.
하지만 그 경우 구현 복잡도가 올라간다.
특히 CSS Scroll Snap은 native scroll과 virtualization을 안정적으로 결합해야 하는 별도 문제가 생긴다.
15. 최종 추천 방향
현재 PoC 결과 기준으로는 Motion 기반 custom gesture + current-index windowing 구조를 1순위 후보로 유지한다.
이유는 다음과 같다.
1. PLick의 릴스 피드는 서비스 핵심 UX이며, 세로 전환과 우측 패널 진입을 직접 제어해야 한다. 2. Motion 구현은 게시물 수가 300개로 늘어도 DOM과 image 수를 낮게 유지한다. 3. 300개 조건에서 Motion은 long task가 발생하지 않았다. 4. current index 기반으로 preload, prefetch, analytics, fan reaction panel을 연결하기 쉽다. 5. Embla와 비슷한 track 기반 전환감으로 개선되었고, 동시에 windowing 최적화가 가능했다.
다만 이 결과만으로 최종 확정하기에는 부족하다.
이번 정량 테스트는 desktop headless Chromium의 wheel 입력 중심이며, 실제 MVP의 핵심 입력은 모바일 touch swipe다.
따라서 다음 단계에서 모바일 실기기 검증을 반드시 진행해야 한다.
16. 이번 테스트의 한계
이번 테스트에는 다음 한계가 있다.
1. 모바일 Safari와 Android Chrome 실기기에서 측정하지 않았다. 2. Playwright headless Chromium 기반이라 실제 touch input과 완전히 같지 않다. 3. 이미지 decode, GPU compositing, 모바일 주소창 접힘/펼침 영향을 충분히 반영하지 못했다. 4. heap metric은 테스트 환경에서 충분히 유의미하게 변하지 않았다. 5. 팬반응 패널 내부 스크롤, 상세 패널, analytics, URL sync는 이번 벤치마크 범위에 포함하지 않았다. 6. Embla, Swiper, CSS Scroll Snap에는 별도 virtualization 최적화를 적용하지 않았다.
17. 다음 검증 체크리스트
17.1 모바일 실기기 UX
iOS Safari에서 세로 swipe가 손가락을 자연스럽게 따라오는가? Android Chrome에서 빠르게 연속 swipe해도 끊김이 없는가? 100dvh 기반 높이가 주소창 접힘/펼침 상황에서도 안정적인가?
17.2 제스처 충돌
오른쪽 swipe로 fan reaction panel을 열 수 있는가? 대각선 swipe가 세로 이동과 패널 진입 중 잘못 해석되지 않는가? 버튼 tap이 drag로 오인되지 않는가? 브라우저 뒤로가기 제스처와 충돌하지 않는가?
17.3 Production-like data
실제 이미지 비율과 용량을 넣어도 동일한 결과가 나오는가? post card에 더 많은 UI, badge, reaction preview가 들어가도 DOM 안정성이 유지되는가? fan reaction panel을 열고 닫은 뒤에도 frame drop이 없는가?
17.4 접근성
keyboard ArrowUp / ArrowDown 이동이 유지되는가? screen reader 흐름을 망치지 않는가? focus가 panel open / close에서 자연스럽게 이동하는가?
18. 다음 단계
다음 단계는 아래 순서로 진행한다.
1. Motion 구현에 모바일 touch 시나리오를 더 명확히 검증한다. 2. /motion에 우측 swipe panel open 동작을 실제 drag gesture 기준으로 더 정교하게 만든다. 3. 모바일 실기기 수동 체크리스트를 작성하고 결과를 decision.md에 반영한다. 4. 필요하면 Embla에도 windowing을 붙인 2차 비교군을 만든다. 5. 최종 구현 후보를 Motion 단독 또는 Motion vs Embla-windowed 두 후보로 좁힌다.
19. 현재 결정
현재 정량 PoC 결과만 기준으로는 다음과 같이 판단한다.
1순위: Motion custom gesture + windowing 2순위: Embla Carousel + future windowing 검토 3순위: CSS Scroll Snap fallback 4순위: Swiper comparison / fallback
따라서 features/01-reels-feed-swipe/decision.md의 최종 결정은 아직 TBD로 유지한다.
다만 현재까지의 정량 결과는 Motion 우선 방향을 강하게 지지한다.
20. 결론
PLick의 릴스형 피드는 서비스의 중심 경험이다.
따라서 구현 속도만을 기준으로 CSS Scroll Snap을 선택하기보다, 사용자 경험 완성도와 장기적인 확장성을 함께 고려해야 한다.
이번 PoC에서는 네 가지 구현 방식을 같은 조건에서 비교했다.
그 결과, 현재 구현 기준으로 Motion 기반 custom gesture는 게시물 수가 300개로 늘어나도 DOM, image, card 수를 낮게 유지했고, long task도 발생하지 않았다.
반면 Embla, Swiper, CSS Scroll Snap은 현재 구현 기준으로 전체 게시물을 DOM에 렌더링했고, 300개 조건에서 long task가 발생했다.
따라서 현재 정량 결과 기준으로는 Motion custom gesture + current-index windowing 구조가 PLick의 릴스형 피드에 가장 적합한 후보로 보인다.
다만 실제 서비스의 핵심 입력은 모바일 touch swipe이므로, 최종 결정 전에는 반드시 iOS Safari와 Android Chrome 실기기에서 제스처 감각, 가로 패널 충돌, 주소창 높이 변화, 접근성을 추가 검증해야 한다.
최종적으로는 다음과 같은 방향으로 의사결정을 이어간다.
Motion 기반 구현을 1순위로 유지한다. 모바일 실기기 검증을 통해 touch swipe 감각과 제스처 충돌을 확인한다. 문제가 크지 않다면 Motion 기반 구현을 메인 후보로 확정한다. 문제가 발생하면 Embla + windowing 조합을 2차 후보로 비교한다.
이번 PoC의 결론은 단순히 “Motion이 빠르다”가 아니다.
더 정확히는 다음과 같다.
PLick처럼 릴스형 피드가 서비스의 핵심 UX이고, 세로 전환과 우측 패널 진입을 직접 제어해야 하며, 장기적으로 많은 게시물과 이미지가 들어갈 가능성이 높은 서비스에서는 current-index 기반 windowing 구조가 중요하다.
현재 구현 기준으로 이 구조를 가장 잘 만족한 것은 Motion custom gesture 방식이었다.