본문으로 건너뛰기

상단 고정 효과(Sticky) 해결 방안

무료2016-11-12#CSS#JS#Solution#position sticky#移动端吸顶#吸顶案例#android sticky#ios sticky#-webkit-sticky

iOS의 scroll 이벤트 제한으로 인해 이 간단한 효과를 구현하기가 매우 어렵습니다.

1. 시나리오

'상단 고정(Sticky)'은 꽤 오래된 인터랙션 방식으로, PC 페이지에서 수년간 사용되어 왔습니다. 그림과 같이:

[caption id="attachment_1210" align="alignnone" width="539"]sticky sticky[/caption]

고정될 요소의 초기 위치는 대개 페이지 상단 근처에 있지만, 상단과 어느 정도 거리가 있습니다. 이 영역에는 배너 이미지와 같은 가장 눈에 띄는 요소가 배치됩니다. 페이지를 아래로 스크롤하여 고정될 요소의 초기 위치를 지나면 해당 요소를 상단에 고정합니다.

상단 고정이 요구되는 요소는 주로 2차 내비게이션 바, 검색창, 게시글 제목(h1), 표 헤더(thead), 탭 바 등입니다. 공통점은 내용이나 기능 면에서 중요하지만 가장 중요한 요소는 아니라는 점입니다(가장 중요한 요소는 대개 페이지 상단에 항상 고정되어 있습니다, navbar-fixed-top).

2. PC 해결 방안

페이지가 특정 위치까지 스크롤될 때 어떤 동작을 수행합니다.

'맨 위로 이동' 버튼도 마찬가지입니다. 페이지를 아래로 150px 이상 스크롤하면 버튼을 표시하고, 그렇지 않으면 숨깁니다.

따라서 구현 아이디어는 scroll 이벤트를 감시하는 것입니다:

var stickyEl = document.querySelector('.sticky');
var stickyT = stickyEl.offsetTop;
window.onscroll = function(e) {
    var scrollT = document.body.scrollTop;
    // console.log(scrollT, stickyT);
    if (scrollT > stickyT) {
        stickyEl.classList.add('fixed-top');
    }
    else {
        stickyEl.classList.remove('fixed-top');
    }
};

'맨 위로 이동' 버튼의 구현 방식과 완전히 똑같습니다. 효과는 꽤 괜찮아 보이지만, 임계 위치인 stickyT에 도달했을 때 페이지가 덜컥거리며 위로 쑥 올라가는 현상을 발견하게 됩니다. 그 이유는 stickyElfixed 상태로 바뀌면서 레이아웃에서 빠져나가고, 아래에 있던 요소들이 올라와 그 자리를 차지하기 때문입니다.

우리는 부드러운 전환을 원하며 떨림 현상을 방지해야 하므로, stickyEl의 원래 자리를 지켜줄 *플레이스홀더(placeholder)*가 필요합니다:

var stickyEl = document.querySelector('.sticky');

// 위치 고정용 플레이스홀더
var stickyHolder = document.createElement('div');
var rect = stickyEl.getBoundingClientRect();
// console.log(rect);
stickyEl.parentNode.replaceChild(stickyHolder, stickyEl);
stickyHolder.appendChild(stickyEl);
stickyHolder.style.height = rect.height + 'px';

var stickyT = stickyEl.offsetTop;
window.onscroll = function(e) {
    var scrollT = document.body.scrollTop;
    // console.log(scrollT, stickyT);
    if (scrollT > stickyT) {
        stickyEl.classList.add('fixed-top');
    }
    else {
        stickyEl.classList.remove('fixed-top');
    }
};

상단 고정 요소를 동일한 높이의 플레이스홀더로 감쌉니다. 임계 위치에서 stickyElfixed로 빠져나가더라도 stickyHolder가 공간을 유지하므로 아래 요소가 밀고 올라오지 않아 페이지가 떨리지 않습니다.

