[chrome] CSS Scroll Snap #1

개요top

크롬69부터 적용되는 CSS Scroll Snap은 웹 상에서 갤러리나 커버앨범, 전체화면을 차지하는 페이징 스타일의 홈페이지를 구현할 때 일일히 자바스크립트로 스크롤을 제어하던 코드를 제거하고 간단히 CSS설정을 통해 이를 구현할 수 있게 해줍니다.
기존 방식의 개발에서 많은 버그를 유발하고 성능 문제도 많이 일으키던 스크롤 제어에 대해 훨씬 안전하고 성능 좋게 구현할 수 있습니다.

CSS Scroll Snap Module Level 1top

이 사양은 제법 역사와 전통이 있습니다. 우선 과거의 역사를 돌이켜 보면 다음과 같습니다.

엣지와 파이어폭스에서 먼저 특정 포인트를 기반으로 하는 스크롤제어 기능을 CSS에 반영했었는데 이후 이를 좀 더 개선하고 사용하기 쉽게 정리한 사양이 CSS Scroll Snap입니다.

이 사양은 https://drafts.csswg.org/css-scroll-snap/로 제안 되어있습니다(…만 크롬이 언제나 그렇든 draft제출은 요식행위에 불과하고 이미 크롬에 구현부터 하고 봅니다 ^^)

2016년 처음 제안되어 해마다 개정을 거쳐 현재는 2018년도 버전으로 수정되어있습니다.

이 글을 쓰는 시점의 크롬은 68버전으로, 이 기능이 반영되는 시점은 69버전이므로 카나리아를 설치해야만 테스트해볼 수 있습니다(모바일 동일) 하지만 69버전이 런칭된 이후에는 평범하게 사용하실 수 있을 것입니다.

어쨌든 구현하려는 사양은 간단히 이미지화 해보면 다음과 같습니다.

스크롤이 일어나다가 적당한 위치에 스냅 즉 달라붙어서 정지하게 하려는 거죠. 과거에는 이를 구현하기 위해 많은 자바스크립트 코드나 라이브러리를 필요로 했습니다. 특히 컨텐츠의 크기가 일정치 않다거나 스크롤 영역 안에 별도의 고정된 공간이 있다던가 하면 더욱 어려워집니다. 애니메이션 처리나 모바일에서의 네이티브 스크롤 제어와의 충돌 처리 등을 포함하면 다양한 경우에 안정적인 스크롤 스냅을 구현하는 난이도는 엄청나게 올라갑니다. 이제 이를 손쉽게 CSS로 처리할 수 있게 된 것입니다.

Scroll Containertop

우선 첫 번째로 익힐 개념은 스크롤 컨테이너의 개념입니다. 스크롤 스냅은 결국 스크롤 컨테이너에 있는 엘리먼트를 적당히 스크롤 시켜 바른 위치에 보이게 하는 것입니다. 우선 엘리먼트를 담을 스크롤 컨테이너가 있어야겠죠.

하지만 스크롤 컨테이너의 개념은 굉장히 복잡합니다. 스크롤바는 일반적으로 overflow상황에서 등장하는데 그럴려면 어떤 경우가 overflow고 그 overflow에서 scroll을 설정할 수 있는 상황은 무엇인가에 대한 이해가 필요합니다.

css-overflow사양은 현재도 draft로 계속 사양이 업데이트 되고 있습니다. CSS 2때부터 존재하던 속성이 왜 draft인가 싶기도 하시겠지만 text-overflow나 max-lines, transform등의 다양한 비쥬얼 처리가 등장하면서 overflow의 계산방법도 다양해져 ink overflow, scrollable overflow는 물론이고 text-overflow의 경우에는 clip이나 ellipsis 등도 등장하게 됩니다.

이 포스트는 scroll시스템에 대한 이해를 목적으로 하고 있지 않기 때문에 overflow, overflow-x, overflow-y, overflow-block, overflow-inline등에 scroll 값이 지정된 경우를 간단히 스크롤 컨테이너로 보겠습니다.

특정 스크롤 컨테이너를 스크롤 스냅이 가능하게 만들려면 다음의 속성을 추가합니다.

<style>
.scroll-container{
  overflow-x:scroll;
  scroll-snap-type:A B
}
</style>
<section class="scroll-container"></section>

이제 scroll-snap-type에 지정되는 A와 B값을 소상히 알아보죠.

Scroll Snap Axis(A)

