[번역] 미래를 향한 스크롤 (Scroll to the future)
사실 용어를 이해하지 못한 부분이 많아 어색한 부분이 많은 글입니다.
크게 잘못 해석된 부분을 알려주신다면 감사하겠습니다.
원문:
크게 잘못 해석된 부분을 알려주신다면 감사하겠습니다.
원문:
스크롤을 구현하는 것에 관해 항상 묻고 싶었지만 두려웠던 모든 것.
이제부터 현대 웹 사양의 스크롤 밑바닥에서부터 최신 CSS와 JavaScript에 이르기까지를 살펴보면서 하나의 페이지를 부드럽고 아름답게, 그리고 적은 리소스로 구동하도록 만들어 봅시다.
대부분의 웹 페이지는 한 화면에 들어가지 않으므로, 모든 사용자들은 스크롤 기능이 당연하다고 여깁니다. 하지만 프론트엔드 개발자와 UX 디자이너에게 브라우저에 상관없이 원활하게 작동하고, 디자인이 잘 어울리면서도 성능이 뛰어난 스크롤 구현은 어려울 수 있습니다. 웹 표준이 그 어느 때보다 빠르게 진화하고 있지만, 코딩 방법은 빈번히 뒤쳐져 있습니다. 몇 가지 일반적으로 쓰이는 프로그램 케이스를 다시 살펴보고 당신이 사용 중인 솔루션이 더 훌륭한 것으로 바뀌었는지 확인해 봅시다.
스크롤바가 사라진 이유 (The curious case of a disappearing scrollbar)
지난 30년 동안, 디자인 트렌드에 맞춰서 스크롤바의 모양은 계속 변했습니다. 색상, 그림자, 화살표 모양, 테두리의 반경 - 인터페이스 디자이너가 이 모든 것을 시험했죠. Windows에서 어떻게 변해왔는지 봅시다:
스크롤바를 계속 볼 수 있는 시스템 설정도 있으며, 일부 사용자는 그것을 사용하기도 합니다.
2011년, iOS에서 영감을 얻은 Apple의 인터페이스 디자이너는 스크롤바를 "미화"하려는 모든 도전에 종지부를 찍었습니다. 바로 모든 맥 제품에서 유비쿼터스 디자인 요소가 사라진 겁니다. 정적 뷰에서 더이상 공간을 차지하지 않으며 사용자가 스크롤을 시작할 때 나타납니다.
스크롤바의 조용한 죽음을 Apple 사람들은 결코 슬퍼하지 않았습니다. 아이폰과 아이패드에서 스크롤하던 방식에 익숙해진 사람들은 그 변화를 빠르게 수용했고, 대부분의 개발자와 디자이너들은 "좋은 속임수야!" 라고 생각했습니다. 스크롤바의 너비를 계산하는 것은 언제나 성가신 일이었기 때문입니다.
그러나, 여전히 우리는 여러 운영체제 및 브라우저 구현을 하는 세상에 살고 있습니다.
우리가 쓰는 웹용으로 개발한다면, 스크롤바의 행방을 무시할 수가 없습니다.
사용자에게 스크롤 사용을 더욱 즐겁게 해주는 몇 가지 트릭을 보여드리겠습니다.
숨은 스크롤 (Hide and scroll)
modal 창에 대한 기본적인 예를 들어 봅시다. 모달이 열릴 때, 페이지의 메인 내용은 스크롤을 멈추어야 합니다. CSS에서의 빠른 방법이 있습니다:
body {
overflow: hidden;
}
하지만 그 코드는 아래와 같이 지저분한 효과를 냅니다.
이 예제에서, Mac의 스크롤바를 시연을 위해 시스템 환경 설정에서 계속 보이도록 설정했습니다(그렇게 해서 Windows 사용자와 동일하게 하였습니다).
어떻게 이 문제를 해결할까요? 스크롤바의 너비를 알고 있다면 modal창이 열릴 때마다 메인 페이지의 오른쪽에 간격을 줄 수 있습니다.
그러나 일관성이 유지되는 Mac(프로그램과 상관없이 언제나 15px)과 달리 운영체제와 브라우저에 따라 다르게 설정되는 Windows는 개발자들을 힘들게 했고, 너비를 측정하기란 쉽지가 않았습니다:
이 측정값은 Windows의 현재 브라우저 버전에서만 해당합니다. 이전 버전에서는 또 다른 모습이었고, 향후에 어떻게 바뀔지도 아무도 모르는셈입니다.
측정하는 대신, JavaScript를 이용하여 동적으로 스크롤 너비를 계산하는 방법도 있습니다:
const outer = document.createElement('div');
const inner = document.createElement('div');
outer.style.overflow = 'scroll';
document.body.appendChild(outer);
outer.appendChild(inner);
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
document.body.removeChild(outer);
이것은 7줄 밖에 안되는 코드지만, DOM과 상호 작용을 합니다. 꼭 필요한 경우가 아니라면 DOM 조작은 피하는 것이 좋습니다.
이 문제를 해결하는 또 다른 방법은 스크롤바를 modal 뒤에 표시되도록 유지하는 것입니다. CSS로 구현하는 방법은 다음과 같습니다:
html {
overflow-y: scroll;
}
"갑작스러운 modal 문제"를 해결했지만, 디자인적으로 시각적인 문제를 일으키는 쓰지 않는 스크롤바가 나타난다는 문제가 여전히 남았습니다.
우리의 생각에 더 나은 해결책은, 모든 스크롤바를 제거하는 것입니다. style 속성으로도 가능합니다. 스크롤바가 스크롤할 때도 나타나지 않기 때문에 이 방법은 macOS와도 엄연히 다릅니다: 항상 숨겨진 상태로 유지되지만, 페이지는 스크롤 가능합니다. Chrome, Safari 및 Opera의 경우 이 CSS를 사용 가능합니다:
.container::-webkit-scrollbar {
display: none;
}
Internet Explorer 와 Edge에서:
.container {
-ms-overflow-style: none;
}
불행하게도 Firefox에서는 스크롤바를 제거할 방법이 없습니다.
보다시피, 정확한 답이란 건 없습니다. 우리가 설명한 모든 방식에는 장단점이 있습니다. 당신의 프로젝트에 가장 적합한 것을 고르면 됩니다.
외양에 관한 다툼 (A fight for appearances)
일부 운영체제에서는 기본 스크롤바를 쓰는 게 그닥 아름답지 않다는 것을 아실 겁니다. 일부 디자이너들은 응용프로그램의 모양과 느낌을 완전히 제어하고 잔재를 남기지 않는 것을 선호합니다. JavaScript에서 사용자 정의 스크롤을 구현하여 시스템 기본값을 완전히 대체할 수 있는 수백개의 GitHub repositories가 있습니다.
하지만 기존의 브라우저 스크롤바를 커스터마이징하려면 어떻게 할까요?일반적인 API는 없기 때문에, 각각 업체는 서로 다른 방법을 제안할 수 있습니다.
Internet Explorer는 버전 5.5 이후로 스크롤바 모양을 변형할 수 있지만, 색상만을 바꿀 수 있습니다. 이것은 thumb(드래그하는 부분)과 화살표의 색상을 바꿀 수 있는 방법입니다:
body {
scrollbar-face-color: blue;
}
하지만 색상을 다루는 것은 사용자 경험을 위해 충분하지 않습니다. WebKit 개발자들은 이것을 깨닫고 2009년에 이미 그들만의 스타일링 방식을 제안했습니다. 다음은 WebKit 브라우저에서 macOS 스크롤바를 복제하기 위해 -webkit vendor prefix를 사용하는 방법입니다:
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-thumb {
background-color: #c1c1c1;
border-radius: 4px;
}
이러한 변형은 Chrome, Safari, Opera 및 UC 브라우저 또는 삼성 인터넷의 모바일에서도 지원됩니다. Edge도 이를 계획했으나, 3년이 지난 지금도 우선 순위가 중간으로 밀리고 있습니다.
스크롤바 커스터마이징 분야에서, Mozilla 재단은 디자이너의 요구를 무시하는 절대적인 챔피언입니다. Firefox에서 스크롤바를 스타일화할 수 있는 기능은 17년 전, 천년도의 시작이었습니다. 불과 몇 달 전, Jeff Griffiths (Mozilla의 Firefox 브라우저 담당 이사)가 마침내 답변으로 스레드를 멋지게 만들었습니다:
“플랫폼 팀의 그 누구도 사전에 스펙을 프로토타입화하는 것에 관심이 없는 한, 실제 스펙이 나올 때까지 그 어떤 일이라도 전혀 관심을 가지지 않을 것이다.”
사실대로 말하자면, W3C의 관점에서 볼 때 WebKit 접근법은 잘 지원되지만 공식적으로 존재하지 않습니다. CSS 스크롤바 규격에 대한 기존 초안은 IE로부터 힌트를 얻었는데, 바로 색상에서의 사용자 지정을 중지한 것입니다.
WebKit 사용자 지정을 지원해달라는 논쟁이 열리는 등 싸움이 계속되고 있습니다. CSS Working Group에 영향력을 가지고 싶다면, 바로 이 논쟁에 참여하면 됩니다. 아마 이 문제가 최우선 과제는 아니겠지만, 표준화된 지원은 많은 프론트엔드 개발자와 디자이너들을 더 편리하게 만들어 줄 것입니다.
원활한 운영자 (Smooth operator)
스크롤바와 관련된 가장 일반적인 작업은 방문 페이지 탐색입니다. 보통 앵커 링크로 구현됩니다. 당신은 해당 엘리먼트의 id만 알면 됩니다:
<a href="#section">Section</a>
이 링크을 클릭하면 해당 영역으로 건너뛸 수 있으며, UX 디자이너는 종종 스크롤을 부드럽게 하기 위한 일종의 애니메이션을 씁니다. GitHub에는 JavaScript를 많게 혹은 적게 사용하는 수많은 솔루션들이 있지만, 현재는 코드 한 줄로도 동일한 효과를 낼 수 있습니다. 최근 DOM API의 Element.scrollIntoView()는 behavior 키가 있는 옵션 개체를 가져와서 영역에서 부드럽게 스크롤할 수 있도록 했습니다:
elem.scrollIntoView({ behavior: 'smooth'});
그러나 옵션에 대한 브라우저 지원은 여전히 제한되어 있으며 아직도 스크립트를 사용합니다. 또한 웬만하면 추가적인 스크립트는 피하는 게 좋습니다.
다행히도, 한 줄의 코드로 전체 페이지의 스크롤 동작을 바꿀 수 있는 새로운 CSS 속성(아직 초안 작업 중)이 존재합니다:
html {
scroll-behavior: smooth;
}
결과는 다음과 같습니다:
CSS에 충실하라
또 다른 일반적인 작업 중 하나는 스크롤 방향에 따라 요소를 동적으로 배치하는 “sticky” 효과입니다.
이전에는 “sticky”효과를 구현하려면 엘리먼트의 크기를 고려해야하는 복잡한 스크롤 핸들러를 써야 했습니다. 그리고 핸들러의 최적화를 위해 “sticking” 및 “unsticking” 동작 시 미묘한 지연이 발생할 수밖에 없었습니다. JavaScript의 경우 특히 Element.getBoundingClientRect()를 사용했을 때 성능이 특히 저하되었습니다.
최근
position: sticky
속성이 CSS에 생겼습니다. 그것은 offset을 표기하는 것만으로도 원하는 효과를 얻을 수 있습니다:.element {
position: sticky;
top: 50px;
}
최고 속도(Full throttle)
브라우저의 관점에서 스크롤은 이벤트이므로, JavaScript에서는 표준 addEventListener로 처리합니다:
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY;
/* doSomething with scrollTop */
});
그러나 사람들은 많은 스크롤을 하고, 이벤트가 너무 자주 발생하면 (심지어 모든 스크롤된 픽셀에서 발생했을 경우)—당연히 성능 문제가 발생할 수밖에 없습니다. 이 때 throttling이라는 기능으로 브라우저의 작업을 더 쉽게 할 수 있습니다:
window.addEventListener('scroll', throttle(() => {
const scrollTop = window.scrollY;
/* doSomething with scrollTop */
}));
이 때, listener 함수를 감싸고 원하는 간격마다 두 번 이상씩 실행되지 않도록 실행을 "지속" 하는
throttle
함수를 정의해야 합니다:function throttle(action, wait = 1000) {
let time = Date.now();
return function() {
if ((time + wait - Date.now()) < 0) {
action();
time = Date.now();
}
}
}
더 부드럽게 하기 위해서, throttling 과 window.requestAnimationFrame() 를 결합할 수도 있습니다:
function throttle(action) {
let isRunning = false;
return function() {
if (isRunning) return;
isRunning = true;
window.requestAnimationFrame(() => {
action();
isRunning = false;
});
}
}
어떤 방식을 선택하든, 스크롤 이벤트 핸들러를 최적화하는 것이 중요합니다.
viewport에 머물러(Staying in the viewport)
이미지의 지연 로딩 또는 무한 스크롤과 같은 트릭은 요소가 viewport에 나타나는지 파악해야 합니다. 가장 일반적인 솔루션 인 Element.getBoundingClientRect()를 사용하여 이벤트 리스너 내에서도 수행됩니다:
window.addEventListener('scroll', () => {
const rect = elem.getBoundingClientRect();
const inViewport = rect.bottom > 0 && rect.right > 0 &&
rect.left < window.innerWidth &&
rect.top < window.innerHeight;
});
Reflow는 문서의 일부 또는 전체를 다시 렌더링하기 위해 문서에서 엘리먼트의 위치 및 형상을 다시 계산하기 위한 웹 브라우저 프로세스의 이름입니다.
이 코드의 문제점은
getBoundingClientRect
를 호출 할 때마다 reflow가 발생하여 전체 성능이 저하된다는 것입니다. 이벤트 처리기 내에서 이러한 호출을 수행하면 훨씬 더 나빠지며, 이 경우 throttling조차 도움이 되지 않습니다.
이 문제는 2016년에 Intersection Observer API를 도입하여 해결되었습니다. 브라우저의 viewport가 아닌, 엘리먼트와 상위 객체 간의 교차점을 추적할 수 있게 된 것입니다. 또한 엘리먼트가 단일 픽셀로만 뷰에 부분적으로 만 표시된 경우에도 callback을 trigger하도록 선택할 수 있습니다:
const observer = new IntersectionObserver(callback, options);
observer.observe(element);
Reflow를 트리거하는 DOM 특성 및 메소드에 대한 것은 이 cheat-sheet를 확인하십시오.
너무 멀리 스크롤하지 말 것 (Don’t scroll too far)
스크롤 가능한 팝업 또는 드롭 다운을 구현해야하는 경우 scroll chaining (엘리먼트의 끝을 지나서 스크롤하면 전체 페이지가 이동하기 시작하는 것) 문제를 알고 있어야합니다.
Scroll chaining 예시
페이지의
overflow
속성을 조작하거나 엘리먼트의 스크롤을 가로채어 경계에 도달할 때마다 스크롤 이벤트를 취소하는 방법으로 "overscroll"을 제거 할 수 있습니다.
JavaScript를 사용할 경우, "스크롤"이 아니라 마우스 휠이나 터치 패드로 페이지를 스크롤 할 때마다 발생하는 “wheel” 이벤트를 처리해야합니다:
function handleOverscroll(event) {
const delta = -event.deltaY;
if (delta < 0 && elem.offsetHeight - delta > elem.scrollHeight - elem.scrollTop) {
elem.scrollTop = elem.scrollHeight;
event.preventDefault();
return false;
}
if (delta > elem.scrollTop) {
elem.scrollTop = 0;
event.preventDefault();
return false;
}
return true;
}
불행하게도, 이 솔루션은 매우 신뢰하기 어렵습니다. 또한 성능에 부정적인 영향을 줄 수 있습니다.
Overscroll은 특히 모바일 장치에서 위험합니다. Loren Brichter는 iOS 용 Tweetie 앱에서 "pull to refresh" 제스처를 만들었으며, 이 트릭은 UX 커뮤니티를 폭풍으로 몰아넣었습니다. 트위터와 페이스북을 포함한 주 서비스에서 이것을 채택했습니다.
T동일한 기능이 Android의 Chrome 브라우저에 들어 왔을 때 문제가 발생했고, 그것은 웹 앱에서 “pull to refresh”를 사용한 모든 사람들에게 골칫거리가 되었습니다. 페이지를 아래로 당기면 더 많은 게시물이 나오는 게 아닌 완전히 새로고침되는 현상이었습니다.
이것은 CSS의 overscroll-behavior라는 새로운 속성 덕분에 해결됩니다: 스크롤 경계에 도달하는 동작을 제어할 수 있게 하며, pull to refresh 및 scroll chaining과 특정 OS에서만 나타나는 특수 효과(안드로이드의 “glow” 및 Apple의 “rubber band”)를 모두 처리합니다.
이제 위의 GIF에 표시된 문제를 한 줄의 코드로 Chrome, Opera 또는 Firefox에서 해결할 수 있습니다:
.element {
overscroll-behavior: contain;
}
마지막 터치 (The final touch)
터치 기반 인터페이스에서 스크롤하는 것은 그 자체로도 논문의 가치가 있는 광대한 주제입니다. 그러나 많은 개발자들이 가능성을 간과하는 경향이 있기 때문에 여기서 언급할 가치가 있습니다.
이 스크롤 제스처는 매우 흔하고 중독적이어서 사람들은 "탈주"하기 위한 미친 해결책을 생각해낸다.
주변 사람들이 스마트폰 화면에서 손가락을 위아래로 움직이는 빈도가 얼마나 될까요? 그렇습니다. 그것은 항상 일어나고, 아마도 당신 또한 이 기사를 읽는 동안 똑같이 하고 있을 것입니다.
화면을 가로 질러 손가락을 움직일 때, 매끄럽고 멈춤 없는 컨텐츠 이동이 필요합니다.
그러나 모바일 OS에서 전체 페이지의 모멘텀 스크롤이 처리되는 동안 해당 페이지의 요소 내부를 스크롤하려고 하면 해당 페이지가 사라지게 된다는 것을 알고 계실 겁니다. 무의식적으로 약간의 관성을 기대하는 모바일 사용자에게는 상당히 실망스러울 수 있습니다.
이것에 대한 CSS 해결책이 있지만, 치트키에 불과합니다:
.element {
-webkit-overflow-scrolling: touch;
}
왜 치트키일까요? 우선 vendor prefix에서만 작동합니다. 두번째로, 터치 기기에서만 작동합니다. 마지막으로, 브라우저가 이 기능을 지원하지 않는다면, 그냥 그대로 두어야 할까요? 물론 만약에 해결책이 있다면, 마음껏 사용해도 좋습니다.
터치 디바이스에서 스크롤 동작을 개발하면서 고려해야할 또 다른 점은,
touchstart
또는 touchmove
이벤트를 처리할 때의 브라우저의 성능입니다. 그 문제에 대한 것은 여기에 자세히 설명되어 있습니다. 간단히 말하자면, 현대의 브라우저는 별도의 thread에서 매끄러운 스크롤을 처리하는 방법을 가지고 있지만, Event.preventDefault()를 통해 스크롤을 취소했을 가능성에 대비하기 위해 때로는 최대 500ms까지 기다리기도 한다는 겁니다.
브라우저가 계속
preventDefault
가 호출되기를 대기하므로, 아무것도 취소하지 않는 빈 listener라도 성능에 상당히 부정적인 영향을 줄 수 있습니다.
T브라우저에게 이벤트가 취소되면 안된다고 명시적으로 알리기 위한, WHATWG의 DOM Living Standard에는 다소 모호한 기능이 있습니다. 상당히 많은 지원을 받는 Passive event listeners가 바로 발생하는 이벤트를 절대 취소 할 수 없음을 브라우저에 알리는 선택적 객체 인수를 listener에 전달하는 것입니다. 이러한 핸들러 내에서 preventDefault를 호출해도 아무것도하지 않습니다:
element.addEventListener('touchstart', e => {
/* doSomething */
}, { passive: true });
망가지지 않았다면, 왜 고쳐야 합니까? (If it ain’t broken, why fix it?)
현대 웹에서는, 모든 클라이언트에 대해 동일한 동작을 달성하기 위해 사용자 지정 JavaScript에 크게 의존하는 것이 더 이상 인정되지 않습니다: "브라우저 간 호환성"이라는 이슈는 CSS 속성과 DOM API 메소드가 표준 브라우저 구현으로 발전함에 따라 과거의 일에 불과하게 되었습니다.
우리의 의견으로는 Progressive Enhancement는 웹 프로젝트에서 사소한 스크롤을 구현할 때 쓰는 가장 좋은 방법입니다.
가능한 최소한의, 보편적으로 지원되는 UX를 제공할 수 있는지 확인한 다음 최신 브라우저 기능을 고려하여 개선하십시오.
필요할 때마다 polyfills를 사용하세요. 그것은 의존성을 만들지 않으며 필요한 support가 도착할 때마다 쉽게 제거할 수 있기 때문입니다.
6개월 전, 이 기사가 그저 아이디어였을 때, 우리가 설명한 몇몇 속성들은 단지 몇 개의 브라우저에서만 소개되었습니다. 그리고 출판될 무렵에는 그것들은 거의 보편적인 지지를 받았습니다.
어쩌면 지금 이 순간에도, 이 기사를 스크롤하는 동안 또 다른 브라우저에서는 삶을 더 쉽게 만들고 bundle 크기를 작게하는 속성에 대한 지원을 제공했을 수도 있습니다.
읽어 주셔서 감사합니다!
브라우저의 changelogs를 읽어 보고, 제안 토론에 참여하여 웹 표준을 올바른 방향으로 지도하고 모든 사람에게 부드러운 항해스크롤이 되기를!
0 개의 댓글:
댓글 쓰기