이 방식에도 몇 가지 문제가 있습니다. 상단 고정 요소 위의 요소들이 로딩이 느리다면 실제보다 작은 stickyT 값을 얻게 되거나 심지어 0이 될 수도 있습니다(위에 아주 큰 배너 이미지가 있는 경우 등). 따라서 기본 이미지 플레이스홀더(base64)를 함께 사용하거나, 우선 min-height로 공간을 확보한 뒤 상단 이미지가 로드(onload)될 때 stickyT를 수정해야 합니다.

3. 모바일 해결 방안

원리적으로는 PC 방식을 그대로 가져올 수 있습니다. 안드로이드 4.0 이상에서는 실제로 가능하지만, iOS에서는 거의 모든 기기에서 통하지 않습니다.

Android scroll

안드로이드 4.0의 scroll 이벤트는 실시간성이 떨어지지만(자체 스로틀링 느낌), 4.1 이후에는 PC와 거의 차이가 없습니다.

The Android browser in Ice Cream Sandwich fires the event but doesn’t feel very responsive and only sporadically re-paints the DOM to move the blue box. Luckily, Jelly Bean’s Android browser handles this example perfectly; everything is updated and rendered smoothly as the user scrolls.

(참고 자료 1 인용)

사용자가 스크롤하는 동안 scroll 이벤트가 계속 발생하므로 수동으로 스로틀링을 해주어야 합니다. 이것이 바로 우리가 원하는 효과입니다. 만약 scroll 이벤트 자체에 스로틀링이 걸려 있다면 임계점 판단을 놓치기 쉬워 고정 요소가 툭 끊기듯 움직이게 되고 부드러운 경험을 줄 수 없습니다.

iOS scroll

iOS 8 미만의 Safari 및 UIWebViewscroll 이벤트에 큰 제한을 둡니다:

손가락으로 화면 스와이프 -> 스크롤 -> 손가락 뗌 -> 관성 스크롤 -> 스크롤 정지

이 전체 과정에서 스크롤이 완전히 멈췄을 때만 scroll 이벤트가 단 한 번 발생합니다. , iOS 8 미만에서 scroll은 사실상 scrollend가 된 것입니다. 스크롤을 감시하여 위치를 판단하는 방식은 완전히 무용지물이 됩니다. 부드러운 고정 효과 대신, 임계점을 한참 지나 스크롤이 멈추고 나서야 요소가 목표 위치로 툭 튀어 오르게 되며, 사용자 경험이 매우 나쁘고 참기 힘든 수준입니다.

scroll을 사용할 수 없으므로 타이머로 scrollTop을 읽거나, touchmove, iscroll을 사용하는 등 다른 방법을 고려해야 합니다.

어떤 선배님이 상세한 테스트를 진행하셨습니다. 참고 자료 1을 확인하세요.

타이머는 손가락이 화면에서 떨어지지 않았을 때 실행되지 않으며, touchmove는 빈도가 충분하고 scrollTop도 얻을 수 있지만, touchend 이후의 관성 스크롤 기간 동안은 사용할 수 있는 이벤트가 전혀 없습니다. 이 구간의 scrollTop을 알 수 없으며 관성 스크롤 거리(감속 운동)를 예측하기 어렵고, 심지어 iOS 버전마다 이 거리의 계산 방식이 동일한지도 불확실합니다.

iscroll과 같은 가상 스크롤 방식은 실시간으로 스크롤 위치를 가져올 수 있습니다. iScroll에는 이를 위한 전용 버전이 있습니다:

iscroll-probe.js, probing the current scroll position is a demanding task, that's why I decided to build a dedicated version for it. If you need to know the scrolling position at any given time, this is the iScroll for you. (I'm making some more tests, this might end up in the regular iscroll.js script, so keep an eye on it).

iOS 8 이상의 Safari 및 WKWebView는 손가락이 화면에 있든 없든, 관성 스크롤 중이든 상관없이 scroll 이벤트를 계속 발생시킵니다. 하��만 iOS 8 이상의 UIWebView에서도 scroll 제한은 여전히 존재합니다.

iOS 8 미만 기기나 모든 iOS 버전의 UIWebView를 지원해야 한다면 이 방식은 막막하므로 scroll은 잊어버려야 합니다.