우선 A에 올 수 있는 값은 스크롤 스냅이 일어날 축을 설정합니다.
축은 다음과 같은 값 중에 하나가 올 수 있습니다.

  • x or y – 물리적인 화면의 x축 또는 y축
  • block or inline – 언어 설정에 따른 블록의 방향 또는 인라인의 방향
  • both – 양쪽 축 전부

최근 css는 물리적인 x, y보다 언어 설정에 따라 가변적인 block, inline을 축 방향으로 지정하는 표준을 확장해가고 있습니다.
overflow속성 역시 기존의 overflow-x, overflow-y가 화면의 축을 기준으로 한다면 overflow-block과 overflow-inline은 언어 설정에 따라 축을 결정하는 방식입니다.

Scroll Snap Strictness(B)

B값은 얼마나 스냅을 강려크하게 적용할 지를 결정합니다.

  • none – 스냅을 일으키지 않습니다(아니 왜!)
  • mandatory – 언제나 스냅이 일어난 상태를 만들어냅니다.
  • proximity – 스크롤의 양이나 위치에 따라 적절히 스냅을 하기도 하고 하지 않기도 합니다.

사실 스크롤 스냅에는 숨겨진 개념이 있습니다. 스크롤이라는 행위에 반응할 것인가 아니면 언제나 스냅을 강제할 것인가에 대한 옵션이죠. 스크롤은 직접 유저가 인터렉션을 통해 스크롤 한 것입니다. 그에 비해 컨테이너에 엘리먼트가 추가되거나 자바스크립트를 통해 스크롤을 조정한 경우는 인터렉션을 통해 스크롤을 조정한 것이 아닙니다. mandatory의 경우는 직접 인터렉션을 하지 않는 상황에서도 언제나 스냅상태를 유지합니다. 또한 스크롤을 시키면 무조건 스냅된 위치로 이동하게 되죠.
이에 비해 proximity는 스크립트 등에는 반응하지 않고 스냅 시켜야 할 위치에서 멀리 떨어진 곳에 조그만 스크롤하는 경우는 스냅 되지 않습니다. 따라서 보다 자연스러운 스냅의 느낌이 납니다.

Scroll Snap Stoptop

이 속성도 스크롤 컨테이너에 적용하는 또 하나의 속성으로 스크롤 중 스냅 위치가 여러 개 나올 때의 정책을 정합니다.
예를 들어 세로로 세게 스크롤을 일으킨 경우 스냅이 일어날 여러 개의 엘리먼트가 휙휙 지나가다가 스크롤 파워가 다 되었을 때 쯤 걸리는 엘리먼트에 스냅되게 할 수 있을 것입니다.
반대로 아무리 세게 스크롤을 시켰다고 해도 반드시 다음 엘리먼트에 스냅이 되게 할 수도 있겠죠.

전자의 경우를 normal이라고 하고 후자의 무조건 걸리는 것을 always라고 합니다. 여기까지의 내용으로 다시 코드를 정리해보면 다음과 같습니다.

<style>
.scroll-container{
  overflow-x:scroll;
  scroll-snap-type:x proimity
  scroll-snap-stop:normal
}
</style>
<section class="scroll-container"></section>

좀 자연스럽게 스냅이 일어나면서 스크롤 파워가 떨어질 때쯤 스냅이 일어나도록 설정이 되었습니다. 이젠 그 안에 들어갈 엘리먼트를 설정할 차례네요.

Scroll Snap Aligntop

컨테이너에 들어갈 엘리먼트에게 적용할 속성은 scroll-snap-align 뿐으로 여기에 올 수 있는 값은 start, end, center입니다. 짜피 축이 컨테이너에서 결정되기 때문에 방향성이 있는 정렬이 무의미하여 시작, 중앙, 끝 정도로 정의하는 것이죠. 각각의 정렬 축은 다음과 같이 이해할 수 있습니다.

실습 예제1top

이 예제를 실행하려면 크롬69이상의 버전이 설치되어있거나 카나리아를 설치해야 합니다.

<style>
html,body{height:100%}
body{margin:0;padding:0}
.gallery {
  overflow-x:scroll;
  scroll-snap-type:x mandatory;
  display:flex;
}
.gallery img {
  scroll-snap-align:center;
  width:300px;height:300px;
}
</style>

<div class="gallery">
  <img src="1.png">
  <img src="2.png">
  <img src="3.png">
  <img src="1.png">
  <img src="2.png">
  <img src="3.png">
  <img src="1.png">
  <img src="2.png">
  <img src="3.png">
</div>

