Scroll snap/Using scroll snap events

김동현·2026년 3월 21일

mdn 학습 번역 - CSS

목록 보기
144/190

안녕하세요! 스크롤 기반 애니메이션에 이어 이번에는 '스크롤 스냅 이벤트(Scroll snap events)' 문서를 가져오셨군요!

요즘 모바일 웹이나 앱(예: 인스타그램 릴스, 틱톡, 넷플릭스 메인 화면)을 쓰다 보면, 화면을 대충 휙 쓸어넘겨도 다음 콘텐츠가 화면 중앙에 '딱!' 하고 예쁘게 맞아떨어지는 걸 보셨을 거예요. 그게 바로 CSS의 '스크롤 스냅(Scroll Snap)' 기능이랍니다. 이 문서에서는 스크롤 스냅이 일어나는 순간을 자바스크립트로 감지해서 화려한 효과를 더해주는 최신 이벤트들에 대해 다루고 있어요.

오늘도 원본 문서의 모든 내용을 빠짐없이, 제 실무 경험을 듬뿍 담아 알기 쉬운 구어체로 번역해 드릴게요. 자, 시작해 볼까요?


스크롤 스냅 이벤트 사용하기 (Using scroll snap events)

CSS 스크롤 스냅 (CSS scroll snap) 모듈은 두 가지의 스크롤 스냅 이벤트를 정의합니다: 바로 scrollsnapchangingscrollsnapchange 입니다. 이 이벤트들은 브라우저가 새로운 스크롤 스냅 대상(scroll snap targets)을 '선택할 예정(pending)'이거나 '선택 완료(selected)'했다고 판단할 때 자바스크립트를 실행할 수 있게 해줍니다.

이 가이드에서는 완전한 예제들과 함께 이 이벤트들에 대한 전반적인 내용을 제공합니다.

💡 강사의 실무 팁 1
예전에는 사용자가 스크롤을 멈췄을 때 "지금 화면에 어떤 요소가 보이지?"를 알아내기 위해 IntersectionObserver를 쓰거나, 스크롤 이벤트를 복잡하게 계산해야 했어요. 하지만 이제 이 두 이벤트가 생기면서, "어느 요소에 스냅(고정)될 예정인지", "어느 요소에 최종적으로 스냅(고정)되었는지"를 아주 직관적으로 잡아낼 수 있게 되었습니다!


이 문서의 목차 (In this article)


이벤트 개요 (Events overview)

스크롤 스냅 이벤트들은 잠재적인 스크롤 스냅 대상(요소)들을 포함하고 있는 스크롤 컨테이너 (scrolling container)에 설정됩니다:

  • scrollsnapchanging 이벤트는 현재 진행 중인 스크롤 제스처가 끝났을 때 새로운 스크롤 스냅 대상이 선택될 것이라고 브라우저가 판단할 때 발생(fire)합니다. 이것이 바로 대기 중인(pending) 스크롤 스냅 대상입니다. 구체적으로 말하자면, 이 이벤트는 스크롤 제스처를 하는 동안 사용자가 잠재적인 새로운 스냅 대상들 위를 지나갈 때마다 발생합니다. 한 번의 스크롤 제스처 동안 scrollsnapchanging 이벤트가 여러 번 발생할 수는 있지만, 여러 타겟을 지나쳐 간다고 해서 모든 잠재적 타겟에 대해 다 발생하는 것은 아닙니다. 오히려, 스냅이 최종적으로 머무르게 될 가능성이 가장 높은 마지막 타겟에 대해서만 발생합니다.
  • scrollsnapchange 이벤트는 스크롤 조작이 끝나고 새로운 스크롤 스냅 대상이 최종적으로 선택되었을 때 발생합니다. 구체적으로 이 이벤트는 스크롤 제스처가 완료되었을 때 발생하지만, 새로운 스냅 대상이 선택된 경우에만 발생합니다. 이 이벤트는 scrollend 이벤트가 발생하기 직전에 발생합니다.