sticky

scroll 방안이 통하지 않더라도 iOS는 다른 방식을 제공합니다: position: sticky입니다. iOS 6.1부터 이미 지원해 왔으며, 최근 Chrome 56부터 지원하기 시작했습니다.

이 CSS 규칙은 상단 고정을 전담하며 일반적인 사용법은 다음과 같습니다:

.sticky {
    // 초기 위치를 지날 때 자동으로 상단 고정
    position: -webkit-sticky;
    position: sticky;
    // 고정 시의 위치
    top: 0;
    left: 0;
    // 하위 요소보다 높은 z-index 설정
    z-index: 9999;
}

임계 위치를 지나기 전에는 position: relative와 비슷하게 동작하지만(공간을 차지하고 자식 요소의 위치 기준이 됨), topleft는 적용되지 않습니다.

임계 위치를 지나면 position: fixed와 비슷하게 동작하여 topleft가 적용되어 화면 가시 영역에 고정되지만, 페이지는 떨리지 않고 원래 차지하던 공간은 그대로 유지됩니다(자체 플레이스홀더 기능이 포함된 느낌입니다).

고정 효과는 매우 부드러우며 안드로이드의 scroll 방안보다 더 쾌적한 경험을 제공하지만 한계가 명확한데, 실시간으로 고정 상태를 알 수 없어서 이와 연관된 다양한 효과가 제한됩니다. 예를 들어 상단 고정 탭 리스트의 경우:

[caption id="attachment_1211" align="alignnone" width="255"]sticky-tab sticky-tab[/caption]

비고정 상태일 때는 리스트 부분을 스와이프하여 페이지가 스크롤되게 하고 고정 상태로 전환되면 여러 탭 리스트를 매끄럽게 전환하며 브라우징 상태가 서로 영향을 주지 않아야 합니다.

고정 상태일 때는 현재 탭 리스트를 끝까지 스와이프하면 페이지가 스크롤되어 비고정 상태로 전환되어야 합니다.

즉, 비고정 상태일 때는 탭 리스트를 스크롤할 수 없게 하고(overflow-y: hidden), 고정 상태일 때는 스크롤할 수 있게(overflow-y: auto) 해야 합니다.

하지만 iOS sticky는 우리가 제어할 수 없으며 고정 상태를 실시간으로 알 수도 없습니다. 고정 상태를 파악하려면 다시 처음의 문제로 돌아가게 됩니다. 페이지 스크롤 중에 어떻게 실시간으로 스크롤바 위치를 알 수 있을까요? CSS sticky이 문제를 해결해 주지 못합니다.

필자는 아직 적절한 해결책을 찾지 못했습니다. 현재 방안은 탭 브라우징 상태의 독립성을 포기하고 여러 탭이 body의 스크롤바를 공유하며 탭 전환 시 이전 위치로 스크롤을 되돌리는 방식입니다. 이렇게 하면 고정 상태 판단을 피할 수 있지만 탭 리스트 간의 매끄러운 전환이라는 완벽한 경험은 희생됩니다.

만약 새로운 아이디어나 좋은 방법, 또는 완성된 해결책이 있다면 꼭 알려주시면 감사하겠습니다.

4. 온라인 데모

5. 요약

  • 일반 요소 고정: 안드로이드는 scroll 방안을 사용하고 성능 향상을 위해 수동으로 스로틀링을 적용합니다. iOS는 CSS sticky를 사용하며, iOS 8 미만이나 모든 버전의 UIWebView 호환이 필요 없다면 scroll 방안을 사용할 수도 있습니다.

  • 상단 고정 탭 리스트: 마땅한 해결책이 없으며 현재는 매끄러운 전환을 포기하는 방식을 임시로 사용 중입니다.

페이지 전체에 iScroll을 적용하는 것은 위험한 방안입니다. 페이지가 복잡하다면 쉽게 시도하지 마세요. 현재 복잡하지 않더라도 나중에 복잡해지지 않으리라는 보장이 없습니다.

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성