가로 스와이프 UX 리서치 및 PoC
2026년 6월 12일
PLick 가로 스와이프 UX 리서치 및 Side Panel Entry PoC 결과 정리
작성자: 김도완
1. 배경
PLick 사용자 서비스의 핵심 화면은 릴스형 피드다.
사용자는 첫 화면에서 스포츠 루머 콘텐츠를 빠르게 넘겨보고, 관심 있는 게시물에서만 상세 정보나 팬반응을 확인한다.
이때 “관심 있는 게시물에서만 더 깊이 들어가는” 동작이 바로 우측 스크롤 / 가로 패널 진입 UX다.
현재 피드 기본 UX의 최소 기능 범위는 다음과 같다.
| 입력 | 동작 |
|---|---|
| 상하 스크롤 | 다음 게시물 / 이전 게시물 이동 |
| 우측 스크롤 | 현재 게시물의 상세 정보 또는 팬반응 패널 진입 |
| 팬반응 버튼 클릭 | 현재 게시물의 팬반응 패널 진입 |
이 문서는 위 범위 중 두 번째 주제인 우측 스크롤 / 가로 패널 진입 UX를 검증하는 자료다.
2. 동작 모델
2.1 이번 PoC가 만들 화면
이번 패널은 다른 화면으로 전환되는 방식이 아니다.
현재 릴스 위에 패널이 슬라이드되어 덮이는 오버레이 방식이다.
즉, 사용자가 현재 보고 있던 릴스 화면은 사라지지 않고, 그 위에 상세/팬반응 패널이 덮인다.
2.2 핵심 동작 정의
1. 패널은 현재 릴스 위에 떠 있는 오버레이 레이어다
릴스 화면은 사라지지 않고 패널 아래에 그대로 남는다.
패널은 화면 바깥에서 들어와 현재 릴스를 덮는다.
릴스 화면 └─ 오버레이 패널
2. 진입은 “슬라이드되어 덮는” 모션이다
우측 스와이프를 하면 패널이 손가락을 따라 들어온다.
일정 거리 또는 속도를 넘으면 패널이 완전히 열린다.
반대로 기준을 넘지 못하면 다시 닫힌다.
우측 스와이프 → 패널이 손가락을 따라 들어옴 → 임계값 충족 시 완전히 열림 → 임계값 미달 시 원래 위치로 스냅백
3. 닫기도 같은 슬라이드의 역방향이다
X 버튼을 누르면 패널이 다시 슬라이드되어 나가며 원래 릴스로 돌아온다.
패널을 반대 방향으로 스와이프해서 밀어내도 똑같이 닫힌다.
즉, 들어올 때와 나갈 때의 모션은 대칭이다.
열기: 화면 바깥 → 화면 안쪽 닫기: 화면 안쪽 → 화면 바깥
4. 진입 경로는 두 가지이고, 둘 다 같은 애니메이션을 쓴다
패널 진입 경로는 두 가지다.
1. 우측 스와이프 제스처로 진입 2. 화면의 버튼으로 진입
예를 들어 사용자가 “팬 반응 1.9K →” 버튼을 누르거나 상세 버튼을 눌러도, 제스처로 연 것과 똑같이 패널이 슬라이드되어 열린다.
즉, 버튼 진입과 제스처 진입은 별개의 UI가 아니라 같은 open 상태와 같은 애니메이션을 공유한다.
2.3 도착 패널 종류
도착 패널은 크게 두 종류를 고려한다.
| 패널 | API |
|---|---|
| 게시물 상세 패널 | GET /api/posts/{postId}/detail |
| 팬반응 패널 | GET /api/posts/{postId}/fan-reactions/summary |
이번 PoC에서는 상세와 팬반응을 하나의 통합 패널로 구성했다.
3. 이 문서의 방향
가로 슬라이드 기술 자체는 세로 피드 PoC에서 확정한 것과 동일하게 Motion을 사용한다.
즉, 세로 전환과 가로 패널 진입은 같은 제스처/애니메이션 레이어를 공유한다.
따라서 이 문서는 “어떤 라이브러리가 더 빠른가”를 비교하는 자료가 아니다.
이미 Motion으로 방향을 정한 상태에서, 다음 두 가지를 검증하는 것이 목적이다.
3.1 구현 가능성
첫 번째 검증 목표는 구현 가능성이다.
확인해야 할 내용은 다음과 같다.
현재 릴스를 덮는 오버레이 슬라이드 진입/닫기를 Motion으로 만들 수 있는가? 버튼 진입과 제스처 진입을 같은 애니메이션으로 통일할 수 있는가?
3.2 대각선 제스처 버그 여부
두 번째이자 가장 중요한 검증 목표는 대각선 제스처 버그 여부다.
세로 피드 이동과 가로 패널 진입이 같은 제스처 레이어에 있을 때, 대각선 또는 비스듬한 스와이프에서 버그가 생기면 안 된다.
확인해야 할 내용은 다음과 같다.
세로 피드 이동과 가로 패널 진입이 동시에 발생하지 않는가? 한 제스처가 중간에 축이 흔들려 의도치 않게 패널을 열거나 피드를 넘기지 않는가? 패널이 열린 상태에서 뒤쪽 피드가 움직이지 않는가?
4. 참고 링크
| 기술 | 링크 | 설명 |
|---|---|---|
| Motion for React | Motion React Docs | drag, gesture, layout transition을 직접 제어할 수 있는 애니메이션 라이브러리 |
| Motion Drag | Motion Drag Animation | drag, dragConstraints, dragElastic, onDrag, onDragEnd 등 드래그 제어 문서 |
| Motion dragDirectionLock | Motion Drag | 한 제스처가 한 축으로만 움직이도록 방향을 잠그는 옵션 |
| Pointer Events | MDN Pointer Events | 터치/마우스/펜 입력을 통합 처리하는 표준 이벤트 |
| touch-action | MDN touch-action | 요소에서 브라우저가 처리할 제스처를 제어하는 CSS 속성 |
| overscroll-behavior | MDN overscroll-behavior | 스크롤 체이닝을 제어하는 CSS 속성 |
5. 핵심 난제: 같은 레이어에서의 세로/가로 분기
PLick 피드는 한 손가락 입력에서 두 가지 의미를 동시에 처리해야 한다.
상하 스와이프 → 게시물 이동 우측 스와이프 → 패널 진입
두 동작이 같은 Motion 제스처 레이어를 공유하므로, 입력을 “세로냐 가로냐”로 명확히 갈라야 한다.
이 분기가 모호하면 다음과 같은 버그가 생길 수 있다.
비스듬히 스와이프했더니 피드도 한 칸 넘어가고 패널도 살짝 열린다. 패널을 여는 도중 손가락이 위로 휘어 피드가 같이 움직인다. 패널이 반쯤 열린 상태에서 세로로 그으면 패널과 피드가 동시에 반응한다.
따라서 이번 PoC의 핵심은 세로 피드 이동과 가로 패널 진입의 분기를 안정적으로 만들 수 있는가이다.
6. 분기 전략
이번 PoC에서는 다음 규칙을 구현하고 검증한다.
6.1 제스처 시작 시 주축(axis)을 먼저 확정한다
터치 시작 후 아주 작은 임계 거리만큼 움직이는 동안 dx와 dy를 비교한다.
abs(dx) > abs(dy) → 가로 제스처 abs(dy) > abs(dx) → 세로 제스처
예를 들어 10~16px 정도 움직인 뒤 어느 축이 더 우세한지 판단한다.
6.2 한 번 확정된 축은 제스처가 끝날 때까지 고정한다
한 번 가로 또는 세로로 확정된 제스처는 손가락을 떼기 전까지 다른 축으로 바뀌지 않아야 한다.
Motion의 dragDirectionLock을 사용하면 한 축이 정해진 뒤 다른 축 이동을 막을 수 있다.
처음에 가로로 확정 → 중간에 손가락이 위로 휘어도 가로 동작만 유지 처음에 세로로 확정 → 중간에 손가락이 옆으로 휘어도 세로 동작만 유지
이렇게 해야 대각선 입력에서 피드 이동과 패널 진입이 동시에 발생하지 않는다.
6.3 가로 패널 진입 방향과 임계값을 정의한다
패널 진입은 특정 방향의 가로 스와이프에서만 발생해야 한다.
이번 PoC에서는 가로 우세 입력이면서 거리 또는 속도 임계값을 넘었을 때 패널 진입으로 판정한다.
가로 우세 조건: absX > absY * DOMINANCE_RATIO 거리 조건: offsetX가 DISTANCE_THRESHOLD 이상 속도 조건: velocityX가 VELOCITY_THRESHOLD 이상
거리 조건 또는 속도 조건 중 하나라도 충족하면 패널 진입으로 볼 수 있다.
이렇게 하면 두 가지 입력을 모두 수용할 수 있다.
길고 느린 드래그 짧고 빠른 flick
반대로 임계값을 넘지 못하면 패널은 다시 닫힌 위치로 스냅백한다.
6.4 패널이 열려 있는 동안에는 세로 피드 드래그를 비활성화한다
오버레이가 떠 있는 상태에서는 입력을 패널 레이어가 가져가야 한다.
패널이 열린 동안 뒤쪽 피드가 움직이면, 패널을 닫았을 때 사용자가 원래 보던 게시물이 아닌 다른 게시물에 위치할 수 있다.
따라서 패널이 열린 동안에는 다음 입력을 차단해야 한다.
뒤쪽 피드 drag 뒤쪽 피드 wheel 뒤쪽 피드 keyboard 이동
또한 패널 내부의 댓글 목록은 세로 스크롤이 필요하다.
이때 댓글 목록의 스크롤이 부모 피드로 전파되지 않도록 overscroll-behavior와 touch-action을 적절히 설정해야 한다.
7. 오버레이 슬라이드 구현 방향
패널은 별도 화면으로 라우팅해 교체하지 않는다.
현재 릴스 위에 고정된 오버레이를 만들고, translateX로 움직인다.
7.1 상태 구조
필요한 상태는 다음과 같다.
isPanelOpen → 패널 열림 여부 panelType → "detail" | "fan-reactions" dragX → 드래그 중 실시간 이동량
7.2 제스처 진입 흐름
사용자 우측 스와이프 → 주축이 가로로 확정 → 패널을 손가락 따라 translateX로 끌어옴 → onDragEnd에서 거리/속도 임계 판정 임계값 충족: translateX 0%로 스냅 isPanelOpen = true 임계값 미달: translateX 100% 또는 -100%로 스냅백 isPanelOpen = false
7.3 버튼 진입 흐름
팬반응/상세 버튼 클릭 → panelType 지정 → isPanelOpen = true → translateX 100% 또는 -100%에서 0%로 동일 애니메이션 재생
버튼으로 열어도 제스처로 연 것과 같은 애니메이션을 사용한다.
7.4 닫기 흐름
X 버튼 클릭 또는 패널 닫기 방향 스와이프 또는 배경 클릭 또는 ESC → translateX 0%에서 화면 바깥으로 슬라이드 → isPanelOpen = false → 원래 릴스로 복귀
7.5 개념 코드
import { motion } from "motion/react"; function PostOverlayPanel({ open, panelType, onClose }) { return ( <motion.aside className="overlayPanel" initial={{ x: "100%" }} animate={{ x: open ? "0%" : "100%" }} transition={{ type: "spring", stiffness: 400, damping: 40 }} drag="x" dragConstraints={{ left: 0, right: 0 }} dragElastic={0.15} onDragEnd={(_, info) => { if (info.offset.x > 80 || info.velocity.x > 500) { onClose(); } }} > {panelType === "detail" ? <PostDetail /> : <FanReaction />} </motion.aside> ); }
피드 레이어 쪽에서는 세로/가로 주축 판별만 담당한다.
가로로 확정되면 오버레이의 open 상태를 토글한다.
버튼 클릭도 동일하게 open 상태를 토글하므로, 진입 경로가 둘이어도 애니메이션은 하나로 통일된다.
8. 대각선 스크롤 버그 시나리오
PoC에서는 다음 상황에서 의도치 않은 동작이 없어야 한다.
8.1 대각선 진입
오른쪽 위 또는 오른쪽 아래로 비스듬히 스와이프했을 때, 피드 이동과 패널 진입이 동시에 일어나면 안 된다.
주축 확정과 dragDirectionLock으로 하나의 동작만 발생해야 한다.
8.2 제스처 도중 방향 전환
처음엔 가로로 긋다가 손가락을 위로 꺾어도, 한 번 가로로 확정됐으면 피드가 같이 움직이면 안 된다.
반대로 처음에 세로로 확정됐다면, 중간에 손가락이 옆으로 움직여도 패널이 열리면 안 된다.
8.3 패널 반쯤 열린 상태에서 세로 입력
진입 임계 미달로 패널이 반쯤 끌린 상태에서 손을 떼면 깔끔히 스냅백해야 한다.
그 직후 세로로 그으면 정상적으로 피드 이동으로 인식돼야 한다.
8.4 닫기 도중 대각선
패널을 밀어 닫는 도중 비스듬히 움직여도, 닫힘 또는 유지 여부만 결정되어야 한다.
이때 뒤쪽 피드가 움직이면 안 된다.
8.5 버튼 진입 직후 제스처
버튼으로 연 패널을 곧바로 스와이프로 닫을 때, 같은 모션으로 자연스럽게 닫혀야 한다.
8.6 빠른 연속 입력
열고 닫고를 빠르게 반복해도 상태가 꼬이거나 애니메이션이 끊기지 않아야 한다.
9. 함께 확인할 사항
9.1 패널 내부 스크롤과 닫기 제스처의 분리
팬반응 패널의 댓글 목록은 세로 스크롤이다.
이 세로 스크롤이 패널 닫기나 뒤쪽 피드 이동으로 새어 나가면 안 된다.
이를 막기 위해 다음 속성을 검토한다.
.panelContent { overscroll-behavior: contain; touch-action: pan-y; }
9.2 브라우저 / WebView 뒤로가기 제스처 충돌
iOS Safari의 좌측 edge swipe는 브라우저 뒤로가기와 연결될 수 있다.
따라서 패널 진입/닫기 제스처가 브라우저의 시스템 제스처와 충돌하지 않는지 확인해야 한다.
Android 또는 WebView에서는 시스템 back 버튼을 눌렀을 때 다음 중 어떤 동작을 할지 정해야 한다.
1. 패널이 열려 있으면 패널을 닫는다. 2. 패널과 무관하게 이전 페이지로 이동한다.
서비스 UX 관점에서는 패널이 열려 있을 때 back 입력은 먼저 패널을 닫는 방식이 자연스럽다.
9.3 모바일 viewport
오버레이가 화면 전체를 덮을 때 100dvh 기반 레이아웃이 안정적인지 확인해야 한다.
모바일 브라우저에서는 주소창이 접히고 펼쳐지면서 화면 높이가 바뀔 수 있기 때문이다.
9.4 prefetch 연동 지점
패널은 사용자가 “관심 있는 게시물”에서만 여는 영역이다.
따라서 패널 진입이 예상되는 시점에 데이터를 미리 가져오면 UX가 좋아진다.
예상 prefetch 지점은 다음과 같다.
우측 드래그 시작 → 주축이 가로로 확정되는 시점에 detail / fan-reactions summary / comments 첫 페이지 prefetch 버튼 진입 → 버튼 노출 또는 근접 시점에 prefetch
패널 open 시 캐시에 있으면 즉시 표시하고, 없으면 조회한다.
9.5 접근성
제스처 전용 진입은 키보드나 보조기술 사용자를 배제할 수 있다.
따라서 버튼 진입 경로는 반드시 보장해야 한다.
패널 접근성에서 확인할 항목은 다음과 같다.
패널 open 시 focus 이동 X 버튼 또는 ESC로 닫기 닫힘 시 포커스를 트리거로 복귀 role="dialog" aria-modal 적용
9.6 우측 스크롤로 어떤 패널을 기본으로 열 것인가
설계상 우측 스크롤은 “상세 또는 팬반응” 진입이다.
첨부된 화면 기준으로는 상세 본문과 베스트/실시간 반응이 한 패널에 함께 보이는 형태를 고려할 수 있다.
PoC에서는 다음 중 하나를 확정해야 한다.
1. 우측 스와이프 = 상세 + 팬반응 통합 패널 1개 2. 상세 패널과 팬반응 패널을 구분하고 버튼으로 갈라 진입
이 결정은 prefetch 우선순위와 직접 연결된다.
통합 패널: detail + fan-reactions summary + comments prefetch 분리 패널: 진입 경로에 따라 detail 또는 fan-reactions 우선 prefetch
10. PoC에서 확인할 항목
10.1 오버레이 슬라이드 동작
우측 스와이프 시 패널이 손가락을 따라 릴스를 덮으며 들어오는가? 임계 미달 시 깔끔히 스냅백되는가? X 버튼 또는 닫기 스와이프로 같은 모션으로 닫히는가? 버튼 진입이 제스처 진입과 동일한 슬라이드 애니메이션으로 열리는가?
10.2 대각선 / 제스처 분기
대각선 스와이프에서 피드 이동과 패널 진입이 동시에 일어나지 않는가? 주축 확정 후 방향을 꺾어도 한 동작만 유지되는가? 패널 반쯤 열린 상태, 닫기 도중 대각선에서 버그가 없는가? 빠른 연속 열고/닫기에서 상태가 꼬이지 않는가?
10.3 패널 내부 스크롤
패널 열림 중 뒤쪽 세로 피드가 움직이지 않는가? 댓글 목록 세로 스크롤이 닫기/피드 이동으로 새어 나가지 않는가?
10.4 데이터 / 상태 연동
우측 드래그 시작 시 detail / summary / comments prefetch를 트리거하는가? 패널 open 시 현재 postId 기준으로 정확한 내용이 뜨는가? 버튼/제스처 어느 경로로 열어도 같은 상태가 되는가?
10.5 모바일 / WebView 호환성
iOS Safari edge swipe와 충돌하지 않는가? Android back / WebView 뒤로가기와 닫기 동작을 일관되게 처리하는가? 100dvh 오버레이에서 주소창 변화에도 레이아웃이 유지되는가? 저사양 기기에서 진입/닫기 애니메이션 프레임 드랍이 없는가?
10.6 접근성
버튼 진입 경로가 키보드/보조기술에서 동작하는가? 패널 open 시 focus 이동, ESC/X 닫기, 닫힘 시 포커스 복귀가 되는가?
11. 리서치 단계 결론
이번 우측 스크롤 / 가로 패널 진입은 세로 피드와 분리된 기능이 아니다.
같은 Motion 제스처·애니메이션 레이어 위에서 동작하는 오버레이다.
패널은 화면을 교체하지 않고 현재 릴스를 슬라이드로 덮으며, 닫을 때도 같은 모션의 역방향으로 릴스에 돌아온다.
진입은 우측 스와이프와 버튼 두 경로 모두 같은 애니메이션을 쓴다.
기술은 세로와 동일하게 Motion으로 확정되어 있으므로, 이 PoC의 검증 목표는 두 가지다.
1. 오버레이 슬라이드 진입/닫기 구현 가능성 2. 세로 피드와 가로 패널이 같은 레이어에 있을 때 대각선/비스듬한 제스처에서 버그가 생기지 않는지
핵심은 제스처 시작 시 주축을 먼저 확정하고, 확정된 축으로만 동작하게 고정하며, 패널이 열린 동안 피드 드래그와 내부 스크롤 전파를 차단하는 것이다.
이 규칙으로 대각선 입력에서의 충돌이 충분히 통제되는지를 PoC로 확인한다.
12. Side Panel Entry PoC Result
12.1 Summary
PLick 릴스형 피드의 우측 스크롤 / 가로 패널 진입 UX를 검증하기 위해, 01번에서 확정한 Motion 세로 피드(/motion) 위에 오버레이 슬라이드 패널을 실제로 구현하고 브라우저에서 동작을 검증했다.
01번 피드 PoC가 “어떤 기술이 더 빠른가”를 보는 정량 벤치마크였다면, 이번 PoC는 기술이 이미 Motion으로 확정된 상태에서 다음 두 가지를 검증하는 동작/충돌 테스트다.
1. 현재 릴스를 덮는 오버레이 슬라이드 진입/닫기를 제스처·버튼 양쪽에서 동일한 애니메이션으로 구현할 수 있는가. 2. 세로 피드와 가로 패널이 같은 제스처 레이어를 공유할 때, 대각선/비스듬한 입력에서 버그가 생기지 않는가.
이번 검증의 핵심 결론은 다음과 같다.
1. 패널은 화면 전환이 아니라 오버레이로 동작하며, 진입 시 translateX로 릴스를 덮고 닫을 때 반대 방향으로 슬라이드되어 사라진다. 2. 버튼 진입과 우측 스와이프 진입이 같은 open 상태를 토글해 동일한 슬라이드 애니메이션으로 열린다. 3. 세로 우세 대각선 스와이프는 패널을 열지 않는다. 가로 우세 우측 스와이프만 진입으로 판별된다. 4. 패널이 열려 있는 동안 피드의 drag, wheel, keyboard 입력이 모두 차단되어 배경 오작동이 없다. 5. 빌드, 타입체크, 기존 단위 테스트 9개가 모두 통과했고 런타임 콘솔 에러가 없다. 6. 실기기 터치, iOS Safari edge swipe, Android/WebView 뒤로가기, prefetch 연동은 이번 범위 밖이며 후속 검증이 필요하다.
13. Test Target
| Item | Value |
|---|---|
| Route | /motion |
| New component | src/components/feed/detail-panel.tsx |
| Modified | src/app/motion/motion-feed.tsx, src/components/feed/feed-card.tsx |
| Gesture arbitration | src/lib/feed-gesture.ts 기존 getGestureIntent 재사용 |
이번 PoC는 4개 구현을 비교하지 않는다.
가로 슬라이드 기술 자체를 세로 피드와 동일하게 Motion으로 쓰기로 확정했기 때문에, 단일 구현(/motion)의 동작 정확성과 제스처 충돌 여부만 검증한다.
14. Behavior Model
패널은 현재 릴스 위에 떠 있는 오버레이 레이어다.
릴스는 사라지지 않고 패널 아래에 남는다.
진입은 “우측 스크롤 방향”으로 덮는다.
이번 구현에서는 패널이 화면 왼쪽 밖에서 들어와 오른쪽으로 슬라이드되며 릴스를 덮는다.
진입: translateX(-100%) → translateX(0) 닫기: translateX(0) → translateX(-100%)
닫기는 진입의 반대 방향 슬라이드다.
X 버튼, 닫기 드래그, 배경 탭, ESC 모두 같은 모션의 역방향으로 릴스에 복귀한다.
진입 경로는 두 가지다.
1. 우측 스와이프 2. 팬 반응 → 버튼
둘 다 같은 open 상태를 토글해 동일한 애니메이션을 사용한다.
진입 방향은 detail-panel.tsx의 ENTER_FROM 상수로 뒤집을 수 있다.
const ENTER_FROM = "-100%"; // 또는 "100%"
15. Test Scenario
/motion 단일 라우트에서 다음 시나리오를 수동/스크립트로 실행했다.
1. 버튼 진입 팬 반응 → 클릭 후 패널 mount 및 최종 위치 확인 2. 버튼 닫기 패널 헤더의 ✕ 클릭 후 슬라이드 아웃 및 unmount 확인 3. 키보드 진입/차단/닫기 ArrowRight 진입 ArrowDown 열림 중 피드 이동 차단 Escape 닫기 4. 제스처 진입 판별 pointer 이벤트로 우측/대각선/좌측 스와이프를 발생시켜 진입 여부 확인 5. 정적 검증 타입체크, 프로덕션 빌드, 단위 테스트, 콘솔 에러 확인
16. Measurement Method
검증은 두 축으로 진행했다.
16.1 정적 검증
다음 명령으로 타입, 빌드, 단위 테스트를 확인했다.
npx tsc --noEmit npm run build npm run test
16.2 런타임 동작 검증
dev server를 mobile viewport로 띄운 뒤, 브라우저 컨텍스트에서 DOM 상태와 transform 값을 직접 읽어 동작을 확인했다.
| 항목 | 값 |
|---|---|
| Base URL | http://localhost:3000 |
| Viewport | 375 x 812 |
| Route | /motion |
런타임 검증에서 사용한 관측 지표는 다음과 같다.
| 관측 지표 | 의미 |
|---|---|
[role="dialog"] 존재 여부 | 패널 mount / unmount 판정 |
패널 aside의 computed transform | 슬라이드 위치 확인 |
| 패널 내부 요소 존재 | 콘텐츠 렌더 확인 |
| 키보드/포인터 이벤트 후 상태 변화 | 제스처 분기 정확성 확인 |
16.3 transform 값을 확인한 이유
오버레이 슬라이드의 핵심은 “패널이 화면을 덮었는가, 그리고 닫을 때 반대 방향으로 빠지는가”이다.
단순히 패널이 보이는지 여부만으로는 슬라이드 방향과 덮임 정도를 확인할 수 없다.
따라서 다음 값을 직접 읽어 검증했다.
진입 완료: transform = none 즉, x: 0 상태로 완전히 덮음 닫기 중간: transform = matrix(...) 즉, 닫히는 방향으로 이동 중
16.4 gesture arbitration을 확인한 이유
세로 피드와 가로 패널이 같은 제스처 레이어를 공유하기 때문에, 한 번의 포인터 입력이 두 동작을 동시에 일으키거나 잘못된 동작으로 해석되면 안 된다.
특히 대각선 스와이프는 사용자가 가장 흔히 만드는 애매한 입력이다.
그래서 우측 입력, 대각선 입력, 좌측 입력을 각각 발생시켜 진입 판별이 의도대로 갈리는지 확인했다.
16.5 background lock을 확인한 이유
패널이 오버레이로 떠 있는 동안 뒤쪽 피드가 입력에 반응하면, 패널을 닫았을 때 엉뚱한 게시물로 이동해 있거나 스크롤이 어긋나는 버그가 생긴다.
따라서 패널 열림 상태에서 피드의 drag, wheel, keyboard가 모두 비활성화되는지 확인했다.
17. Gesture / Animation Parameters
검증 시점의 임계값은 다음과 같다.
| Parameter | Value | Source | Meaning |
|---|---|---|---|
DISTANCE_THRESHOLD | 72px | feed-gesture.ts | 진입/이동으로 인정할 최소 이동 거리 |
VELOCITY_THRESHOLD | 650 | feed-gesture.ts | 거리 미달 시 진입으로 인정할 속도 |
DOMINANCE_RATIO | 1.2 | feed-gesture.ts | 한 축이 우세하다고 볼 비율 |
ENTER_FROM | "-100%" | detail-panel.tsx | 진입 시작 위치 |
CLOSE_DISTANCE | 96px | detail-panel.tsx | 드래그로 닫기 인정 거리 |
CLOSE_VELOCITY | 520 | detail-panel.tsx | 드래그로 닫기 인정 속도 |
| Slide transition | spring(stiffness 380, damping 40, mass 0.9) | detail-panel.tsx | 진입/닫기 슬라이드 스프링 |
진입 제스처 판별은 getGestureIntent가 담당한다.
가로 우세이면서 거리 또는 속도 임계를 넘고, 우측 방향일 때만 panel로 판정한다.
가로 우세: absX > absY * 1.2 진입 조건: offsetX > 0 AND (distance threshold 충족 OR velocity threshold 충족)
세로 우세 입력은 패널 진입으로 새지 않는다.
18. Environment
| Item | Value |
|---|---|
| App | apps/reels-feed-swipe-motion |
| Route | /motion |
| Base URL | http://localhost:3000 |
| Viewport | 375 x 812 |
| Node / Next | Next.js 16.2.9 (Turbopack) |
| Motion | motion v12 (motion/react) |
19. Result
19.1 Overlay slide — 진입/닫기
| Check | Observed | Pass |
|---|---|---|
| 버튼 클릭 → 패널 mount | [role="dialog"] 존재, 닫기 버튼/탭/입력 모두 렌더 | ✅ |
| 진입 완료 위치 | aside transform = none (x: 0, 완전히 덮음) | ✅ |
| ✕ 클릭 → 닫기 슬라이드 | 닫기 중간 transform = matrix(1,0,0,1,-181.895,0) | ✅ |
| 닫기 완료 | [role="dialog"] unmount | ✅ |
진입 완료 시 패널이 x: 0으로 화면을 완전히 덮고, 닫을 때는 진입의 반대 방향으로 슬라이드된 뒤 unmount되는 것을 확인했다.
즉, 진입과 닫기의 모션이 대칭이라는 요구를 만족한다.
19.2 Gesture arbitration — 대각선 버그 검증
pointer 이벤트로 같은 시작점 (100, 400)에서 세 종류 스와이프를 발생시켰다.
| Input | dx, dy | Panel opened | Expected | Pass |
|---|---|---|---|---|
| 우측, 가로 우세 | +130, +10 | true | 진입 | ✅ |
| 대각선, 세로 우세 | +60, +120 | false | 진입 안 함 | ✅ |
| 좌측 | -130, +10 | false | 진입 안 함 | ✅ |
세로 우세 대각선은 패널을 열지 않았다.
좌측 스와이프도 패널을 열지 않았다.
가로 우세 우측 스와이프만 진입으로 판별되어, 대각선 입력에서의 오작동이 없음을 확인했다.
이 결과는 기존 단위 테스트인 feed-gesture.test.ts의 다음 케이스와도 일치한다.
ignores short taps and ambiguous diagonal drags
즉, 애매한 대각선 입력은 none으로 처리된다.
19.3 Keyboard / background lock
| Check | Observed | Pass |
|---|---|---|
| ArrowRight 진입 | 패널 open | ✅ |
| 열림 중 ArrowDown | 패널 콘텐츠 그대로 유지, 피드 이동 차단 | ✅ |
| Escape 닫기 | 패널 unmount | ✅ |
패널이 열린 동안 ArrowDown이 뒤쪽 피드를 이동시키지 않았다.
키보드뿐 아니라 pointer/wheel 핸들러도 panelOpen일 때 early return하도록 막았다.
또한 피드 컴포넌트의 drag도 다음처럼 비활성화했다.
drag={panelOpen ? false : "y"}
19.4 Static checks
| Check | Result |
|---|---|
npx tsc --noEmit | 통과, 에러 없음 |
npm run build | 통과, 8 routes 정적 생성 |
npm run test | 9 passed, 2 files |
| Runtime console error | 로그 없음 |
20. Implementation Detail
20.1 detail-panel.tsx
detail-panel.tsx는 오버레이 패널의 mount/unmount와 슬라이드 애니메이션을 담당한다.
핵심 구현은 다음과 같다.
AnimatePresence로 enter / exit 슬라이드 처리 enter: x: -100% → 0 exit: x: 0 → -100%
닫기 드래그는 다음 설정으로 처리했다.
drag="x" dragConstraints={{ left: 0, right: 0 }} dragElastic={{ left: 1, right: 0 }}
이 설정으로 패널은 좌측으로만 따라오게 하고, 임계 미달이면 다시 0으로 스냅백한다.
onDragEnd에서는 다음 조건을 만족할 때 닫는다.
if (info.offset.x < -96 || info.velocity.x < -520) { onClose(); }
배경 dim은 opacity로 fade in/out되며, 클릭 시 패널을 닫는다.
접근성 처리는 다음과 같이 적용했다.
role="dialog" aria-modal mount 시 닫기 버튼 focus 캡처 단계 Escape 핸들러
패널 내부 스크롤은 뒤쪽 피드로 전파되지 않도록 다음 설정을 적용했다.
overscroll-behavior: contain; touch-action: pan-y;
콘텐츠는 이번 PoC에서 다음 요소를 하나의 패널에 통합했다.
상세 본문 요약 더미 본문 찬반 비율 바 베스트/실시간 탭 댓글 목록 댓글 입력 바
20.2 motion-feed.tsx
motion-feed.tsx에서는 패널 열림 여부를 기준으로 피드 입력을 제어한다.
const panelOpen = panelPost !== null;
panelOpen이 true이면 피드 입력을 차단한다.
onPointerDown onPointerUp onWheel keyboard listener
위 입력들은 panelOpen 상태에서 early return하거나 리스너를 붙이지 않는다.
피드 track의 drag도 다음처럼 비활성화한다.
drag={panelOpen ? false : "y"}
패널 진입은 기존과 동일하게 onPointerUp에서 getGestureIntent가 panel을 반환하면 setPanelPost로 연다.
21. Limitations
이번 PoC에는 다음 한계가 있다.
21.1 실기기 터치로 측정하지 않았다
iOS Safari의 좌측 edge swipe와 닫기 제스처의 충돌, Android/WebView 시스템 뒤로가기와 닫기의 일치 여부는 실기기 검증이 필요하다.
21.2 실제 손가락 드래그의 추종감을 완전히 반영하지 못했다
런타임 검증은 합성 pointer/keyboard 이벤트 기반이다.
따라서 실제 손가락 드래그의 중간 추종감, 관성, 고무줄 감각을 그대로 반영하지 못한다.
21.3 패널 진입 방향은 추후 변경 가능하다
이번 구현에서는 패널 진입 방향을 “왼쪽에서 오른쪽으로 덮음”으로 확정했다.
이는 사용자의 “우측 스크롤 방향으로 덮어씌워진다”는 서술에 대한 해석이다.
실사용 피드백에 따라 ENTER_FROM 상수로 즉시 뒤집을 수 있다.
const ENTER_FROM = "-100%"; // 왼쪽에서 진입 const ENTER_FROM = "100%"; // 오른쪽에서 진입
21.4 prefetch 연동은 미구현이다
우측 드래그 시작 시 detail, summary, comments 조회를 미리 트리거하는 prefetch 연동은 아직 구현하지 않았다.
현재는 이미지 preload만 동작한다.
21.5 상세/팬반응은 하나의 통합 패널로 구성했다
이번 PoC에서는 상세와 팬반응을 하나의 통합 패널로 구성했다.
두 패널을 분리하는 안은 이번 범위에 포함하지 않았다.
21.6 댓글 입력과 탭 전환은 정적 UI다
댓글 입력 바와 탭 전환은 시각 확인용 정적 UI다.
아직 실제 작성/조회 API와 연결하지 않았다.
21.7 긴 댓글 목록 검증이 필요하다
패널 내부 댓글 목록은 항목 수가 적다.
따라서 댓글이 많은 긴 목록에서 내부 스크롤과 닫기 제스처가 충돌하지 않는지는 더 많은 데이터로 재확인해야 한다.
22. Next Validation Checklist
최종 확정 전 다음 항목을 추가로 확인한다.
22.1 Mobile Device
iOS Safari 좌측 edge swipe와 패널 진입/닫기가 충돌하지 않는가? Android/WebView 시스템 뒤로가기로 패널을 닫을지, history 연동과 분리할지 정한다. 100dvh 오버레이가 주소창 접힘/펼침에서도 레이아웃이 유지되는가? 저사양 기기에서 진입/닫기 스프링 애니메이션에 프레임 드랍이 없는가?
22.2 Gesture / Scroll
댓글이 많은 긴 패널에서 내부 세로 스크롤이 닫기/피드 이동으로 새지 않는가? 진입 임계 미달로 반쯤 끌렸다 스냅백한 직후 세로 입력이 정상적으로 피드 이동으로 인식되는가? 빠른 연속 열고/닫기에서 상태가 꼬이지 않는가?
22.3 Data / State
우측 드래그 시작 시 detail / summary / comments prefetch를 트리거하는가? 패널 open 시 캐시 데이터를 즉시 표시하고, 없으면 조회하는가? 버튼/제스처 어느 경로로 열어도 같은 상태가 되는가?
22.4 Accessibility
패널 닫힘 시 포커스가 트리거 버튼으로 복귀하는가? 스크린리더에서 패널 진입, 내용 탐색, 닫기 흐름이 자연스러운가?
23. Proposed Next Step
다음 단계는 아래 순서로 진행한다.
1. 실기기에서 edge swipe, 뒤로가기 충돌, 슬라이드 추종감을 수동 점검한다. 2. 우측 드래그 시작 시점에 detail / summary / comments prefetch를 연결한다. 3. 긴 댓글 목록 데이터로 패널 내부 스크롤과 닫기 제스처 경합을 재검증한다. 4. 실사용 피드백을 바탕으로 진입 방향과 통합/분리 패널 구성을 확정한다.
24. Current Decision
현재 동작 검증 결과만 기준으로는 다음과 같이 판단한다.
확정: 세로 피드와 동일한 Motion 단일 레이어 위 오버레이 슬라이드 패널 진입: 우측 스크롤 방향 버튼/제스처 동일 애니메이션 닫기: 반대 방향 슬라이드 X / 닫기 드래그 / 배경 / ESC 지원 대각선 버그: 세로 우세 입력은 패널 미진입 배경 입력 차단으로 통제됨
세부 결정과 후속 항목은 features/02-side-panel-entry/decision.md에 반영했다.
실기기 검증과 prefetch 연동은 후속 단계로 남긴다.
25. 최종 결론
이번 우측 스크롤 / 가로 패널 진입 UX는 세로 피드와 분리된 별도 기능이 아니다.
01번에서 확정한 Motion 기반 세로 피드 위에 같은 제스처·애니메이션 레이어로 동작하는 오버레이 패널이다.
이번 PoC를 통해 확인한 것은 크게 두 가지다.
1. 현재 릴스를 덮는 오버레이 슬라이드 패널을 Motion으로 구현할 수 있다. 2. 버튼 진입과 제스처 진입을 같은 open 상태와 같은 애니메이션으로 통일할 수 있다. 3. 세로 우세 대각선 입력은 패널 진입으로 새지 않는다. 4. 패널이 열린 동안 뒤쪽 피드 입력을 차단할 수 있다. 5. 타입체크, 빌드, 단위 테스트, 런타임 콘솔 확인 결과 모두 문제없다.
따라서 현재 기준으로는 Motion 단일 레이어 기반 오버레이 패널 구조를 유지한다.
다만 최종 확정 전에는 반드시 실기기 검증이 필요하다.
특히 다음 항목은 후속 단계에서 확인해야 한다.
iOS Safari edge swipe와 충돌 여부 Android / WebView back 처리 방식 실제 손가락 드래그 추종감 긴 댓글 목록 내부 스크롤과 닫기 제스처 충돌 우측 드래그 시작 시 prefetch 연동
현재 결정은 다음과 같다.
PLick의 가로 패널 진입 UX는 Motion 기반 오버레이 슬라이드 패널로 구현한다.
버튼 진입과 제스처 진입은 동일한 애니메이션을 사용한다.
세로 피드와 가로 패널의 충돌은 주축 판별, 임계값, 패널 open 중 배경 입력 차단으로 통제한다.
실기기 터치와 prefetch 연동은 후속 검증 과제로 남긴다.