이 두 가지 이벤트가 어떻게 작동하는지 보여주는 예제를 살펴보겠습니다 (이 예제가 어떻게 만들어졌는지는 이 문서의 후반부에서 설명해 드릴 거예요):

MDN Playground에서 실행하기 (Play)

박스 목록 위아래로 스크롤을 한번 해보세요:

  • 스크롤 제스처를 떼지 않은 상태에서(마우스를 누른 채로, 또는 모바일에서 손가락을 떼지 않고) 컨테이너를 위아래로 천천히 스크롤해 보세요. 박스 위를 지나갈 때마다 그 박스의 색상이 짙은 회색으로 변하고, 벗어나면 다시 원래대로 돌아오는 것을 볼 수 있을 겁니다. 이것이 바로 scrollsnapchanging 이벤트가 작동하는 모습입니다. (즉, "지금 손을 떼면 여기에 스냅될 거야!"라고 미리 알려주는 거죠.)
  • 이제 스크롤 제스처를 놓아보세요(마우스 버튼에서 손을 떼기). 그러면 스크롤 위치와 가장 가까운 박스가 부드럽게 보라색 배경과 흰색 글씨로 애니메이션 될 것입니다. 이 애니메이션은 scrollsnapchange 이벤트가 발생했을 때 일어납니다.
  • 마지막으로, 화면을 빠르게 휙! 하고 넘겨보세요(플릭 제스처). 최종적으로 고정될 타겟 근처에서 스크롤 속도가 느려지기 시작할 때쯤 단 한 번의 scrollsnapchanging 이벤트만 발생하는 것을 보실 수 있습니다. 그 이후에 scrollsnapchange 이벤트가 발생하며 최종 선택된 스냅 타겟이 보라색으로 변하게 됩니다.

SnapEvent 이벤트 객체 (The SnapEvent event object)

위에서 설명한 두 이벤트는 모두 SnapEvent 이벤트 객체를 공유합니다. 이 객체에는 스크롤 스냅 이벤트가 작동하는 방식에 핵심적인 역할을 하는 두 가지 프로퍼티(속성)가 있습니다:

  • snapTargetBlock: 이벤트가 발생했을 때 블록 방향 (block direction)(일반적으로 수직 방향)으로 스냅된 요소에 대한 참조(reference)를 반환합니다. 만약 스크롤 스냅이 인라인(수평) 방향으로만 일어난다면 블록 방향으로는 스냅된 요소가 없으므로 null을 반환합니다.
  • snapTargetInline: 이벤트가 발생했을 때 인라인 방향 (inline direction)(일반적으로 수평 방향)으로 스냅된 요소에 대한 참조를 반환합니다. 블록 방향으로만 스크롤 스냅이 일어난다면 null을 반환합니다.

이 프로퍼티들을 활용하면, 이벤트 핸들러 함수에서 (1차원이든 2차원이든 상관없이) 현재 스냅이 '완료된' 요소(scrollsnapchange의 경우)나, 지금 스크롤을 멈춘다면 스냅이 '될 예정인' 요소(scrollsnapchanging의 경우)를 바로 알아낼 수 있습니다. 이렇게 찾아낸 요소들을 여러분 마음대로 조작할 수 있죠. 예를 들어 요소의 style 프로퍼티를 통해 직접 스타일을 변경하거나, 스타일시트에 미리 정의해둔 클래스를 요소에 추가/제거하는 방식으로 말입니다.

CSS scroll-snap-type과의 관계 (Relationship with CSS scroll-snap-type)

SnapEvent에서 사용할 수 있는 프로퍼티 값들은 스크롤 컨테이너에 설정된 CSS scroll-snap-type 속성의 값과 직접적으로 연결됩니다:

  • 스냅 축(snap axis)이 block (또는 현재 쓰기 모드에서 block과 동일한 물리적 축 값)으로 지정된 경우, snapTargetBlock만 요소의 참조를 반환합니다.
  • 스냅 축이 inline (또는 현재 쓰기 모드에서 inline과 동일한 물리적 축 값)으로 지정된 경우, snapTargetInline만 요소의 참조를 반환합니다.
  • 스냅 축이 both (양방향 모두)로 지정된 경우, snapTargetBlocksnapTargetInline 둘 다 요소의 참조를 반환합니다.

