pile·
프론트엔드·카카오 엔터테인먼트 FE카카오 엔터테인먼트 FE·

Scroll Snap API를 통해 이미지 캐러셀 완성하기

SwiperJS 의 `cssMode` 가 내부적으로 쓰는 Scroll Snap API 가 궁금해서, 자바스크립트 없이 CSS 만으로 이미지 캐러셀을 만들어 본 글이다. 스크롤이 사진 A 와 B 사이에 어정쩡하게 멈추는 문제를 `scroll-snap-type` 과 `scroll-snap-align` 두 속성으로 잡는다.

핵심 포인트
  • 터치 스와이프나 휠 스크롤은 입력이 부정확해서 스크롤 종료 위치가 콘텐츠 경계에 딱 맞지 않는다. Scroll Snap 은 그 종료 위치를 제어된 지점으로 보정한다.
  • 컨테이너에 `scroll-snap-type: <축> <강도>` 를 주면 그 요소가 scroll-snap-container 가 된다.
  • 축은 x·y·block·inline·both, 강도는 none·mandatory·proximity 중에서 고른다.
  • 자식 요소에는 `scroll-snap-align` 을 start·center·end 중 하나로 지정해 어느 모서리를 맞출지 정한다.
  • 결과적으로 자바스크립트 없이 스크롤 보정이 되며, 캐러셀뿐 아니라 페이지 스크롤·드롭다운에도 쓸 수 있다.
상세 정리
  • 문제 상황 설명: 웹에서 포토 앨범을 제공할 때 사진을 하나씩 클릭하는 것보다 슬라이드 쇼가 낫지만, 스크롤이 사진 두 장에 걸쳐 멈추면 경험이 망가진다.
  • 스냅의 정의: 스와이프·휠 동작의 정도에 따라 스냅 포트를 현재 자식 요소에 계속 두거나 다음 자식으로 이동시키는 동작이다.
  • 기본 마크업: `article.carousel > div.carousel__slides > section.carousel__slides__slide` 구조에 각 슬라이드마다 이미지를 넣는다.
  • 기본 CSS: 컨테이너는 500×300, 슬라이드 목록은 `display: flex` 에 `overflow-x: auto`, 슬라이드는 `flex: 0 0 auto` 로 수평 나열한다. 이 상태에서는 스냅이 걸리지 않는다.
  • 1단계: 스크롤이 일어나는 `.carousel__slides` 에 `scroll-snap-type: x mandatory` 를 추가한다.
  • `mandatory` 는 스크롤이 끝나면 반드시 스냅시키고, `proximity` 는 두 슬라이드 사이에 걸쳐 둘 기회를 주되 스냅이 필요한 정도에 가까워졌을 때만 붙인다.
  • 2단계: 슬라이드에 `scroll-snap-align: start` 를 준다. x축 기준일 때 컨테이너의 왼쪽 가장자리와 슬라이드의 왼쪽 가장자리가 일치하도록 멈춘다.
  • `center` 는 수평 중심축끼리, `end` 는 오른쪽 가장자리끼리 맞춘다. 세 경우를 각각 이미지로 비교해 보여준다.
  • 결과 비교: `x mandatory + start`, `x proximity + start`, `y mandatory + start` 세 조합의 동작을 나란히 놓는다. 세로 스냅을 쓰려면 컨테이너의 스크롤 축을 `overflow-y` 로 바꿔야 한다는 단서를 붙인다.
  • 마무리에서 Tailwind 의 scroll-snap 유틸리티, caniuse, CSS 명세 초안 등 참고 링크를 남긴다.
왜 읽나캐러셀 하나 만들자고 라이브러리를 넣기 전에, CSS 두 속성으로 되는 범위를 확인하고 싶은 프론트엔드 개발자에게 맞다.
카카오 엔터테인먼트 FE
카카오 엔터테인먼트 FE 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 프론트엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    항공 프론트엔드 구축기 (7/10): 창구를 하나만 두었습니다

    여기어때 항공 서비스 프론트엔드가 웹과 앱 웹뷰 두 환경에서 동일한 함수 호출로 동작하는 앱 브릿지 추상화 레이어를 설계한 과정을 다룬다. iOS·안드로이드 규약 차이와 "웹에 존재하지 않는 브릿지를 어떻게 호출하나"라는 문제를 단일 추상화 층으로 해결한 구현 사례다.

    요약 이어보기
    #react#typescript#webview+2