스크롤과 관련된 CSS 속성 3가지

자바스크립트 없어도 가능한 스크롤 기능

가끔 MDN을 둘러보다 보면 재미있는 속성을 발견할 때가 있는데 예전에 발견했지만 아직 적용은 해보지 못한 세 가지 CSS 속성을 이 글에서 소개해보려고 한다. 소개할 세 가지 속성 중 두 가지는 현재 작업 초안(Working Draft) 상태인 명세에 정의되어 있고 이 글을 쓰는 현재 데스크톱 크롬과 데스크톱 파이어폭스에서만 지원된다. 나머지 한 가지는 명세로 분리되어 있으며 100% 완벽하게 지원되는 건 아니지만 어쨌든 앞선 두 가지 속성보다는 지원하는 브라우저가 많은 편이다.

아래 설명하는 세 가지 CSS 기능은 모두 "없어도 크게  불편하지는 않은" 특성이 있어 점진적으로 적용 가능하다는 장점이 있다.

부드러운 스크롤 이동

스크롤 이동이 가장 많이 사용되는 예는 아마도 페이지 제일 위로 이동하는 기능일 것이다. 가장 간단한 형태의 페이지 상단 이동 기능은 앵커 태그를 사용해 구현할 수 있다.

<a href="#">제일 위로</a>

사용자가 이 링크를 클릭하면 현재 위치가 어디든 상관없이 해당 페이지의 제일 위쪽으로 이동한다. 하지만 이 때 아무런 애니메이션도 없이 이동하기 때문에 마치 순간 이동을 하는 듯한 느낌이 든다. 그래서 jQuery의 animate() 함수를 사용한 부드러운 스크롤 이동 방법이 오랫동안 사용되었다.

그런데 이제 CSS만 사용해도 해당 기능을 구현할 수 있게 되었다. 파이어폭스 36, 구글 크롬 61부터는 scroll-behavior라는 속성에 smooth라는 값을 주면 앵커로 이동하는 스크롤도 부드럽게 이동한다. 방법도 간단하다. 스크롤 컨테이너가 되는 엘리먼트에 scroll-behavior: smooth라는 값만 추가하면 된다. 다음 예시를 확인해보자(출처: MDN).

늘 그렇듯 IE에서는 지원되지 않는 속성이다. 하지만 지원되지 않는 브라우저에서도 링크 이동은 문제없이 실행되므로 기능 자체에는 이상이 없다. 점진적 향상(Progressive enhancement)의 좋은 예라 할 수 있다.

아직 스크롤 속도나 애니메이션 방식(ease-in 등)을 조절할 수 없다는 점이 아쉽기는 하지만 JS를 사용하지 않고 단 한 줄의 CSS 코드만으로 편리하게 스크롤 효과를 줄 수 있어 꽤 유용할 것으로 생각한다.

스크롤 영역 제한

웹 페이지에 여러 스크롤 영역이 겹쳐 있는 경우를 생각해보자. 흔히 볼 수 있는 예로는 웹 페이지(부모 스크롤 영역) 안에 있는 채팅창(자식 스크롤 영역)을 들 수 있다. 채팅창에서 스크롤을 하다가 채팅창 스크롤 영역의 끝에 다다르면 브라우저는 이제 웹 페이지를 스크롤한다. 다음 동영상을 보자.

기본 스크롤 동작: 자손 스크롤 영역의 끝에 다다르면 조상 엘리먼트를 스크롤 한다.

화면에서 보듯이 마우스 커서는 채팅창에 머문 채로 있지만 한쪽 방향으로 계속 스크롤하다보면 채팅창을 넘어 부모 스크롤 영역까지 스크롤이 되버린다. 스크롤링 체인(scrolling chain)이라고 부르는 이 동작은 때로는 편리할 수 있지만 채팅창과 같은 웹 애플리케이션에서는 오히려 원하지 않는 동작일 가능성이 높다.

아직 초안 단계인 CSS의 Overscroll Behavior Module Level 1에 정의된 overscroll-behavior 속성을 사용하면 이런 동작을 제어할 수 있다(MDN 링크). 이 속성의 값을 contain으로 설정해두면 스크롤링 동작이 연결되지 않는다. 위 동영상을 예로 들면, 채팅창의 끝에 다다르면 거기서 스크롤링 동작도 끝이라는 것이다. 커서를 채팅창 바깥으로 옮기지 않는 한, 웹 페이지는 스크롤되지 않는다.