💡 강사의 실무 팁 2
우리나라처럼 글을 가로로 읽는(왼쪽에서 오른쪽, 위에서 아래) 문화권에서는 block수직(상하) 방향, inline수평(좌우) 방향이라고 생각하시면 편합니다.

1차원 스크롤러 다루기 (Handling one-dimensional scrollers)

수평 스크롤러(가로 스크롤)를 다루고 있고 콘텐츠가 수평 쓰기 모드(writing-mode)라면, 스냅된 요소가 바뀔 때 이벤트 객체의 snapTargetInline 프로퍼티만 변경됩니다. (콘텐츠가 수직 쓰기 모드라면 snapTargetBlock이 변경됩니다.)

반대로 수직 스크롤러(세로 스크롤)를 다루고 있고 콘텐츠가 수평 쓰기 모드라면, 스냅된 요소가 바뀔 때 snapTargetBlock 프로퍼티만 변경됩니다. (수직 쓰기 모드라면 snapTargetInline이 변경되겠죠.)

두 경우 모두, 값이 변하지 않는 나머지 하나의 프로퍼티는 항상 null을 반환합니다.

1차원 스크롤 스냅 이벤트 핸들러 함수의 전형적인 예시 코드를 살펴봅시다:

scrollingElem.addEventListener("scrollsnapchange", (event) => {
  event.snapTargetBlock.className = "select-section";
});

이 코드 스니펫에서는 스냅 타겟들이 들어있는 블록 방향(수직) 스크롤 컨테이너 요소에 scrollsnapchange 핸들러 함수를 설정했습니다. 이벤트가 발생하면, 우리는 snapTargetBlock이 가리키는 요소에 select-section이라는 클래스를 부여합니다. 이 클래스를 통해 방금 새롭게 선택된 스냅 대상이 마치 '선택된' 것처럼 보이게끔 (예: 애니메이션을 넣어서) 스타일링할 수 있습니다.

2차원 스크롤러 다루기 (Handling two-dimensional scrollers)

가로 그리고 세로 스크롤이 모두 가능한 스크롤러를 다룬다면 코드가 좀 더 복잡해집니다. 왜냐하면 snapTargetBlock 프로퍼티 snapTargetInline 프로퍼티 값 모두 어떤 요소에 대한 참조를 반환하기 때문입니다(둘 다 null이 아님). 그리고 어떤 방향으로 스크롤하느냐, 콘텐츠의 writing-mode가 무엇이냐에 따라 둘 중 하나의 값이 변경될 것입니다:

  • 스크롤러를 가로로 스크롤하면, 수평 쓰기 모드에서는 snapTargetInline 값이 변경되고, 수직 쓰기 모드에서는 snapTargetBlock 값이 변경됩니다.
  • 스크롤러를 세로로 스크롤하면, 수평 쓰기 모드에서는 snapTargetBlock 값이 변경되고, 수직 쓰기 모드에서는 snapTargetInline 값이 변경됩니다.

이를 처리하기 위해, 여러분은 아마도 snapTargetBlock이 변경된 것인지 아니면 snapTargetInline 요소가 변경된 것인지 그 상태를 추적해야 할 것입니다. 예제를 보겠습니다:

const prevState = {
  snapTargetInline: "s1",
  snapTargetBlock: "s1",
};

scrollingElem.addEventListener("scrollsnapchange", (event) => {
  if (!(prevState.snapTargetBlock === event.snapTargetBlock.id)) {
    console.log(
      `컨테이너가 블록(수직) 방향으로 스크롤되어 요소 ${event.snapTargetBlock.id} 에 스냅되었습니다.`
    );
  }

  if (!(prevState.snapTargetInline === event.snapTargetInline.id)) {
    console.log(
      `컨테이너가 인라인(수평) 방향으로 스크롤되어 요소 ${event.snapTargetBlock.id} 에 스냅되었습니다.`
    );
  }

  prevState.snapTargetBlock = event.snapTargetBlock.id;
  prevState.snapTargetInline = event.snapTargetInline.id;
});

