GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

css 스크롤바 디자인 9가지 — 관성과 반발로 움직인다

css 스크롤바 디자인은 손잡이 값 하나로 움직임이 갈리는 아홉 장면이다. 손끝이 트랙 끝에 닿는 그 짧은 순간, 손잡이가 한 번 밀려났다가 튕겨 돌아온다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 값 하나짜리 손잡이에서 시작해 값이 겹치는 손잡이로 끝나도록 늘어놓았다. 01은 두께 값과 반경 값만으로 트랙을 채우고, 02는 그 위에 포인터가 올라올 때만 켜지는 광택 값을 얹는다. 03과 04는 손을 놓거나 끝에 닿는 순간의 물리 값(속도·반발)을 계산하고, 05·06·08은 위치·개수·비율 같은 상태 값을 손잡이 모양이나 색으로 그대로 옮긴다. 07은 시간 값으로 손잡이를 지웠다 되살리고, 09는 앞선 계산 세 개를 손잡이 하나 위에 포갠다.

01굵기 값이 트랙을 채우는 기본 손잡이

트랙 두께 값 하나와 모서리 반경 값 하나만으로 손잡이를 그리고, 스크롤 위치 비율을 손잡이의 세로 이동 값(transform)에 그대로 옮긴다. 최소 길이 값(THUMB_MIN_PX)을 넉넉히 잡아 두어 내용이 아무리 길어져도 손잡이가 손끝보다 얇아지지 않는다.

굵기 값이 트랙을 채우는 기본 손잡이
새 창에서 열기

01 · 굵기 값이 트랙을 채우는 기본 손잡이 — 측정 누적면적 33.433% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

THUMB_MIN_PXKEY_STEP_PXsyncThumb
.sb__track {
  position: relative;
  width: $sp-2;
  border-radius: $r-pill;
  background: rgba(255, 255, 255, .1);
}

.sb__thumb {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  border-radius: $r-pill;
  background: $subject-blue;
  transition: transform $duration $easing;
  cursor: grab;
}

측정: 누적면적 33.4% · 강도 평균 56.0 · 23/23 프레임에서 움직였다.

02커서가 올라오면 광택이 쓸고 지나가는 손잡이

포인터가 손잡이 위로 들어오는 순간에만 광택 띠 하나가 위에서 아래로 한 번 쓸고 지나간다. 지속시간 값(GLOSS_SWEEP_MS) 520이 그대로 keyframes 시간이 되므로 값을 줄이면 스치듯 짧게, 늘리면 천천히 흐르듯 지나간다.

커서가 올라오면 광택이 쓸고 지나가는 손잡이
새 창에서 열기

02 · 커서가 올라오면 광택이 쓸고 지나가는 손잡이 — 측정 누적면적 33.401% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GLOSS_SWEEP_MStriggerGlosssyncThumb
.sb__thumbGloss {
  position: absolute;
  top: -20%;
  left: -60%;
  width: 40%;
  height: 140%;
  background: linear-gradient(115deg, transparent, rgba(255, 255, 255, .85), transparent);
  transform: translateY(-120%);
  pointer-events: none;
}

.sb__thumbGloss.is-sweeping {
  animation: glossSweep $duration linear;
}

@keyframes glossSweep {
  0%   { transform: translateY(-120%); }
  100% { transform: translateY(220%); }
}

측정: 누적면적 33.4% · 강도 평균 43.9 · 23/23 프레임에서 움직였다.

03손을 놓아도 관성 값만큼 더 미끄러지는 손잡이

드래그하다 손을 놓는 순간의 프레임당 이동 값이 속도 값에 저장되고, 매 프레임 감쇠 값(SCROLLBAR_INERTIA_DECAY, 0.92)을 곱해 조금씩 줄어들며 몇 프레임 더 미끄러진 뒤 멈춘다. 속도의 절대값이 문턱값(VEL_EPS, 0.4) 아래로 떨어지면 그 자리에서 완전히 멈춘다.

손을 놓아도 관성 값만큼 더 미끄러지는 손잡이
새 창에서 열기

