한 줄 요약 —
scroll-snap은 스크롤이 아무 데서나 멈추지 않고 한 칸씩 딱 맞춰 멈추게 하는 CSS다. 스크롤이 일어나는 통(부모)에scroll-snap-type, 멈출 칸(자식)에scroll-snap-align만 주면 자바스크립트 없이 인스타 스토리처럼 한 화면씩 넘어가는 페이지가 된다. 여기에 지난 시간의sticky헤더를 얹으면 헤더는 위에 붙은 채 본문만 칸칸이 넘어간다.
학습 목표#
- 스크롤 통에
scroll-snap-type을 줘 "칸칸이 멈춤"을 켤 수 있다. - 각 칸에
scroll-snap-align으로 어디에 맞춰 멈출지 정할 수 있다. mandatory와proximity의 차이를 알고 상황에 맞게 고를 수 있다.sticky헤더와scroll-snap을 함께 써서 풀스크린 슬라이드를 만들 수 있다.overscroll-behavior로 스크롤이 바깥으로 새지 않게 막을 수 있다.
오늘의 비유 — 책장 넘기듯 한 페이지씩 딱딱 멈추는 감각#
잘 제본된 양장 사진첩을 떠올려 보자. 한 장을 넘기면 페이지가 스스로 탁 하고 펼쳐져 평평하게 눕는다. 반쯤 넘어가다 어정쩡하게 선 상태로 멈추는 일이 없다. 손을 떼면 언제나 한 페이지가 깔끔히 펼쳐져 있다.
반대로 싸구려 스프링 노트는 아무 각도에서나 멈춘다. 펼치다 손을 놓으면 비스듬히 선 채 그대로다. 일부러 끝까지 넘겨야 다음 장이 보인다. 웹 페이지의 기본 스크롤이 딱 이 스프링 노트다 — 어디서 멈추든 그 자리에 선다. scroll-snap은 이 페이지에 양장 사진첩의 감각을 입힌다. 손을 떼면 가장 가까운 칸으로 끌어당겨, 늘 한 화면이 딱 맞춰진 채로 멈추게 한다.
핵심 개념#
두 역할 — 멈추는 통과 멈출 칸#
scroll-snap은 항상 두 곳에 나눠 적는다. 스크롤이 일어나는 **통(부모)**은 "여기서는 칸칸이 멈춘다"를 선언하고(scroll-snap-type), 그 안의 칸(자식) 하나하나는 "나는 여기에 맞춰 멈춰라"를 선언한다(scroll-snap-align). 사진첩의 제본(전체 규칙)과 낱장(멈출 자리)의 관계와 같다.
.album { /* 스크롤이 일어나는 통 */
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
}
.page { /* 멈출 칸 하나하나 */
height: 100vh;
scroll-snap-align: start;
}
scroll-snap-type: y mandatory는 "세로(y)로 스크롤할 때 반드시 한 칸에 맞춰라"는 뜻이고, scroll-snap-align: start는 "칸의 시작(위쪽 모서리)을 통의 시작에 맞춰라"는 뜻이다. 둘 중 하나라도 빠지면 멈추지 않는다.
scroll-snap-type — 어느 축으로, 얼마나 강하게#
scroll-snap-type은 값 두 개를 받는다. 앞은 축(y 세로, x 가로, both 양쪽), 뒤는 강도(mandatory 또는 proximity)다. 강도 두 값의 차이가 이 회차의 핵심이다.
mandatory— 손을 떼면 반드시 가장 가까운 칸으로 끌어당겨 멈춘다. 어중간한 위치에서 놓아도 한 칸이 딱 펼쳐진다. 항상 한 페이지가 평평하게 눕는 양장 사진첩이다.proximity— 칸 근처에서 놓을 때만 맞춰 주고, 어중간한 자리에서 놓으면 그대로 둔다. 살짝 넘기다 놓으면 가까운 페이지로 맞지만, 중간쯤 펼쳐 두면 그대로 서 있는 느슨한 노트다.
풀스크린 슬라이드처럼 모든 칸이 한 화면 크기로 똑같으면 mandatory가 잘 맞는다. 반대로 칸마다 길이가 다르거나 한 화면보다 긴 글이 들어가면 proximity가 무난하다(이유는 흔한 실수 3번에서 짚는다).
scroll-snap-align — 칸의 어디를 기준선에 맞출까#
칸이 멈출 때 칸의 어느 부분을 통의 기준선에 맞출지는 scroll-snap-align이 정한다. start는 칸의 시작 모서리(세로면 위쪽), center는 칸의 한가운데, end는 끝 모서리를 통의 해당 모서리에 맞춘다. 한 화면을 꽉 채우는 세로 슬라이드라면 보통 start가 자연스럽다.
.page {
scroll-snap-align: start; /* 칸 위쪽을 통 위쪽에 맞춰 멈춘다 */
}
함께 따라하기 — 한 화면씩 멈추는 풀스크린 슬라이드#
인스타 스토리처럼 한 화면씩 딱딱 멈추는 세로 스크롤 페이지를 만들어 본다. 위에는 칸이 넘어가도 그대로 붙어 있는 sticky 머리띠를 얹는다.
<div class="album">
<header class="cover">한 화면 앨범</header>
<section class="page">첫 번째 화면</section>
<section class="page">두 번째 화면</section>
<section class="page">세 번째 화면</section>
</div>
.album {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
overscroll-behavior: contain; /* 마지막 칸을 넘겨도 바깥으로 안 샌다 */
}
.cover {
position: sticky;
top: 0;
padding: 12px 20px;
background: #1f2937;
color: #fff;
}
.page {
height: 100vh;
display: grid;
place-items: center;
scroll-snap-align: start;
font-size: 2rem;
}
저장하고 통 안에서 스크롤하면, 화면이 한 칸씩 딱딱 멈추며 넘어간다. 어중간한 위치에서 손을 떼도 가장 가까운 화면으로 끌려가 깔끔히 멈춘다. 맨 위 "한 화면 앨범" 머리띠는 칸이 넘어가도 위에 그대로 붙어 따라온다 — sticky가 스크롤 통(.album) 안에서 작동하기 때문이다. overscroll-behavior: contain을 줘서, 마지막 칸을 넘기려 해도 스크롤이 바깥 페이지로 새지 않는다.
흔한 실수 3가지#
1. 스크롤 통에 height를 안 줘서 scroll-snap이 작동하지 않는다#
scroll-snap-type을 분명히 줬는데 아무것도 안 멈추는 상황. 십중팔구 통에 스크롤 자체가 안 생긴 것이다.
/* (.album에 높이가 없어 스크롤이 생기지 않고, 그래서 멈출 일도 없습니다) */
.album {
scroll-snap-type: y mandatory;
}
.page {
height: 100vh;
scroll-snap-align: start;
}
스냅은 스크롤이 일어나는 통 안에서 벌어지는 일이다. 통에 height(예: 100vh)와 overflow가 있어야 내용이 통보다 길어져 스크롤이 생기고, 그제야 멈출 거리가 만들어진다. 통 높이가 없으면 내용이 그냥 아래로 늘어날 뿐 스크롤이 안 생긴다.
.album {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
}
별도의 통 없이 페이지 전체를 칸칸이 넘기고 싶다면, 래퍼를 만드는 대신 html에 scroll-snap-type을 주면 된다. 그때는 화면(뷰포트)이 곧 스크롤 통이라 따로 높이를 줄 필요가 없다.
2. sticky 부모에 overflow:hidden을 줘서 헤더가 안 붙는다#
지난 회차에서 본 함정이 여기서도 똑같이 나온다. 머리띠를 sticky로 맞게 짰는데 꼼짝도 안 한다면, 조상 어딘가에 overflow: hidden이 걸려 있는 경우가 많다.
/* (이 .wrap의 overflow:hidden이 안쪽 sticky 헤더를 묶어 버립니다) */
.wrap {
overflow: hidden;
}
.cover {
position: sticky;
top: 0;
}
sticky는 자기를 감싼 스크롤 통을 기준으로 붙는다. 그런데 위 실습처럼 스크롤이 일어나야 할 통은 overflow-y: scroll(또는 auto)이어야 하고, 거기에 무심코 overflow: hidden을 얹으면 붙을 기준이 엉킨다. 불필요한 overflow: hidden을 걷어내고, 스크롤이 필요한 통에만 overflow-y: scroll/auto를 남긴다.
3. 어디서나 mandatory를 써서 긴 내용이 잘려 읽기 불편해진다#
mandatory는 손을 떼는 순간 무조건 가까운 칸으로 끌어당긴다. 칸이 한 화면보다 길면, 중간을 읽으려 멈춰도 가까운 칸으로 끌려가 읽던 자리를 놓친다.
/* (긴 글 섹션인데 mandatory라, 중간을 읽으려 멈춰도 가까운 칸으로 끌려갑니다) */
.feed {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
}
칸 길이가 제각각이거나 한 화면을 넘는 내용이 섞여 있으면 proximity로 바꾼다. 그러면 사용자가 멈춘 자리를 존중하고, 칸 가까이에서 놓았을 때만 살짝 맞춰 준다.
.feed {
scroll-snap-type: y proximity; /* 칸 근처에서 놓을 때만 맞춘다 */
}
정리하면 — 모든 칸이 한 화면으로 똑같은 슬라이드면 mandatory, 길이가 들쭉날쭉한 콘텐츠면 proximity다.
오늘 배운 것 체크리스트#
- 스크롤 통에
scroll-snap-type, 각 칸에scroll-snap-align을 준다. - 스크롤 통에는
height와overflow가 있어야 스냅이 작동한다. -
mandatory는 항상 끌어당기고,proximity는 가까울 때만 맞춘다. -
sticky헤더는overflow: hidden조상이 없어야 붙는다. -
overscroll-behavior: contain으로 스크롤이 바깥으로 새는 걸 막는다.
자주 묻는 질문#
Q. scroll-snap이 작동하지 않아요. 무엇부터 봐야 하나요?
A. 두 가지를 확인하세요. 첫째, 스크롤 통(부모)에 height와 overflow가 있어 실제로 스크롤이 생기는지 — 통 높이가 없으면 스크롤이 안 생겨 멈출 일도 없습니다. 둘째, 각 칸에 scroll-snap-align이 있는지 — scroll-snap-type은 부모에, scroll-snap-align은 자식에 줘야 하고, 둘 중 하나만 있으면 멈추지 않습니다.
Q. mandatory와 proximity 중 뭘 써야 하나요?
A. 모든 칸이 한 화면 크기로 똑같은 풀스크린 슬라이드라면 mandatory가 좋습니다 — 손을 떼면 늘 한 칸이 딱 맞춰집니다. 칸마다 길이가 다르거나 한 화면보다 긴 글이 들어가면 proximity가 무난합니다 — 사용자가 읽다 멈춘 자리를 존중하고, 칸 가까이에서 놓을 때만 맞춰 줍니다.
Q. 가로로 넘기는 카드 슬라이더도 같은 방법인가요?
A. 네, 축만 바꾸면 됩니다. 통에 scroll-snap-type: x mandatory와 가로 스크롤(overflow-x: auto)을 주고, 칸들을 가로로 나열(예: display: flex)하면 좌우로 한 장씩 넘어가는 카드가 됩니다. 세로 앨범을 옆으로 눕힌 것과 같습니다.
다음 시간 예고#
내일은 의사 요소 — ::before와 ::after, content 속성의 진짜 쓸모를 다룬다. HTML에 태그를 더 쓰지 않고도 본문 앞뒤에 장식을 자동으로 붙이는 방법과, content 속성이 없으면 왜 아무것도 안 보이는지를 짚는다.