이 코드에서는 가장 먼저 prevState라는 객체를 정의하여, 이전에 스냅되었던 snapTargetBlocksnapTargetInline 요소들의 ID를 저장해둡니다.

이벤트 핸들러 함수 안에서는 if 문을 사용해 다음을 검사합니다:

  • prevState.snapTargetBlock에 저장된 ID와 현재 이벤트의 event.snapTargetBlock.id가 같은가?
  • prevState.snapTargetInline에 저장된 ID와 현재 이벤트의 event.snapTargetInline.id가 같은가?

만약 값이 서로 다르다면, 이는 스크롤러가 해당 방향(블록 또는 인라인)으로 스크롤되었다는 뜻입니다. 따라서 이를 알리기 위해 콘솔에 메시지를 기록합니다. 실제 프로젝트라면 여기서 해당 요소가 선택되었음을 시각적으로 알리기 위해 스타일을 추가하는 로직이 들어가겠죠.

마지막으로 다음번 이벤트가 발생할 때를 대비하여 prevState.snapTargetBlockprevState.snapTargetInline의 값을 지금 방금 스냅된 타겟의 ID로 업데이트해 줍니다.

이 문서의 나머지 부분에서는 완전하게 작동하는 스크롤 스냅 이벤트 예제 두 가지를 살펴보겠습니다. 각 섹션 끝에 있는 라이브 렌더링 버전에서 직접 만져보실 수 있습니다.


1차원 스크롤러 예제 (One-dimensional scroller example)

이 예제는 여러 개의 밝은 회색 <section> 요소들을 담고 있는 수직 스크롤 <main> 요소로 구성되어 있습니다. 이 <section>들은 모두 스크롤 스냅 타겟입니다. 새로운 스냅 타겟이 대기 중(pending)일 때는 짙은 회색으로 변합니다. 그리고 스냅 타겟이 최종 선택되면 부드럽게 보라색 배경에 흰색 텍스트로 애니메이션이 일어납니다. 만약 다른 스냅 타겟이 선택되어 선택이 해제되면, 다시 부드럽게 회색 배경에 검은색 텍스트로 돌아옵니다.

HTML

예제의 HTML은 단 하나의 <main> 요소만 있습니다. 페이지 공간을 아끼기 위해 안에 들어갈 <section> 요소들은 나중에 자바스크립트로 동적으로 추가할 것입니다.

MDN Playground에서 실행하기 (Play)

<main></main>

CSS

CSS 코드를 보면, 먼저 <main> 요소에 두꺼운 검은색 테두리(border)와 고정된 너비(width), 높이(height)를 주었습니다. 내용이 넘쳐흐를 경우 숨기고 스크롤을 활성화하기 위해 overflow 값을 scroll로 설정했습니다. 그리고 scroll-snap-typeblock mandatory로 설정하여, 스크롤할 때 블록 방향(수직)으로 무조건(mandatory) 스냅 타겟에 고정되도록 강제했습니다.

main {
  border: 3px solid black;
  width: 250px;
  height: 450px;
  overflow: scroll;
  scroll-snap-type: block mandatory;
}

<section> 요소에는 50px의 마진(margin)을 주어 서로 떨어지게 만들었고, 이렇게 하면 스크롤 스냅 동작이 눈에 훨씬 잘 보이게 됩니다. 그런 다음 scroll-snap-align 속성을 center로 주어서, 스크롤이 끝날 때 타겟의 중앙에 화면이 맞춰지도록 했습니다. 마지막으로, 스냅 타겟이 대기 중이거나 선택되었을 때 발생하는 스타일 변화가 자연스럽게 전환되도록 트랜지션(transition)을 추가했습니다.

section {
  margin: 50px auto;
  scroll-snap-align: center;
  transition: 0.5s ease;
}