이 속성에는 세 가지 값을 사용할 수 있다. 기본값인 auto는 브라우저의 기본 동작이고, contain으로 설정해두면 스크롤링 체인 동작이 현재 커서가 위치한 스크롤 위치를 넘어가지 않는다. 위 동영상에서는 채팅창에 overscroll-behavior: contain CSS 속성이 설정되어 있다. 마지막 값은 none으로 contain과 같이 스크롤링 체인 동작이 발생하지 않는다. 다른 점이 있다면 none은 스크롤 영역 경계를 넘는 스크롤 동작도 방지한다는 것이다. 아래 두 동영상은 각각 <body> 엘리먼트에 overscroll-behavior를 설정하지 않았을 때와 overscroll-behavior: none을 설정했을 때를 보여준다.

overflow-behavior를 설정하지 않았을 때
overflow-behavior: none을 설정했을 때

뿐만 아니라 overscroll-behavior: nonebody에 설정하면 터치 인터페이스를 사용해 좌우 방향으로 손가락을 쓸면 동작하는 네비게이션 이동 기능도 작동하지 않도록 방지한다. 종종 의도치 않게 이동 기능이 동작해 불편함을 유발하는 경우가 있는데 이럴 때 유용할 것이다. 이름이 유사한 overflow 속성처럼 이 값도 overscroll-behavior-xoverscroll-behavior-y 속성을 통해 가로 또는 세로에만 속성을 적용할 수있다.

지원하는 브라우저로는 크롬, 파이어폭스, Edge, Opera 브라우저 등이 있지만 현재 Edge에서는 none 값이 contain처럼 동작한다고 한다. IE와 Safari 브라우저는 이 값을 지원하지 않는다.

스크롤 스냅

Note: 스크롤 스냅의 최신 명세는 현재 구글 크롬과 사파리에서만 지원한다. 별도의 호환성 표기가 없다면 이들 브라우저를 사용해야 예시가 정상적으로 동작할 것이다.

카드 뉴스나 슬라이드 갤러리처럼 한 번에 한 개의 자식 엘리먼트만 화면에 보여주는 형태의 엘리먼트를 생각해보자. 가장 간단한 HTML과 CSS는 각각 다음과 같은 형태가 될 것이다.

<div id="container">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
  <div class="item">4</div>
  <div class="item">5</div>
  <div class="item">6</div>
</div>
#container {
  width: 300px;
  height: 200px;
  border: 1px solid #000;
  overflow: auto;
}
.item {
  display: flex;
  height: 100%;
  align-items: center;
  justify-content: center;
  font-size: 5em;
}
.item:nth-child(3n+1) {
  background: #fff6be;
}
.item:nth-child(3n+2) {
  background: #ffa1ac;
}
.item:nth-child(3n) {
  background: #cbbcf6;
}

위에 나타난 엘리먼트를 스크롤 해보면 자주 보던 방식대로 동작하는 것을 알 수 있다. 그런데 원하는 위치에서 스크롤이 멈추도록 하는 스크롤 스냅 기능을 적용하고 싶다면 어떨까? 지금까지는 자바스크립트를 사용해 이 기능을 구현하곤 했었다. 이해를 돕기 위해 Scrollfy라는 jQuery 플러그인으로 구현한 스크롤 스냅 기능을 살펴보자.

스크롤을 조금 움직이면 구분된 영역에 딱 맞춰서 스크롤이 이동한다. 이런 기능을 스크롤 스냅 기능이라고 하는데 자바스크립트를 사용해 구현되곤 했다. 하지만 후보 권고안(Candidate Recommendation) 상태인 CSS Scroll Snap Module Level 1에 정의된 scroll-snap- 계열 속성을 사용하면 CSS만으로도 이 기능을 구현할 수 있다. 심지어 자바스크립트로 하는 것보다 더 부드럽게 동작한다. 위에서 살펴 본 예제에 이 속성을 적용해보면 다음과 같다.

위 엘리먼트를 스크롤 해보면 자식 엘리먼트의 시작점에 스크롤 위치가 맞추어 진다. 심지어 일부러 중간에 걸쳐두려고 해도 스크롤이 자동으로 이동한다. 자바스크립트없이 CSS만 사용해서 얻은 결과이다.

CSS Scroll Snap Module Level 1 명세에서 정의하는 속성은 스냅의 동작을 설정하는 scroll-snap-type, scroll-snap-align, scroll-snap-stop 세 가지와 스크롤 영역의 모양을 설정하는 scroll-padding, scroll-margin의 두 가지 총 5가지가 있다. 사실 scroll-paddingscroll-marginpadding이나 margin과 마찬가지로 방향 속성에 따라 나누어 질 수 있다. 이 중 크롬 브라우저에서만 지원하는(2019년 3월 현재) scroll-margin이나 scroll-padding은 물론 지원하는 브라우저가 없는 scroll-snap-stop을 제외한 나머지 두 속성 scroll-snap-typescroll-snap-align만 간단히 살펴보자.