03 · 손을 놓아도 관성 값만큼 더 미끄러지는 손잡이 — 측정 누적면적 33.617% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SCROLLBAR_INERTIA_DECAYcoastStepVEL_EPS
  thumb.addEventListener('pointermove', function (e) {
    if (!dragging) return;
    var trackLen = track.clientHeight;
    var thumbLen = thumb.clientHeight;
    var moveRange = Math.max(1, trackLen - thumbLen);
    var scale = maxScroll() / moveRange;
    var frameDeltaPx = (e.clientY - lastMoveY) * scale;
    vel = frameDeltaPx;
    lastMoveY = e.clientY;
    var totalDeltaPx = (e.clientY - dragStartY) * scale;
    viewport.scrollTop = clamp(dragStartScroll + totalDeltaPx, 0, maxScroll());
  });
  function coastStep() {
    if (Math.abs(vel) <= VEL_EPS) { vel = 0; return; }
    vel = vel * SCROLLBAR_INERTIA_DECAY;
    viewport.scrollTop = clamp(viewport.scrollTop + vel, 0, maxScroll());
  }

측정: 누적면적 33.6% · 강도 평균 57.3 · 23/23 프레임에서 움직였다.

04끝에 닿으면 밀려났다 튕겨 돌아오는 손잡이

트랙 끝에 닿는 순간 손잡이가 반발 거리 값(BOUNDARY_REBOUND_PX, 12)만큼 더 밀려났다가, 감쇠 값(SPRING_DECAY, 0.7)을 매 프레임 곱한 오차 값을 따라 제자리로 튕겨 돌아온다. 오차의 절대값이 문턱값(OVERSHOOT_EPS, 0.3) 아래로 떨어지면 스프링 계산을 멈춘다.

끝에 닿으면 밀려났다 튕겨 돌아오는 손잡이
새 창에서 열기