위에서 말한 스타일 변화는 자바스크립트를 통해 <section> 요소들에 특정 클래스를 부여하는 방식으로 적용됩니다. select-section 클래스는 선택되었음을 나타내며 배경을 보라색으로, 글씨를 흰색으로 바꿉니다. pending 클래스는 곧 스냅될 예정(대기 중)임을 나타내며 배경을 짙은 회색으로 바꿉니다.

.pending {
  background-color: #cccccc;
}

.select-section {
  background: purple;
  color: white;
}

JavaScript

자바스크립트에서는 먼저 <main> 요소의 참조를 가져오고, 생성할 <section> 요소의 갯수(여기서는 21개)와 카운트를 시작할 변수 n을 선언합니다. 그리고 while 루프를 돌면서 <section> 요소를 하나씩 만들어 안에 Section과 숫자 n이 적힌 <h2> 태그를 넣어줍니다.

const mainElem = document.querySelector("main");
const sectionCount = 21;
let n = 1;

while (n <= sectionCount) {
  mainElem.innerHTML += `
    <section>
      <h2>Section ${n}</h2>
    </section>
  `;
  n++;
}

이제 scrollsnapchanging 이벤트 핸들러 함수를 살펴보겠습니다. <main> 요소의 자식(즉, 어떤 <section> 요소든)이 대기 중인(pending) 스냅 타겟으로 지정되면 다음을 수행합니다:

  1. 이전에 pending 클래스를 받았던 요소가 있는지 확인하고, 있다면 해당 클래스를 제거합니다. 이렇게 해야 현재 대기 중인 타겟 하나만 pending 클래스를 가지고 짙은 회색으로 렌더링되기 때문입니다. 더 이상 대기 중이 아닌 이전 타겟에 스타일이 남는 걸 막아줍니다.
  2. snapTargetBlock 프로퍼티가 가리키는 요소(새로 대기 중이 된 <section> 요소)에 pending 클래스를 추가하여 짙은 회색으로 만듭니다.
mainElem.addEventListener("scrollsnapchanging", (event) => {
  const previousPending = document.querySelector(".pending");
  if (previousPending) {
    previousPending.classList.remove("pending");
  }

  event.snapTargetBlock.classList.add("pending");
});

참고 (Note):
이 데모에서는 snapTargetInline 이벤트 객체 프로퍼티를 전혀 신경 쓸 필요가 없습니다. 오직 수직으로만 스크롤하고 있으며 수평 쓰기 모드를 사용하고 있기 때문에 snapTargetBlock 값만 변경될 테니까요. 이 경우 snapTargetInline은 항상 null을 반환합니다.

스크롤 제스처가 끝나고 <section> 요소가 실제로 스냅 타겟으로 확정(선택)되면, scrollsnapchange 이벤트 핸들러 함수가 실행됩니다. 이 함수는 다음을 수행합니다:

  1. 이전에 이미 선택되었던 스냅 타겟이 있는지 확인합니다 (즉, select-section 클래스를 가진 요소가 있는지 확인). 만약 있다면 클래스를 제거합니다.
  2. snapTargetBlock 프로퍼티에 참조된 방금 선택된 <section> 요소에 select-section 클래스를 적용하여, 선택되었음을 알리는 애니메이션(스타일)이 나타나게 합니다.
mainElem.addEventListener("scrollsnapchange", (event) => {
  const currentlySnapped = document.querySelector(".select-section");
  if (currentlySnapped) {
    currentlySnapped.classList.remove("select-section");
  }

  event.snapTargetBlock.classList.add("select-section");
});

결과 (Result)

직접 위아래로 스크롤 해보면서 앞서 설명한 동작들이 어떻게 작동하는지 확인해 보세요!

MDN Playground에서 실행하기 (Play)


2차원 스크롤러 예제 (Two-dimensional scroller example)

이 예제는 방금 본 예제와 아주 비슷하지만, 가로 그리고 세로로 스크롤 할 수 있는 <main> 요소 안에 여러 개의 스냅 타겟인 <section> 요소들을 배치했다는 점이 다릅니다.