scroll-snap-type은 스크롤 스냅의 동작을 결정한다. 이 속성에는 두 가지 종류의 값을 설정할 수 있다. 첫 번째는 스크롤링 방향으로 x, y, block, inline, both를 사용할 수 있고 두 번째는 스크롤 스냅의 허용 정도로서 mandatory, proximity를 사용할 수 있다. blockinline이라는 방향이 어색하게 느껴질 수 있는데 텍스트를 왼쪽에서 오른쪽으로 표현하고 블럭을 위에서 아래로 배열하는 대중적인 환경에서 blocky방향, inlinex방향을 의미한다. 소위 CSS3로 오면서 사용하게 된 방향 표기 방식인데 아마 FlexBox를 다루어 보았다면 들어본 적 있을 것이다.

스크롤 허용 정도는 생략할 수 있는데 proximity가 기본값으로 사용된다. proximity로 설정하면 스크롤 영역이 경계선 근처에 올 때만 스크롤 스냅이 동작한다. 일부러 중간에 걸쳐두면 그대로 존재하게 된다는 의미이다. mandatory로 설정하면 항상 스크롤 스냅이 동작한다. 중간에 애매하게 걸치는 게 어렵게 된다. 앞서 살펴본 예는 scroll-snap-type: y mandatory를 설정한 것이고 아래의 예는 scroll-snap-type: y proximity를 설정한 것이다.

다음으로 살펴볼 scroll-snap-align은 스크롤의 스냅의 기준을 어디에 맞출 것인지 정한다. 이 속성에는 none, start, end, center라는 값을 사용할 수 있다. start라고 하면 스크롤 스냅의 기준을 해당 엘리먼트의 시작 위치로 설정한다는 의미이며 none이면 스크롤 스냅의 기준을 설정하지 않는다는 의미이다. 따라서 none으로 설정해두면 설령 부모 엘리먼트에서 scroll-snap-type을 설정했더라도 스크롤 스냅이 동작하지 않는다. 이해를 돕기 위해 위 예시에 있는 자식 엘리먼트의 높이를 컨테이너의 1.5배로 만들어 보았다.

숫자는 여전히 엘리먼트의 중간에 위치하지만 스크롤 스냅의 기준이 엘리먼트의 시작으로 맞추어져 있어서 스냅이 동작했을 때 숫자가 아래로 치우쳐 보이게 된다.

이 때 스크롤 스냅의 기준을 중간(center)으로 만들어주면 스크롤이 스냅될 때 자식 엘리먼트의 중간이 화면에 위치하게 된다. 다음 웹 페이지에서 스크롤을 시도해보자.

후보 권고안까지 진행된 명세인만큼 앞 섹션에서 언급한 다른 스크롤 관련 속성보다 브라우저 지원이 조금 나은 편이다. 특히 scroll-snap-type-ms- 접두어를 사용하면 IE 10에서도 사용할 수 있다! 비록 초기 명세를 기준으로 구현되어서 방향 설정도 할 수 없고 스크롤 스냅의 기준도 설정할 수 없지만 말이다.

스냅의 기준을 정하는 scroll-snap-align속성은 현재 크롬과 사파리에서만 지원한다.현재 사라진 과거 명세에서는 scroll-snap-align으로 스크롤 스냅의 기준을 정하는 대신 scroll-snap-points-x, scroll-snap-points-y, scroll-snap-destination, scroll-snap-coordinate 등을 사용하여 직접 스냅의 기준을 설정했어야 했다. 파이어폭스, IE 등은 아직 예전 명세를 기준으로 스냅을 구현하고 있다. 아래는 예전 방식대로 구현한 예시이다. 크롬 브라우저에서는 동작하지 않으므로 파이어폭스 또는 IE에서 확인해보자.

마치며

과거에는 자바스크립트가 필수적이었던 스크롤 관련 기능을 이제는 브라우저가 직접 지원하기 시작했고 CSS를 사용해 이를 제어할 수 있다. 훨씬 더 간편한 것은 물론이고 성능 또한 비할 바 없이 훌륭하다. 당장은 모든 브라우저에서 지원하지 않는다지만 대체로 선택적인 기능이므로 동작하지 않아도 그만이고, 동작한다면 더 나은 사용자 경험을 선사할 수 있다.

댓글을 남겨주세요

This site uses Akismet to reduce spam. Learn how your comment data is processed.