
스크롤을 내려도 화면은 제자리에 멈춰 있고, 안의 내용만 하나씩 바뀌는 효과가 있습니다. 앞 글에서 다룬 책 넘김과 같은 페이지에 들어간 두 번째 효과입니다. 원리는 생각보다 단순합니다.
긴 영역을 만들고 화면을 붙잡는다
이 효과는 두 겹으로 되어 있습니다. 바깥에 아주 긴 영역을 만들고, 그 안에 화면 높이만큼의 칸을 하나 넣어 position: sticky로 붙잡습니다.
.story { height: 470svh; /* 스크롤 거리를 만드는 영역. 높이가 곧 슬라이드 개수 */ position: relative;}.story-sticky { position: sticky; top: 0; /* 화면 맨 위에 붙어서 안 움직인다 */ height: 100svh; overflow: hidden;}바깥 영역의 높이가 곧 슬라이드 개수입니다. 470svh면 화면 높이의 약 4.7배만큼 스크롤하는 동안 내용이 바뀝니다. 슬라이드를 늘리려면 이 숫자만 키우면 됩니다.
svh 단위를 쓴 이유
예시의 470svh와 100svh에 쓰인 svh는 “작은 뷰포트” 기준 높이입니다. MDN 문서는 모바일에서 주소창과 도구 막대가 펼쳐졌다 접히기 때문에 화면 크기가 달라진다고 설명하고, 세 가지 단위의 차이를 다음과 같이 구분합니다.
| 단위 | 기준 | 특징(MDN 문서) |
|---|---|---|
svh | 작은 뷰포트 | 브라우저 도구 막대가 펼쳐진 가장 작은 화면 기준. 값이 안정적이라 내용이 도구 막대에 가려지지 않습니다. 대신 도구 막대가 접히면 요소 주변에 여백이 생길 수 있습니다. |
lvh | 큰 뷰포트 | 도구 막대가 접힌 가장 큰 화면 기준. 도구 막대가 펼쳐지면 내용 일부가 가려질 수 있습니다. |
dvh | 동적 뷰포트 | 도구 막대 상태에 맞춰 계속 바뀜. 스크롤하는 동안 크기가 변해 화면이 흔들리고 성능에 부담이 될 수 있다고 경고합니다. |
vh | 기본 뷰포트 | 브라우저가 정하며, MDN은 vh가 lvh와 같다고 설명합니다. |
스크롤하는 동안 높이가 흔들리면 슬라이드 경계 계산이 어긋나기 쉬우므로, 값이 고정되는 svh를 고른 이 글의 선택은 문서의 설명과 맞습니다. 슬라이드 개수만큼 높이를 키울 때는 “화면 높이의 몇 배”를 이 작은 화면 기준으로 센다는 점을 기억해 두세요.

sticky가 붙는 조건
MDN 문서에 따르면 position: sticky 요소는 먼저 평소 위치에 놓였다가, 가장 가까운 스크롤 조상과 부모 칸을 기준으로 움직임이 정해집니다. 그래서 코드가 맞는데도 안 붙는다면 아래 표의 조건부터 확인하는 것이 빠릅니다.
| 조건 | MDN 문서에 적힌 내용 | 이 효과에서 뜻하는 것 |
|---|---|---|
| 기준값이 있어야 함 | top·bottom 같은 값이 하나도 없으면 해당 방향에서는 relative처럼 움직입니다. | top: 0이 빠지면 화면에 붙지 않습니다. |
| 붙는 시점 | 기준값에 닿기 전까지는 평소처럼 움직이다가, 닿는 순간부터 그 간격에 고정됩니다. | 바깥 영역이 화면 맨 위에 닿을 때 안쪽 칸이 붙기 시작합니다. |
| 부모 칸을 벗어나지 못함 | 붙어 있는 요소는 부모 칸의 반대쪽 끝에 닿으면 더 이상 따라가지 못합니다. | 바깥 영역이 끝나면 안쪽 칸도 함께 사라지며 다음 내용으로 넘어갑니다. |
| 조상의 overflow | overflow가 hidden·scroll·auto인 조상이 있으면 그 조상에 붙고, 그 조상이 스크롤되지 않으면 붙지 않습니다. | 바깥 영역 위쪽 어딘가에 overflow: hidden이 걸려 있는지 먼저 봅니다. |
같은 문서는 스크롤하는 고정·sticky 요소가 화면을 다시 그리는 부담을 줄 수 있고, will-change: transform이 도움이 될 수 있다고도 적습니다. 다만 이 글의 예시는 그 설정을 쓰지 않았으므로 필요할 때만 검토하시면 됩니다.
스크롤한 거리를 순서로 바꾼다
화면이 붙잡혀 있는 동안 지금 몇 번째 슬라이드인지를 계산해야 합니다. 스크롤한 거리를 0에서 1 사이 값으로 바꾸고, 거기에 슬라이드 개수를 곱합니다.
const travel = clamp01((progress - coverEnd) / (1 - coverEnd));const journey = travel * (slides.length - 1);const current = Math.floor(journey); // 지금 슬라이드const next = Math.min(slides.length - 1, current + 1);const phase = journey - current; // 이 슬라이드 안에서의 진행도// 앞뒤로 여백을 둬서 딱 붙지 않게 한다const turn = smoothstep((phase - 0.06) / 0.88);// 절반을 넘어가야 다음 것을 '현재'로 친다const active = turn < 0.52 ? current : next;여기서 0.52라는 숫자가 중요합니다. 정확히 0.5로 두면 경계에서 슬라이드가 덜덜 떨립니다. 살짝 넘겨 두면 한 번 바뀐 뒤에는 잘 안 돌아옵니다.
smoothstep은 시작과 끝을 부드럽게 만드는 계산입니다. 이게 없으면 바뀌는 순간이 뚝 끊깁니다.
바뀔 때 애니메이션을 붙인다
내용이 바뀌는 순간에 짧은 전환을 넣었습니다. 방법은 간단합니다. 같은 칸에 겹쳐 두고 key를 바꿔 요소를 새로 만들면, 새로 생긴 요소에 애니메이션이 자동으로 붙습니다.
@keyframes copy-swap { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; }}.lesson-copy { animation: copy-swap .3s cubic-bezier(.2,.7,.2,1) both; grid-area: 1 / 1; /* 같은 칸에 겹친다 */}MDN 문서에 따르면 @keyframes의 from은 0%, to는 100%와 같은 뜻이고, 시작이나 끝 상태를 적지 않으면 브라우저가 요소의 현재 스타일을 그 자리에 씁니다. 위 예시에서 to의 transform: none이 “원래 자리로 돌아온다”는 뜻으로 읽히는 이유입니다.

