관성·모멘텀

Inertia & momentum

손을 떼는 순간의 속도를 그대로 이어받아 움직이다가, 마찰로 서서히 느려지며 멈추는 동작. 스크롤·드래그를 놓았을 때 뚝 멈추지 않고 "던진 만큼" 미끄러지는 느낌을 만듭니다.

다른 이름: Flick gestureMomentum scrollingFriction decay
···
html
<div class="track">
  <div class="rail"></div>
  <i class="hand" id="hand"></i>
  <i class="ball" id="ball"></i>
</div>
css
.track{position:relative;width:min(84%,440px);height:40px}
.rail{position:absolute;top:50%;left:0;right:0;height:2px;margin-top:-1px;background:var(--line);border-radius:2px}
.ball{position:absolute;top:50%;left:0;width:28px;height:28px;margin-top:-14px;border-radius:50%;background:var(--accent);box-shadow:0 4px 10px rgba(0,0,0,.2)}
.hand{position:absolute;top:-14px;left:0;width:14px;height:14px;border-radius:50% 50% 50% 4px;background:var(--accent-2);opacity:0;transform:rotate(-45deg)}
js
const ball = document.getElementById('ball');
const hand = document.getElementById('hand');
const track = document.querySelector('.track');
let x = 0, v = 0, w = 300, ballW = 28;
function measure() { w = track.clientWidth - ballW; }
measure();
window.addEventListener('resize', measure);

function flick() {
  v = w * (0.045 + Math.random() * 0.02);
  hand.style.opacity = '1';
  hand.style.transition = 'none';
  hand.style.left = '0px';
  requestAnimationFrame(() => {
    hand.style.transition = 'left .22s ease-out, opacity .22s ease-out .12s';
    hand.style.left = Math.min(w, x + v * 6) + 'px';
    hand.style.opacity = '0';
  });
}

let waiting = 0;
function loop() {
  if (v > 0.03) {
    x = Math.min(w, x + v);
    v *= 0.955;
    if (x >= w) v = 0;
  } else if (waiting <= 0) {
    x = 0;
    flick();
    waiting = 60;
  } else {
    waiting--;
  }
  ball.style.left = x + 'px';
  ball.style.transform = 'scale(' + (1 + Math.min(v, 6) / 40) + ',' + (1 - Math.min(v, 6) / 60) + ')';
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

핵심은 속도(velocity)라는 상태 하나를 프레임마다 유지하는 것입니다. 사용자가 드래그하는 동안 최근 몇 프레임의 이동량으로 속도를 추정해두고, 손을 떼는 순간 그 속도로 위치를 계속 갱신합니다(position += velocity). 매 프레임 velocity에 마찰 계수(0.9~0.97 사이)를 곱해 점점 0에 가까워지게 하고, 속도가 아주 작아지면(예: 0.02 미만) 애니메이션을 멈춥니다.

마찰 계수가 클수록(1에 가까울수록) 오래 미끄러지고, 작을수록 빨리 멎습니다. iOS의 스크롤이 유난히 "잘 미끄러진다"고 느껴지는 것도 이 계수 튜닝의 결과입니다. 경계에 부딪히면 단순히 멈추는 대신 반대 방향으로 감쇠시키는(바운스백) 처리를 더하면 러버밴드 효과와 자연스럽게 이어집니다.

duration 기반 애니메이션과 달리 "얼마나 걸릴지"를 미리 정하지 않고 물리량만 정의하기 때문에, 사용자가 세게 던지면 더 멀리 가고 살짝 밀면 조금만 갑니다 — 입력 강도가 결과에 비례하는 게 관성 모델의 핵심 가치입니다.

언제 쓰나

드래그로 놓아주는 리스트·캐러셀·지도 패닝처럼 사용자 입력의 세기가 결과에 반영돼야 하는 동작에 씁니다. 클릭으로 트리거되는 전환에는 필요 없습니다 — 클릭엔 "던지는 세기"가 없습니다.