HTML은 이전 예제와 똑같이 단일 <main> 요소로 시작합니다.

<main></main>

CSS

이 예제의 CSS도 이전과 매우 유사합니다. 하지만 눈여겨볼 만한 중요한 차이점들이 있습니다.

먼저 <main> 요소의 스타일을 보겠습니다. 우리는 <section> 요소들이 격자 모양(Grid)으로 배치되기를 원하기 때문에 CSS 그리드 레이아웃 (CSS grid layout)을 사용합니다. grid-template-columns 값을 repeat(7, 1fr)로 주어 7개의 열(Column)을 만듭니다. 또한 <section> 요소에 각각 마진을 주지 않고, <main> 요소에 직접 패딩(padding)과 gap을 설정하여 요소 사이의 여백을 조절했습니다.

마지막으로, 이번에는 양방향으로 스크롤하기 때문에 scroll-snap-type 속성을 both mandatory로 설정했습니다. 이렇게 하면 수직(block) 방향이든 수평(inline) 방향이든 스크롤을 놓으면 무조건 타겟에 고정되게 됩니다.

main {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  padding: 100px;
  gap: 50px;
  overflow: scroll;
  border: 3px solid black;
  width: 350px;
  height: 350px;

  scroll-snap-type: both mandatory;
}

다음으로, 이번 예제에서는 단순한 transition이 아닌 CSS 애니메이션(@keyframes)을 사용했습니다. 코드가 조금 더 길어지긴 하지만, 적용되는 애니메이션을 훨씬 정밀하게 제어할 수 있습니다.

먼저 스냅 타겟이 선택되었거나 대기 중임을 나타내기 위해 사용할 클래스들을 정의합니다. select-sectiondeselect-section 클래스는 선택 혹은 해제를 나타내는 키프레임 애니메이션을 실행시킵니다. pending 클래스는 대기 중인 타겟에 적용되어 (이전 예제처럼) 배경을 짙은 회색으로 만듭니다.

@keyframes를 보면 선택 시에는 회색 배경/검은 글씨에서 보라색 배경/흰색 글씨로, 선택 해제 시에는 다시 그 반대로 애니메이션 됩니다. 해제 애니메이션(deselect)은 선택 애니메이션과 다르게 opacity를 활용해 서서히 사라졌다가 나타나는 페이드 효과를 추가했습니다.

.select-section {
  animation: select 0.8s ease forwards;
}

.deselect-section {
  animation: deselect 0.8s ease forwards;
}

.pending {
  background-color: #cccccc;
}

@keyframes select {
  from {
    background: #eeeeee;
    color: black;
  }

  to {
    background: purple;
    color: white;
  }
}

@keyframes deselect {
  0% {
    background: purple;
    color: white;
    opacity: 1;
  }

  80% {
    background: #eeeeee;
    color: black;
    opacity: 0.1;
  }

  100% {
    background: #eeeeee;
    color: black;
    opacity: 1;
  }
}

JavaScript

자바스크립트 부분은 처음에는 이전 예제와 동일하게 시작합니다. 다만 이번에는 49개의 <section> 요소를 동적으로 생성하며, 나중에 추적하기 쉽도록 각각에 s라는 접두사와 숫자 n을 결합해 고유한 ID(id="s1", id="s2" 등)를 부여했습니다. 우리가 설정한 CSS 그리드에 의해, 이들은 7행 7열의 구조로 배치될 것입니다.

const mainElem = document.querySelector("main");
const sectionCount = 49;
let n = 1;

while (n <= sectionCount) {
  mainElem.innerHTML += `
    <section id="s${n}">
      <h2>Section ${n}</h2>
    </section>
  `;
  n++;
}

다음으로 prevState라는 객체를 만듭니다. 이 객체는 어느 시점에서든 이전에 선택되었던 스냅 타겟을 추적하기 위한 용도로, 수평(inline)과 수직(block) 방향의 이전 스냅 타겟 ID를 각각 저장합니다. 이벤트가 발생할 때마다 새로운 수평 타겟에 스타일을 줄지, 아니면 수직 타겟에 스타일을 줄지 판별하기 위해 이 상태 저장이 꼭 필요합니다.