04 · 끝에 닿으면 밀려났다 튕겨 돌아오는 손잡이 — 측정 누적면적 13.787% · 움직인 프레임 12/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BOUNDARY_REBOUND_PXspringBackSPRING_DECAY
  function springBack() {
    if (Math.abs(overshoot) <= OVERSHOOT_EPS) { overshoot = 0; return false; }
    overshoot = overshoot * SPRING_DECAY;
    return true;
  }

  var startTime = null;
  var phaseAt = 0;
  var target = 'bottom';
  function demoTick(now) {
    if (startTime === null) { startTime = now; }
    var t = (now - startTime) / 1000;
    if (root.classList.contains('is-demo') && !dragging) {
      var cycle = t - phaseAt;
      if (cycle < 0.35) {
        var ms = maxScroll();
        var toward = target === 'bottom' ? ms : 0;
        viewport.scrollTop = toward;
        if (cycle > 0.28) {
          overshoot = target === 'bottom' ? BOUNDARY_REBOUND_PX : -BOUNDARY_REBOUND_PX;
        }
      } else {
        springBack();
      }

측정: 누적면적 13.8% · 강도 평균 144.1 · 12/23 프레임에서 움직였다.

05스크롤 위치 값에 따라 색이 옮겨가는 손잡이

스크롤 위치 비율 값을 CSS 사용자 정의 속성(--pos)에 그대로 써서, 손잡이와 트랙의 그라디언트 색 구간이 같은 값을 따라 함께 이동한다. 계산은 한 함수(applyPositionColor)에서만 일어나고 손잡이 크기·위치 계산(syncThumb)은 그 값을 그대로 넘겨받아 쓰기만 한다.

스크롤 위치 값에 따라 색이 옮겨가는 손잡이
새 창에서 열기

05 · 스크롤 위치 값에 따라 색이 옮겨가는 손잡이 — 측정 누적면적 33.326% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

applyPositionColorsyncThumbTHUMB_MIN_PX
  function applyPositionColor(ratio) {
    // 계산 레이어: 위치 비율 값을 손잡이 그라디언트 구간과 트랙 발광 톤에 같은 값으로 전파한다.
    thumb.style.setProperty('--pos', ratio.toFixed(3));
    track.style.setProperty('--pos', ratio.toFixed(3));
  }

  function syncThumb() {
    var trackLen = track.clientHeight;
    var total = viewport.scrollHeight;
    var visible = viewport.clientHeight;
    var thumbLen = clamp(trackLen * (visible / total), THUMB_MIN_PX, trackLen);
    var ms = maxScroll();
    var ratio = ms > 0 ? viewport.scrollTop / ms : 0;
    var thumbPos = (trackLen - thumbLen) * ratio;
    thumb.style.height = thumbLen.toFixed(1) + 'px';
    thumb.style.transform = 'translateY(' + thumbPos.toFixed(1) + 'px)';
    thumb.setAttribute('aria-valuenow', String(Math.round(ratio * 100)));
    applyPositionColor(ratio);
  }

측정: 누적면적 33.3% · 강도 평균 59.8 · 23/23 프레임에서 움직였다.

06카드 개수 값에 맞춰 늘어나는 가로 트랙

가로로 나열된 카드 개수 값이 전체 스크롤 너비를 정하고, 보이는 너비와의 비율 값이 손잡이 너비(thumbLen)로 그대로 바뀐다. 세로용 계산(syncThumb)과 거의 같은 식을 가로 축(scrollWidth·scrollLeft)으로만 바꿔 쓴 짝이다.

카드 개수 값에 맞춰 늘어나는 가로 트랙
새 창에서 열기

06 · 카드 개수 값에 맞춰 늘어나는 가로 트랙 — 측정 누적면적 26.656% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

maxScrollXsyncThumbXCARD_COUNT
  function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
  function maxScrollX() { return Math.max(0, viewport.scrollWidth - viewport.clientWidth); }

  function syncThumbX() {
    var trackLen = track.clientWidth;
    var total = viewport.scrollWidth;
    var visible = viewport.clientWidth;
    var thumbLen = clamp(trackLen * (visible / total), THUMB_MIN_PX, trackLen);
    var ms = maxScrollX();
    var ratio = ms > 0 ? viewport.scrollLeft / ms : 0;
    var thumbPos = (trackLen - thumbLen) * ratio;
    thumb.style.width = thumbLen.toFixed(1) + 'px';
    thumb.style.transform = 'translateX(' + thumbPos.toFixed(1) + 'px)';
    thumb.setAttribute('aria-valuenow', String(Math.round(ratio * 100)));
  }

측정: 누적면적 26.7% · 강도 평균 55.1 · 23/23 프레임에서 움직였다.

07멈추면 투명도 값이 줄어 사라지는 오버레이 손잡이

스크롤이 멈춘 뒤 흐른 시간 값이 지연 값(OVERLAY_HIDE_DELAY_MS, 600)을 넘으면 투명도 값이 줄기 시작해 서서히 옅어지고, 다시 스크롤하면 투명도 값이 즉시 1로 되돌아온다. 콘텐츠 위에 겹쳐 떠 있는 오버레이형이라 평소엔 화면을 가리지 않다가 손이 필요할 때만 나타난다.

멈추면 투명도 값이 줄어 사라지는 오버레이 손잡이
새 창에서 열기

07 · 멈추면 투명도 값이 줄어 사라지는 오버레이 손잡이 — 측정 누적면적 21.918% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

OVERLAY_HIDE_DELAY_MSOVERLAY_FADE_MSsyncThumb
    if (scrolledFlag) { lastActiveT = t; scrolledFlag = false; }
    var elapsedMs = (t - lastActiveT) * 1000;
    var opacity;
    if (elapsedMs <= OVERLAY_HIDE_DELAY_MS) {
      opacity = 1;
    } else if (reduce) {
      opacity = 0;
    } else {
      opacity = clamp(1 - (elapsedMs - OVERLAY_HIDE_DELAY_MS) / OVERLAY_FADE_MS, 0, 1);
    }
    track.style.opacity = opacity.toFixed(3);

    requestAnimationFrame(demoTick);
  }

  syncThumb();

측정: 누적면적 21.9% · 강도 평균 78.6 · 13/23 프레임에서 움직였다.

08손잡이 길이 값이 남은 분량을 그대로 보여주는 스크롤바

전체 콘텐츠 높이와 보이는 영역의 비율 값을 손잡이 길이 값에 그대로 반영하고, 같은 비율 값에서 100을 뺀 나머지를 퍼센트 배지 글자로 옮긴다. 손잡이 길이와 배지 숫자가 같은 값 하나에서 나오므로 둘이 어긋날 일이 없다.

손잡이 길이 값이 남은 분량을 그대로 보여주는 스크롤바
새 창에서 열기

08 · 손잡이 길이 값이 남은 분량을 그대로 보여주는 스크롤바 — 측정 누적면적 27.411% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

THUMB_MIN_PXsyncThumbremaining
  function syncThumb() {
    var trackLen = track.clientHeight;
    var total = viewport.scrollHeight;
    var visible = viewport.clientHeight;
    var lengthRatio = clamp(visible / total, 0, 1);
    var thumbLen = clamp(trackLen * lengthRatio, THUMB_MIN_PX, trackLen);
    var ms = maxScroll();
    var scrollRatio = ms > 0 ? viewport.scrollTop / ms : 0;
    var thumbPos = (trackLen - thumbLen) * scrollRatio;
    thumb.style.height = thumbLen.toFixed(1) + 'px';
    thumb.style.transform = 'translateY(' + thumbPos.toFixed(1) + 'px)';
    thumb.setAttribute('aria-valuenow', String(Math.round(scrollRatio * 100)));
    var remaining = Math.round((1 - scrollRatio) * 100);
    badge.textContent = '남은 분량 ' + clamp(remaining, 0, 100) + '%';
  }

측정: 누적면적 27.4% · 강도 평균 48.0 · 23/23 프레임에서 움직였다.

09여러 값이 손잡이 하나 위에서 함께 반응하는 히어로 스크롤바

광택 값(02)·관성 감쇠 값(03)·경계 반발 값(04) 세 계산을 같은 손잡이 위에 그대로 포갰을 뿐, 새 계산은 하나도 더하지 않았다. 손을 놓으면 관성으로 미끄러지다 끝에서 반발로 튕기고, 호버하면 그 와중에도 광택이 따로 한 번 지나간다.

여러 값이 손잡이 하나 위에서 함께 반응하는 히어로 스크롤바
새 창에서 열기

09 · 여러 값이 손잡이 하나 위에서 함께 반응하는 히어로 스크롤바 — 측정 누적면적 36.332% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GLOSS_SWEEP_MSSCROLLBAR_INERTIA_DECAYBOUNDARY_REBOUND_PX
  function coastStep() {
    if (Math.abs(vel) <= VEL_EPS) { vel = 0; return; }
    vel = vel * SCROLLBAR_INERTIA_DECAY;
    var ms = maxScroll();
    var next = viewport.scrollTop + vel;
    if (!reduce && (next < 0 || next > ms)) {
      overshoot = next < 0 ? clamp(next, -BOUNDARY_REBOUND_PX, 0) : clamp(next - ms, 0, BOUNDARY_REBOUND_PX);
    }
    viewport.scrollTop = clamp(next, 0, ms);
  }
  function springBack() {
    if (Math.abs(overshoot) <= OVERSHOOT_EPS) { overshoot = 0; return; }
    overshoot = overshoot * SPRING_DECAY;
  }

측정: 누적면적 36.3% · 강도 평균 66.4 · 23/23 프레임에서 움직였다.

아홉 손잡이를 숫자로 겹쳐 보면 무엇이 드러나는가

NN 이름 누적면적 % 강도 평균 x/23
01 굵기 값이 트랙을 채우는 기본 손잡이 33.4 56.0 23/23
02 커서가 올라오면 광택이 쓸고 지나가는 손잡이 33.4 43.9 23/23
03 손을 놓아도 관성 값만큼 더 미끄러지는 손잡이 33.6 57.3 23/23
04 끝에 닿으면 밀려났다 튕겨 돌아오는 손잡이 13.8 144.1 12/23
05 스크롤 위치 값에 따라 색이 옮겨가는 손잡이 33.3 59.8 23/23
06 카드 개수 값에 맞춰 늘어나는 가로 트랙 26.7 55.1 23/23
07 멈추면 투명도 값이 줄어 사라지는 오버레이 손잡이 21.9 78.6 13/23
08 손잡이 길이 값이 남은 분량을 그대로 보여주는 스크롤바 27.4 48.0 23/23
09 여러 값이 손잡이 하나 위에서 함께 반응하는 히어로 스크롤바 36.3 66.4 23/23

04와 07만 23프레임을 다 채우지 못한다. 04는 끝에 닿아야만 반발 계산이 켜지니 화면 가운데를 오가는 프레임엔 값이 비어 있고, 07은 반대로 스크롤이 멈춘 뒤에야 투명도가 움직이니 계속 스크롤 중인 프레임엔 값이 없다. 강도 평균은 그런데도 04가 가장 높다 — 반발 거리 12px가 12프레임이라는 짧은 구간 안에서만 몰아치기 때문이다.

값 하나만 바꾸면 반응이 어떻게 갈라지는가

같은 자리 값 하나만 옮겨 보면 각 항목이 무엇으로 움직이는지 더 또렷하게 드러난다.

항목 바꾸는 값 줄이면 늘리면
03 SCROLLBAR_INERTIA_DECAY 0.92 → 0.98 손을 놓자마자 손잡이가 곧바로 멎어 관성이 거의 안 느껴진다 놓은 뒤에도 한참을 더 흘러가듯 미끄러진다
04 BOUNDARY_REBOUND_PX 12 → 32 끝에 닿아도 손잡이가 살짝만 물러났다 돌아온다 끝에서 크게 튀어나갔다가 요란하게 튕겨 돌아온다
07 OVERLAY_HIDE_DELAY_MS 600 → 1800 스크롤을 멈추자마자 손잡이가 서둘러 옅어진다 한참 그대로 떠 있다가 뒤늦게 옅어진다

어디서 깨지는가 — 클릭이 진짜 손잡이를 빗나가는 함정

네이티브 스크롤바를 지우려면 표준 속성 하나(scrollbar-width)와 엔진별 접두사 선택자를 함께 써야 하는데, 지우는 순간부터 드래그·키보드·포커스 링까지 전부 직접 만든 값으로 대신 책임져야 한다. 흔한 함정은 장식으로 얹은 요소가 진짜 손잡이 위를 가리는 경우다 — 02의 광택 띠는 pointer-events 값을 none 으로 꺼 두지 않으면 광택이 지나가는 순간 클릭이 손잡이가 아니라 광택 레이어에 먼저 맞는다. 직접 만든 아홉 개와 압축 파일 안 소스가 같은지 궁금하면, 압축을 열 때 wn4b2gqq 을 넣어 scss·js 를 그대로 대조해 보면 된다.

손잡이 크기 계산(syncThumb·syncThumbX)은 scroll 이벤트에만 물려 있다 — 이미지가 늦게 채워지는 등 스크롤 없이 콘텐츠 높이만 바뀌면 손잡이 길이가 그대로 남고 다음 스크롤이 일어나야 갱신된다. 콘텐츠가 늦게 채워지는 화면에 붙일 땐 준비가 끝난 시점에 같은 계산 함수를 한 번 더 불러 주는 값을 직접 추가해야 한다.

실무에서는 이 아홉 개를 어디에 붙이나

css 스크롤바 만들기를 검색해 처음 여기까지 왔다면 01의 두 값(두께·반경)만 손대도 감이 잡힌다. 사이드 내비게이션이나 긴 설정 패널처럼 세로로만 길어지는 화면에는 01이나 03이면 충분하고, 이미지·카드가 가로로 넘치는 갤러리에는 06을 그대로 깐다. 문서형 페이지처럼 어디까지 읽었는지가 중요한 화면에는 05나 08처럼 위치·비율 값을 손잡이 자체로 드러내는 쪽이 정보를 더 준다. 지도나 캔버스처럼 화면을 가리면 안 되는 곳엔 07의 오버레이형을 얹고, css 스크롤바 커스텀 요구가 인터랙션까지 포함한다면 09처럼 값을 겹쳐 쓰면 된다.

다른 스크롤 편과 어디서 갈라지는가

관성 스크롤 9종은 스크롤 위치 값 자체에 관성을 먹여 콘텐츠가 화면 안에서 흔들리거나 밀리는 편이고, 여기 03·04·09는 그 관성·반발 계산을 콘텐츠가 아니라 손잡이 하나에만 건다. 뷰 타임라인 전환은 스크롤 구간 값으로 요소 전환을 만드는 편이라 손잡이가 아예 등장하지 않는다. 스크롤 인디케이터 9종은 읽기 진행률을 눈에만 보여줄 뿐 손으로 잡아 끌 수 없는데, 여기 아홉 개는 모두 pointer 이벤트로 드래그하고 role="scrollbar"로 키보드까지 받는다는 점이 다르다. css 스크롤바 항상 보이게 두고 싶다면 07의 지연 값(OVERLAY_HIDE_DELAY_MS)을 아주 크게 잡아 오버레이가 거의 사라지지 않게 하면 된다. css 스크롤바 위치 변경까지 필요하다면 트랙 요소를 반대쪽 절대 위치로 옮기기만 하면 되고, 움직이는 값 자체는 01과 같다.

접근성 — 아홉 개가 reduced-motion 에서 남기는 것

01·03·04·09는 자동 재생 루프(requestAnimationFrame(demoTick))를 reduced-motion 이면 아예 시작하지 않는다 — 속도를 늦추는 게 아니라 처음부터 돌리지 않는 쪽이다. 03은 그래서 손을 놓은 뒤의 관성 미끄러짐까지 함께 사라지고 드래그 중 직접 반응만 남으며, 09도 같은 이유로 광택·관성·반발 세 계산이 전부 멈춘 채 손끝이 움직인 만큼만 반응한다. 07은 애니메이션 대신 값 자체를 바꾼다 — 경과 시간이 지연 값을 넘으면 서서히 옅어지는 대신 투명도가 곧장 0으로 떨어진다. 아홉 개 모두 손잡이에 role="scrollbar" aria-orientation aria-controls aria-valuenow·valuemin·valuemax 를 달고 tabindex="0" 으로 포커스를 받아, 방향키·Page Up·Page Down·Home·End 만으로 마우스 없이 스크롤할 수 있다. 탭으로 손잡이에 포커스가 오면 :focus-visible 규칙이 윤곽선(outline)을 그려 지금 키보드가 어느 손잡이를 쥐고 있는지 놓치지 않게 한다(MDN, prefers-reduced-motion).

손잡이를 직접 만들면 정말 느려질까 — 자주 나오는 질문 셋

손잡이를 직접 만들면 네이티브보다 느려지지 않나?

아니다. 손잡이는 트랙 위에 올려 둔 요소 하나를 transform 으로만 옮기므로 레이아웃을 다시 계산하지 않는다. 아홉 개 모두 위치·크기를 style.transform·style.height(또는 width)로만 바꾸고 나머지 값은 CSS 트랜지션에 맡긴다.

마우스 휠로 스크롤해도 손잡이가 따라오나?

그렇다. 손잡이 위치는 스크롤 이벤트 종류를 따지지 않고 viewport 의 실제 scrollTop·scrollLeft 값에서 매번 다시 계산(syncThumb·syncThumbX)하므로 휠·터치·키보드 어느 입력으로 스크롤해도 같은 값을 따라간다.

터치 기기에서도 같은 손잡이를 그대로 쓸 수 있나?

쓸 수 있다. pointerdown·pointermove·pointerup 이벤트로 짰기 때문에 마우스든 터치든 같은 코드가 반응한다. 다만 손가락은 커서보다 굵으니 트랙 두께 값을 조금 키워 주는 편이 잡기 쉽다 — 값 하나만 바꾸면 되는 부분이다. 가로형(06)도 마찬가지로 손가락 하나로 가로 스크롤과 손잡이 드래그를 같은 pointer 이벤트가 함께 처리하므로 별도의 터치 전용 코드를 더 짤 필요는 없다. 노트북 트랙패드처럼 휠 이벤트가 아주 잘게 쪼개져 들어오는 입력 장치에서도 손잡이는 scroll 이벤트가 일어난 뒤의 값만 읽으므로 입력 장치 종류를 가리지 않고 같은 계산을 반복한다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.