간단히 코드를 리뷰 해보면 컨테이너 설정에서 x축과 mandatory를 설정하고 flex박스를 적용하여 가로로 배치되도록 했습니다.
내부의 엘리먼트인 img에는 center기준의 스냅 정렬을 적용하여 가운데에 맞게 스냅이 일어나도록 했죠.
실제 실행 예제는 여기에 있습니다.
예제 1

실습 예제2top

세로 스크롤 기반의 모바일 페이지를 만들어보죠. 우선 상단에 nav를 배치하고 각 섹션을 아우르는 main을 배치해볼 수 있을 것 입니다.

<nav>CSS Scroll Snap</nav>
<main>
  <section></section>
  <section></section>
  <section></section>
</main>

전체적으로는 아래와 같은 구조가 됩니다.

화면에서 상단 네비게이션 만큼의 제외한 크기가 각 섹션의 높이가 되어야 합니다. 네비게이션 바의 크기는 변수로 지정하면 되니 이를 이용하여 간단히 크기를 정의해보죠.

<style>
html, body{height:100%}
body{margin:0;padding:0}

/*변수지정*/
:root{--nav-height: 40px;}

main{
  overflow-y:scroll;height:100%;
}
nav{
  height:var(--nav-height);
  position:fixed;top:0;left:0;right:0;
  background:#666;
}
section{
  height:calc(100% - var(--nav-height));
}
section:first-of-type{
  margin-top:var(--nav-height);
}
</style>
<nav>CSS Scroll Snap</nav>
<main>
  <section></section>
  <section></section>
  <section></section>
</main>
  1. –nav-height에 네비게이션 바의 크기를 정의하고 이를 이용해 nav의 크기를 정했습니다.
  2. 이어지는 section은 100%에서 네비게이션 바의 크기를 빼서 크기를 구합니다.
  3. section중 첫번째 요소의 경우 margin-top을 네비게이션 바 크기만큼 줘야 정상적으로 표시됩니다.

기본적인 레이아웃 설정이 끝났으므로 스타일 부분에 스크롤 스냅과 관련된 설정을 더해줍니다.

main{
  scroll-snap-type:y mandatory;
  scroll-padding-top:var(--nav-height);
  overflow-y:scroll;
  height:100%;
}
section{
  scroll-snap-align:start;
  height:calc(100% - var(--nav-height));
}

스크롤 컨테이너인 main에 스냅 타입과 scroll-padding-top을 줬습니다. scroll-padding-top을 이용하면 스냅이 될 때 내부의 패딩을 적용할 수 있습니다. 이 예제에서는 네비게이션 바만큼 상단을 비운 상태로 스냅될 기준 위치를 계산하게 했습니다.
section에는 스냅 정렬 기준을 start로 줘 시작되는 상단 부분에 스냅이 일어나게 설정했습니다. 아래에서 직접 확인해볼 수 있습니다.

예제 2

결론top

스크롤 스냅은 기존 자바스크립트로 복잡하게 구현해야 하는 스냅 로직을 가볍게 CSS선언 만으로 구현할 수 있게 해줍니다. 특히 자바스크립트에서는 모바일 네이티브 스크롤과의 충돌 문제 등 해결하기 어려운 난점이 많습니다만 스크롤 스냅은 부드럽게 브라우저 레벨의 스크롤 시스템과 통합됩니다.

스크롤 스냅은 다음과 같은 속성으로 이뤄져 있습니다.

  • scroll-snap-type: 스크롤 컨테이너의 작동 방법을 정의
  • scroll-snap-stop: 스크롤이 될 때 snap이 되는 규칙을 정의
  • scroll-padding-*: 내부 스냅의 기준을 계산할 때 패딩을 반영
  • scroll-margin-*: 내부 스냅의 기준을 계산할 때 마진을 반영
  • scroll-snap-align: 엘리먼트가 스냅될 때 어디를 기준으로 스냅 될 지 정의함

크롬69를 기준으로 여전히 scroll-snap-stop은 구현되어 있지 않습니다.
스크롤 스냅은 자바스크립트와의 상호작용에서 주의할 점이 다수 있으며 단순히 하나의 스크롤 영역이 아니라 다중 스크롤 영역 및 컨테이너의 다양한 엘리먼트 정렬 방법에 따라 패럴랙스를 비롯한 다양한 스크롤 기반의 UI를 효과적으로 구현할 수 있게 해줍니다.

다음 편에서는 스크롤 스냅을 다양한 방법으로 활용해보도록 하죠.