const prevState = {
  snapTargetInline: "s1",
  snapTargetBlock: "s1",
};

예를 들어, 컨테이너가 스크롤되어 새로운 SnapEvent.snapTargetBlock 요소의 ID는 바뀌었지만(즉, prevState.snapTargetBlock과 다름), 새로운 SnapEvent.snapTargetInline 요소의 ID는 prevState.snapTargetInline과 같다고 해봅시다. 이는 우리가 수직(block) 방향으로는 새로운 타겟으로 스크롤했지만 수평(inline) 방향으로는 이동하지 않았음을 의미합니다. 따라서 SnapEvent.snapTargetBlock에만 스타일을 주고 SnapEvent.snapTargetInline에는 스타일을 주면 안 됩니다.

이번에는 scrollsnapchange 이벤트 핸들러 함수부터 먼저 설명해 보겠습니다. 이 함수 안에서는 다음을 수행합니다:

  1. 가장 먼저, 이전에 선택되었던 <section> 스냅 타겟(select-section 클래스를 가진 요소)에 deselect-section 클래스를 적용하여 '선택 해제' 애니메이션이 나오게 만듭니다. 만약 이전에 선택된 타겟이 아예 없었다면(페이지 첫 로드 시), DOM의 가장 첫 번째 <section>select-section을 적용해서 선택된 것처럼 보여줍니다.
  2. 수평(inline)과 수직(block) 양쪽 모두에 대해, 이전에 선택된 타겟의 ID와 새롭게 선택된 타겟의 ID를 비교합니다. 만약 값이 다르다면 선택된 대상이 변경되었다는 의미이므로, 알맞은 타겟 요소에 select-section 클래스를 적용하여 시각적으로 표시해 줍니다.
  3. 다음번 이벤트가 발생할 때 이전 상태로 사용하기 위해, prevState.snapTargetBlockprevState.snapTargetInline의 값을 방금 선택된 스냅 타겟들의 ID로 업데이트합니다.
mainElem.addEventListener("scrollsnapchange", (event) => {
  if (document.querySelector(".select-section")) {
    document.querySelector(".select-section").className = "deselect-section";
  } else {
    document.querySelector("section").className = "select-section";
  }

  if (!(prevState.snapTargetBlock === event.snapTargetBlock.id)) {
    event.snapTargetBlock.className = "select-section";
  }

  if (!(prevState.snapTargetInline === event.snapTargetInline.id)) {
    event.snapTargetInline.className = "select-section";
  }

  prevState.snapTargetBlock = event.snapTargetBlock.id;
  prevState.snapTargetInline = event.snapTargetInline.id;
});

scrollsnapchanging 핸들러 함수가 작동할 때는 다음을 수행합니다:

  1. 이전에 pending 클래스를 가지고 있던 요소에서 해당 클래스를 지워, 현재 대기 중인 하나의 타겟만 짙은 회색으로 칠해지도록 만듭니다.
  2. 현재 대기 중인 요소에 pending 클래스를 줍니다. 단, 이미 select-section 클래스를 가지고 있지 않은 경우에만 적용합니다. 우리는 이전에 선택되었던 타겟이 새로운 타겟으로 최종 변경되기 전까지는 보라색(선택됨) 상태를 계속 유지하기를 바라기 때문입니다. 또한 if 문에 추가적인 검증을 넣어서 수평 타겟과 수직 타겟 중 실제로 변경된 타겟에만 스타일을 적용하도록 만듭니다. 이번에도 이전 타겟과 현재 타겟의 ID를 비교해서 체크하죠.