진행 표시도 같이 만들었다
지금 몇 번째인지 보여 주는 점을 아래에 뒀습니다. 현재 위치만 가로로 길어지게 했습니다. 점 다섯 개가 같은 모양이면 어디쯤인지 알기 어렵습니다.
움직임을 줄이는 사용자를 위한 설정
스크롤에 맞춰 내용이 바뀌고 움직이는 효과는 어떤 사람에게는 불편할 수 있습니다. MDN 문서는 prefers-reduced-motion 미디어 기능으로 사용자가 기기에서 “움직임 줄이기”를 켰는지 알 수 있고, 값이 reduce이면 애니메이션을 줄이거나 다른 방식으로 바꿀 수 있다고 안내합니다. 문서는 확대·이동하는 큰 움직임이 전정 기관 질환이 있는 사람에게 불편을 줄 수 있다고 설명합니다.
@media (prefers-reduced-motion: reduce) {
.lesson-copy {
animation: none; /* 움직임 줄이기를 켠 사용자에게는 전환을 끈다 */
}
}
Microsoft Edge 개발자 문서도 같은 방식으로 reduce일 때 애니메이션을 끄는 예시를 보여 줍니다. 운영체제 설정을 바꾸지 않고 시험하려면 개발자 도구를 열고 명령 메뉴(Windows는 Ctrl+Shift+P, macOS는 Command+Shift+P)에서 Emulate CSS prefers-reduced-motion을 고른 뒤 페이지를 새로고침해 애니메이션이 멈추는지 보면 됩니다.
| 증상 | 먼저 볼 곳 | 근거 |
|---|---|---|
| 화면이 안 붙고 같이 스크롤됨 | top 값이 있는지, 위쪽 조상에 overflow: hidden이 있는지 | MDN position 문서 |
| 모바일에서 아래가 주소창에 가림 | 높이를 vh 대신 svh로 지정 | MDN 길이 단위 문서 |
| 슬라이드 수를 바꿨는데 끝이 어긋남 | 바깥 영역 높이(470svh)를 개수에 맞게 수정 | 이 글의 한계 항목 |
| 움직임이 불편하다는 사용자가 있음 | prefers-reduced-motion: reduce에서 전환 끄기 | MDN, Microsoft Learn |
한계
스크롤 거리를 높이로 고정해 뒀습니다. 슬라이드 개수가 바뀌면 CSS의 470svh도 같이 고쳐야 합니다. 지금은 개수가 잘 안 바뀌어서 그대로 뒀지만, 자주 바뀐다면 자바스크립트로 계산해 넣는 게 맞습니다.
그리고 스크롤 방향을 가로채지는 않습니다. 사용자가 빠르게 훑어 내리면 중간 슬라이드는 그냥 지나갑니다. 이건 일부러 그렇게 뒀습니다. 스크롤을 붙잡는 페이지는 답답하다는 인상을 주기 쉽습니다.
확인 자료
- MDN: position
- MDN: <length> (뷰포트 단위 svh·lvh·dvh)
- MDN: @keyframes
- MDN: prefers-reduced-motion
- Microsoft Edge 개발자 문서: Simulate reduced motion
확인 날짜: 2026-10-18