mainElem.addEventListener("scrollsnapchanging", (event) => {
  const previousPending = document.querySelector(".pending");
  if (previousPending) {
    previousPending.className = "";
  }

  if (
    !(event.snapTargetBlock.className === "select-section") &&
    !(prevState.snapTargetBlock === event.snapTargetBlock.id)
  ) {
    event.snapTargetBlock.className = "pending";
  }

  if (
    !(event.snapTargetInline.className === "select-section") &&
    !(prevState.snapTargetInline === event.snapTargetInline.id)
  ) {
    event.snapTargetInline.className = "pending";
  }
});

결과 (Result)

직접 컨테이너를 가로와 세로로 이리저리 스크롤 해보면서 위에서 설명한 복잡한 제어 로직이 어떻게 눈앞에 그려지는지 관찰해 보세요!

MDN Playground에서 실행하기 (Play)


DocumentWindow에서의 스크롤 스냅 이벤트 (Scroll snap events on Document and Window)

이 문서에서는 지금까지 Element 인터페이스(DOM 요소)에서 발생하는 스크롤 스냅 이벤트들을 살펴봤습니다. 하지만 완전히 동일한 이벤트들이 Document 객체와 Window 객체에서도 발생합니다. 다음 레퍼런스를 참조하세요:

이벤트 객체의 동작 자체는 Element에서의 동작과 똑같습니다. 단지 차이점이라면, 전체 HTML 문서 자체가 스크롤 스냅 컨테이너가 되어야 한다는 것뿐이죠. (즉, scroll-snap-type 속성이 <html> 요소에 설정되어 있어야 합니다.)

예를 들어, 방금까지 봤던 예제와 비슷하게 주요 콘텐츠들이 꽉 찬 <main> 태그가 있다고 가정해 봅시다:

<main>
  </main>

CSS를 통해 이 <main> 요소를 스크롤 컨테이너로 만들 수 있겠죠. 이렇게 말입니다:

main {
  width: 250px;
  height: 450px;
  overflow: scroll;
}

이제 이 스크롤되는 콘텐츠에 스크롤 스냅 동작을 심어주기 위해 <html> 요소에 scroll-snap-type 속성을 지정합니다:

html {
  scroll-snap-type: block mandatory;
}

이렇게 세팅해 두면, 아래의 자바스크립트 코드처럼 <main> 요소의 자식들 중 새로운 스냅 타겟이 선택되었을 때 HTML 문서(document) 전체에 scrollsnapchange 이벤트가 발생하게 됩니다. 핸들러 함수 내부에서 SnapEvent.snapTargetBlock이 참조하는 자식 요소에 selected 클래스를 부여하여 애니메이션 같은 스타일을 넣을 수 있습니다.

document.addEventListener("scrollsnapchange", (event) => {
  event.snapTargetBlock.classList.add("selected");
});

물론 똑같은 기능을 만들기 위해 window 객체에 이벤트를 달아도 됩니다:

window.addEventListener("scrollsnapchange", (event) => {
  event.snapTargetBlock.classList.add("selected");
});

💡 강사의 실무 팁 3
주로 풀페이지 스크롤(Full-page scroll, '원페이지 스크롤'이라고도 부르죠!) 웹사이트를 만들 때 html이나 body 자체에 스크롤 스냅을 주고, window 이벤트로 타겟을 감지해서 사이드 네비게이션 점(dot) 색깔을 바꿔주는 등의 기능을 구현할 때 이 방식이 아주아주 유용하게 쓰입니다.


함께 보기 (See also)


MDN 향상에 도움 주기 (Help improve MDN)


지금까지 CSS 스크롤 스냅과 최신 자바스크립트 이벤트를 결합하는 완벽한 방법을 살펴보셨습니다! 취업용 포트폴리오를 만드실 때 이 기능들을 활용해서 넷플릭스 스타일의 가로 슬라이더나 애플 스타일의 부드러운 스크롤 인터랙션을 만들어보세요. 최신 API를 실무에 잘 활용하는 센스 있는 신입 프론트엔드 개발자로 주목받으실 수 있을 거예요.

추가로 코드가 헷갈리거나 궁금한 점이 생기면 언제든 질문 남겨주세요!

profile
프론트에_가까운_풀스택_개발자

0개의 댓글