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

드래그 앤 드롭 구현 9가지 — 손 뗀 뒤 속도까지 잇는 계산

드래그 앤 드롭 구현이라는 말은 보통 잡고 옮기는 동작 하나를 가리키지만, 여기 아홉 개는 손을 뗀 뒤까지 계산이 이어진다. 자석 격자가 24px 안쪽에서 아이콘을 붙잡고, 고무줄처럼 늘어난 경계가 그 손을 다시 놓아주며, 손을 뗀 순간의 속도가 나머지 거리를 밀고 간다.

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

드래그 앤 드롭 구현 9가지 — 손을 뗀 뒤에도 계속되는 계산

아홉 항목 전부 포인터 이벤트 표준의 pointerdown·pointermove·pointerup 을 공유한다(HTML5 드래그 앤 드롭 API 는 어디에도 쓰지 않는다). 그 위에서 포인터 위치와 주변 요소를 매 프레임 다시 비교하는 쪽(01·03·04·08), 삭제 영역 위에 머문 시간을 재는 쪽(07), 손을 놓는 순간의 값으로 계산을 이어가는 쪽(02·05·06)으로 나뉘고, 09 는 이 중 세 계산을 한 보드 위에 그대로 얹었다.

패턴별 코드

항목마다 물리 계산이 걸린 함수 구간만 소스에서 그대로 떼어 왔고, 마크업·스타일 전체는 데모 아래 압축 파일 안에 들어 있다.

01거리 안에 들어오면 칸이 끌어당기는 격자 자석 드래그

아이콘을 드래그하면 매 프레임 nearestCell 이 각 격자 칸과의 거리를 계산해 GRID_MAGNET_SNAP_PX(24px) 안으로 들어온 칸에 강조 표시를 건다. 손을 놓으면 springTo 가 settleTick 을 돌려 SPRING_STIFFNESS·SPRING_DAMPING 값으로 목표 칸까지 스프링처럼 당겨 붙인다. 01 항목은 누적면적 14.1%·강도 평균 177.0·23프레임 중 20프레임에서 움직였다.

거리 안에 들어오면 칸이 끌어당기는 격자 자석 드래그
새 창에서 열기

01 · 거리 안에 들어오면 칸이 끌어당기는 격자 자석 드래그 — 측정 누적면적 14.062% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GRID_MAGNET_SNAP_PXSPRING_STIFFNESSSPRING_DAMPING
  var raf = 0, lastTs = null;
  function settleTick(ts) {
    if (lastTs === null) lastTs = ts;
    var dt = Math.min((ts - lastTs) / 1000, 0.032);
    lastTs = ts;
    var fx = (settleTarget.x - pos.x) * SPRING_STIFFNESS - vel.x * SPRING_DAMPING;
    var fy = (settleTarget.y - pos.y) * SPRING_STIFFNESS - vel.y * SPRING_DAMPING;
    vel.x += fx * dt; vel.y += fy * dt;
    pos.x += vel.x * dt; pos.y += vel.y * dt;
    applyPos();
    var rest = Math.hypot(vel.x, vel.y) < 2 && Math.hypot(settleTarget.x - pos.x, settleTarget.y - pos.y) < 0.6;
    if (!rest) { raf = requestAnimationFrame(settleTick); return; }
    pos.x = settleTarget.x; pos.y = settleTarget.y; vel.x = 0; vel.y = 0;
    applyPos();
    settling = false;
    lastTs = null;
  }

02원점과 줄로 이어진 듯 늘어나는 탄성 테더 드래그

아이콘을 원점에서 끌어낼수록 drawTether 가 두 점 사이 거리·각도를 계산해 얇은 선의 길이·각도·TETHER_THICKNESS_BASE_PX 기준 굵기를 매 프레임 다시 그린다. 손을 놓으면 settleTick 이 TETHER_SPRING_STIFFNESS·TETHER_SPRING_DAMPING 값으로 원점을 향해 스프링처럼 당겨 되돌린다. 02 항목은 누적면적 9.7%·강도 평균 39.6·23프레임 중 22프레임에서 움직였다.

원점과 줄로 이어진 듯 늘어나는 탄성 테더 드래그
새 창에서 열기

02 · 원점과 줄로 이어진 듯 늘어나는 탄성 테더 드래그 — 측정 누적면적 9.738% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TETHER_SPRING_STIFFNESSTETHER_SPRING_DAMPINGTETHER_THICKNESS_BASE_PX
  function drawTether() {
    var or = origin.getBoundingClientRect();
    var ir = icon.getBoundingClientRect();
    var ox = or.left + or.width / 2, oy = or.top + or.height / 2;
    var ix = ir.left + ir.width / 2, iy = ir.top + ir.height / 2;
    var dx = ix - ox, dy = iy - oy;
    var length = Math.hypot(dx, dy);
    var angle = Math.atan2(dy, dx) * 180 / Math.PI;
    var thickness = Math.max(1.5, TETHER_THICKNESS_BASE_PX - length * 0.012);
    var boardRect = root.getBoundingClientRect();
    line.style.left = (ox - boardRect.left) + 'px';
    line.style.top = (oy - boardRect.top) + 'px';
    line.style.width = length + 'px';
    line.style.height = thickness + 'px';
    line.style.transform = 'rotate(' + angle + 'deg)';
    line.style.opacity = length < 2 ? '0' : '1';
  }

03경계 밖으로 밀면 늘어났다 되돌아오는 고무 바운더리

아이콘이 컨테이너 경계를 넘으면 overflowOf 가 넘어간 거리를 계산하고 applyVisual 이 그 비율만큼 scale·skew 를 섞어 고무처럼 눌린 모양을 그린다. 그 거리는 BOUNDARY_RUBBER_MAX_PX(40px) 에서 더 늘지 않게 클램프되고, 손을 놓으면 BOUNDARY_SPRING_STIFFNESS·BOUNDARY_SPRING_DAMPING 스프링이 경계 안으로 되돌린다. 03 항목은 누적면적 10.5%·강도 평균 99.0·23프레임 중 20프레임에서 움직였다.

경계 밖으로 밀면 늘어났다 되돌아오는 고무 바운더리
새 창에서 열기

03 · 경계 밖으로 밀면 늘어났다 되돌아오는 고무 바운더리 — 측정 누적면적 10.521% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BOUNDARY_RUBBER_MAX_PXBOUNDARY_SPRING_STIFFNESSBOUNDARY_SPRING_DAMPING
  function overflowOf(clientX, clientY) {
    var b = bounds();
    var ox = 0, oy = 0;
    if (clientX < b.minX) ox = clientX - b.minX;
    else if (clientX > b.maxX) ox = clientX - b.maxX;
    if (clientY < b.minY) oy = clientY - b.minY;
    else if (clientY > b.maxY) oy = clientY - b.maxY;
    return { x: ox, y: oy };
  }

  function applyVisual(clampedX, clampedY, overflow) {
    var ratioX = Math.min(1, Math.abs(overflow.x) / BOUNDARY_RUBBER_MAX_PX);
    var ratioY = Math.min(1, Math.abs(overflow.y) / BOUNDARY_RUBBER_MAX_PX);
    var ratio = Math.max(ratioX, ratioY);
    var scaleStretch = 1 + ratio * 0.28;
    var scaleSquash = 1 - ratio * 0.22;
    var skewX = overflow.x !== 0 ? (overflow.x > 0 ? 1 : -1) * ratioX * 6 : 0;
    var skewY = overflow.y !== 0 ? (overflow.y > 0 ? 1 : -1) * ratioY * 6 : 0;
    var sx = ratioX >= ratioY ? scaleStretch : scaleSquash;
    var sy = ratioY >= ratioX ? scaleStretch : scaleSquash;
    icon.style.transform = 'translate(' + clampedX.toFixed(1) + 'px,' + clampedY.toFixed(1) + 'px) scale(' + sx.toFixed(3) + ',' + sy.toFixed(3) + ') skew(' + skewX.toFixed(2) + 'deg,' + skewY.toFixed(2) + 'deg)';
    root.classList.toggle('is-stretch', ratio > 0.02);
  }

04이웃 중간점을 지나면 자리를 맞바꾸는 스왑 드래그

드래그 칩의 화면 중심이 옆 슬롯 중간점을 넘는 순간을 moveDrag 가 매 프레임 계산해 moveInOrder 로 두 칩의 순서를 맞바꾼다. 자리를 바꾼 두 칩은 kickSpring 이 걸어 둔 SWAP_FLIP_STIFFNESS·SWAP_FLIP_DAMPING 스프링으로 새 자리까지 미끄러지듯 이동한다. 04 항목은 누적면적 22.5%·강도 평균 103.3·23프레임 중 17프레임에서 움직였다.

이웃 중간점을 지나면 자리를 맞바꾸는 스왑 드래그
새 창에서 열기

04 · 이웃 중간점을 지나면 자리를 맞바꾸는 스왑 드래그 — 측정 누적면적 22.487% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SWAP_FLIP_STIFFNESSSWAP_FLIP_DAMPINGKEY_STEP_SLOTS
  function moveDrag(clientX) {
    if (!STEP) STEP = measureStep();
    var deltaX = clientX - pointerStartX;
    var rawX = dragStartX + deltaX;
    applyChip(DRAGGED_ID, rawX);
    // 중점 판정: 드래그 칩의 화면 중심을 재서 어느 슬롯에 가장 가까운지 계산
    var chipRect = dragChip.getBoundingClientRect();
    var trackRect = track.getBoundingClientRect();
    var centerX = chipRect.left + chipRect.width / 2 - trackRect.left;
    var targetSlot = Math.max(0, Math.min(chips.length - 1, Math.round((centerX - STEP / 2) / STEP)));
    while (targetSlot !== slotOf(DRAGGED_ID)) {
      var from = slotOf(DRAGGED_ID);
      var step = targetSlot > from ? 1 : -1;
      var to = from + step;
      moveInOrder(from, to);
      retarget();
      kickSpring();
      caption.textContent = '자리가 바뀌었어요';
    }
    state[DRAGGED_ID].x = rawX;

05중심을 기준으로 각도만 바뀌는 궤도 핸들 드래그

포인터 좌표를 angleFromPointer 가 중심점 기준 각도값으로 바꾸고, 핸들은 그 각도만큼만 ORBIT_RADIUS_PX(74px) 고정 반지름 원 위를 돈다. 손을 놓으면 inertiaTick 이 각속도에 ORBIT_DECAY_RATE 를 매 프레임 곱해 서서히 늦추다가 멈춘다. 05 항목은 누적면적 11.3%·강도 평균 186.6·23프레임 중 23프레임에서 움직였다.

중심을 기준으로 각도만 바뀌는 궤도 핸들 드래그
새 창에서 열기

05 · 중심을 기준으로 각도만 바뀌는 궤도 핸들 드래그 — 측정 누적면적 11.255% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ORBIT_RADIUS_PXORBIT_DECAY_RATEORBIT_KEY_STEP_DEG
  function angleFromPointer(clientX, clientY) {
    var hr = hub.getBoundingClientRect();
    var hx = hr.left + hr.width / 2, hy = hr.top + hr.height / 2;
    return Math.atan2(clientY - hy, clientX - hx);
  }

  function shortestDelta(a, b) {
    var d = b - a;
    while (d > Math.PI) d -= Math.PI * 2;
    while (d < -Math.PI) d += Math.PI * 2;
    return d;
  }

  function inertiaTick(ts) {
    if (lastTs === null) lastTs = ts;
    var dt = Math.min((ts - lastTs) / 1000, 0.032);
    lastTs = ts;
    angle += angularVel * dt;
    angularVel *= Math.pow(ORBIT_DECAY_RATE, dt * 60);
    applyHandle();
    if (Math.abs(angularVel) > 0.02) { raf = requestAnimationFrame(inertiaTick); return; }
    angularVel = 0;
    lastTs = null;

06놓는 속도가 남은 거리를 미는 관성 플링 드래그

손을 떼기 직전 이동량으로 구한 속도값을 flingTick 이 매 프레임 위치에 더하고, 경계에 닿으면 부호를 뒤집어 튕겨낸다. 그 속도는 FLING_DECAY_RATE(0.92) 를 매 프레임 곱해 점점 줄어들다가 자연스럽게 멈춘다. 06 항목은 누적면적 9.3%·강도 평균 55.6·23프레임 중 17프레임에서 움직였다.

놓는 속도가 남은 거리를 미는 관성 플링 드래그
새 창에서 열기

06 · 놓는 속도가 남은 거리를 미는 관성 플링 드래그 — 측정 누적면적 9.251% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FLING_DECAY_RATEKEY_IMPULSE_PX_SflingTick
  function flingTick(ts) {
    if (lastTs === null) lastTs = ts;
    var dt = Math.min((ts - lastTs) / 1000, 0.032);
    lastTs = ts;
    pos.x += vel.x * dt;
    pos.y += vel.y * dt;
    var b = bounds();
    var origin = ballOrigin();
    var cx = origin.x + pos.x, cy = origin.y + pos.y;
    if (cx < b.minX) { pos.x += (b.minX - cx); vel.x = Math.abs(vel.x); root.classList.add('is-bounce'); }
    else if (cx > b.maxX) { pos.x -= (cx - b.maxX); vel.x = -Math.abs(vel.x); root.classList.add('is-bounce'); }
    if (cy < b.minY) { pos.y += (b.minY - cy); vel.y = Math.abs(vel.y); root.classList.add('is-bounce'); }
    else if (cy > b.maxY) { pos.y -= (cy - b.maxY); vel.y = -Math.abs(vel.y); root.classList.add('is-bounce'); }
    vel.x *= Math.pow(FLING_DECAY_RATE, dt * 60);
    vel.y *= Math.pow(FLING_DECAY_RATE, dt * 60);
    applyBall();
    var speed = Math.hypot(vel.x, vel.y);
    if (speed > 4) { raf = requestAnimationFrame(flingTick); return; }
    vel.x = 0; vel.y = 0;
    root.classList.remove('is-bounce');

07삭제 영역 위에서 떨리는 경고를 보이는 트래시 셰이크

아이콘이 삭제 영역 위에 머문 시간을 shakeTick 이 dwellMs 로 재고, 그 시간에 비례해 TRASH_SHAKE_MAX_PX(5px) 진폭으로 좌우로 떤다. 머문 시간이 TRASH_DELETE_DWELL_MS(550ms) 를 넘으면 문구가 '지금 놓으면 삭제돼요' 로 바뀌어 놓는 순간 삭제됨을 미리 알린다. 07 항목은 누적면적 17.5%·강도 평균 115.5·23프레임 중 17프레임에서 움직였다.

삭제 영역 위에서 떨리는 경고를 보이는 트래시 셰이크
새 창에서 열기

07 · 삭제 영역 위에서 떨리는 경고를 보이는 트래시 셰이크 — 측정 누적면적 17.508% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TRASH_DWELL_SHAKE_MSTRASH_SHAKE_MAX_PXTRASH_DELETE_DWELL_MS
  function shakeTick(ts) {
    if (deleted) return;
    var isOver = overlapsTrash();
    if (isOver && !overTrash) { dwellStart = ts; }
    overTrash = isOver;
    trash.classList.toggle('is-armed', overTrash);
    var shakeX = 0, shakeY = 0;
    var dwellMs = 0;
    if (overTrash && dwellStart !== null) {
      dwellMs = ts - dwellStart;
      var amp = Math.min(1, dwellMs / TRASH_DWELL_SHAKE_MS) * TRASH_SHAKE_MAX_PX;
      shakeX = Math.sin(ts / 40) * amp;
      shakeY = Math.cos(ts / 55) * amp * 0.6;
      if (dwellMs > TRASH_DELETE_DWELL_MS) {
        caption.textContent = '지금 놓으면 삭제돼요';
        root.classList.add('is-armed');
      } else {
        caption.textContent = '더 있으면 삭제 준비돼요';
        root.classList.remove('is-armed');
      }
    } else {
      caption.textContent = dragging ? '휴지통 위로 옮겨보세요' : '휴지통 위에 오래 있으면 떨리다 사라져요';
      root.classList.remove('is-armed');
    }
    applyIcon(shakeX, shakeY, 1);

08끌어당긴 사각형 안쪽만 고르는 라쏘 셀렉트 드래그

빈 자리를 끌면 drawLasso 가 시작점과 현재 좌표로 사각형을 그리고, updateLassoSelection 이 rectsIntersect 로 카드마다 겹침을 매 프레임 판정한다. LASSO_MIN_DRAG_PX(4px) 보다 짧게 움직이면 사각형을 그리지 않아 실수로 스친 클릭이 선택으로 번지지 않는다. 08 항목은 누적면적 14.5%·강도 평균 74.3·23프레임 중 17프레임에서 움직였다.

끌어당긴 사각형 안쪽만 고르는 라쏘 셀렉트 드래그
새 창에서 열기

08 · 끌어당긴 사각형 안쪽만 고르는 라쏘 셀렉트 드래그 — 측정 누적면적 14.549% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LASSO_MIN_DRAG_PXrectsIntersectrole="listbox"
  function rectsIntersect(a, b) {
    return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
  }

  function updateLassoSelection(rect) {
    items.forEach(function (el, idx) {
      var r = el.getBoundingClientRect();
      setSelected(idx, rectsIntersect(rect, r));
    });
  }

  function drawLasso(x0, y0, x1, y1) {
    var left = Math.min(x0, x1), top = Math.min(y0, y1);
    var w = Math.abs(x1 - x0), h = Math.abs(y1 - y0);
    var fieldRect = field.getBoundingClientRect();
    lasso.style.left = (left - fieldRect.left) + 'px';
    lasso.style.top = (top - fieldRect.top) + 'px';
    lasso.style.width = w + 'px';
    lasso.style.height = h + 'px';
    lasso.style.opacity = '1';
    return { left: left, top: top, right: left + w, bottom: top + h };
  }

09여러 물성을 한 보드에 모은 드래그 히어로 장면

endDrag 가 놓은 좌표를 순서대로 검사해 01 의 격자 자석 거리 안이면 스프링으로 칸에 붙이고, 아니면 03 의 경계 판정으로 넘어간다. 경계 안쪽이고 속도가 남아 있으면 06 과 같은 방식의 관성 계산이 이어받아 튕기게 하는 식으로, 세 계산이 한 판 위에서 순서대로 이어진다. 09 항목은 누적면적 8.1%·강도 평균 125.5·23프레임 중 18프레임에서 움직였다.

여러 물성을 한 보드에 모은 드래그 히어로 장면
새 창에서 열기

09 · 여러 물성을 한 보드에 모은 드래그 히어로 장면 — 측정 누적면적 8.088% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HERO_GRID_MAGNET_SNAP_PXHERO_BOUNDARY_RUBBER_MAX_PXHERO_FLING_DECAY_RATE
  function endDrag() {
    dragging = false;
    root.classList.remove('is-dragging');
    var o = iconOrigin();
    var cx = o.x + pos.x, cy = o.y + pos.y;
    var near = nearestCell(cx, cy);
    cells.forEach(function (c) { c.classList.remove('is-target'); });
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (near && near.dist < HERO_GRID_MAGNET_SNAP_PX) {
      caption.textContent = '자석 칸에 붙었어요';
      var target = { x: near.x - o.x, y: near.y - o.y };
      if (reduce) { pos.x = target.x; pos.y = target.y; applyIcon(1, 1); }
      else { cancelAnimationFrame(raf); settleSpring(target); }
      return;
    }
    var b = boardBounds();
    var overX = 0, overY = 0;
    if (cx < b.minX) overX = cx - b.minX; else if (cx > b.maxX) overX = cx - b.maxX;
    if (cy < b.minY) overY = cy - b.minY; else if (cy > b.maxY) overY = cy - b.maxY;
    if (overX !== 0 || overY !== 0) {
      caption.textContent = '경계 밖이라 안으로 되돌아가요';
      var clampX = Math.max(b.minX, Math.min(b.maxX, cx)) - o.x;
      var clampY = Math.max(b.minY, Math.min(b.maxY, cy)) - o.y;
      if (reduce) { pos.x = clampX; pos.y = clampY; applyIcon(1, 1); }
      else { cancelAnimationFrame(raf); settleSpring({ x: clampX, y: clampY }); }

값이 반응을 바꾸는 곳 — 자석 스냅 거리·고무줄 최대 늘어남·감속 계수

아홉 항목 중 세 개(02·06·09)는 아이콘 하나만 움직이고 배경·격자 전체는 그대로라 누적면적이 10% 밑으로 떨어진다 — 화면이 넓게 반응해야 커지는 수치일 뿐이라 컴포넌트 하나짜리 데모에서는 정상이고, 억지로 배경을 흔들지 않았다. 이 중 02 탄성 테더처럼 원점에서 짧게 당겼다 놓는 동작은 드래그 핸들 ui 로 옮겨 쓰기에도 알맞고, 04 처럼 한 줄로 늘어선 칩은 드래그 바 ui 형태의 순서 조정에도 그대로 맞는다.

항목 누적면적 % 강도 평균 움직인 프레임
01 격자 자석 드래그 14.1% 177.0 20/23
02 탄성 테더 드래그 9.7% 39.6 22/23
03 고무 바운더리 드래그 10.5% 99.0 20/23
04 스왑 드래그 22.5% 103.3 17/23
05 궤도 핸들 드래그 11.3% 186.6 23/23
06 관성 플링 드래그 9.3% 55.6 17/23
07 트래시 셰이크 17.5% 115.5 17/23
08 라쏘 셀렉트 드래그 14.5% 74.3 17/23
09 드래그 히어로 장면 8.1% 125.5 18/23

이 중 세 상수를 바꾸면 반응 속도가 달라진다. GRID_MAGNET_SNAP_PX 는 자석 격자가 아이콘을 끌어당기기 시작하는 거리를, BOUNDARY_RUBBER_MAX_PX 는 고무 경계가 최대로 늘어나는 거리를, FLING_DECAY_RATE 는 관성 플링이 감속하는 속도를 정한다. 이 세 상수는 바로 다음 변형 표에서 값을 직접 바꿔 비교한다.

어디서 깨지는가 — 터치 스크롤과의 충돌·경계 밖 좌표·키보드 대안 경로

터치 화면에서는 손가락으로 끄는 동작이 곧 페이지 스크롤 제스처와 같아서, touch-action: none 을 걸어 두지 않으면 사각형 선택이든 아이콘 이동이든 페이지 스크롤과 동시에 반응해 버린다 — 아홉 항목 모두 이 속성을 드래그 영역에 걸어 그 충돌을 막아 뒀다(08 라쏘 셀렉트는 선택 사각형이 그려지는 .ls__field 전체에 걸려 있다). 01·02·04·06·07 다섯 항목은 드래그 중 포인터 좌표를 그대로 옮겨 카드 밖으로 나가도 멈추지 않고, 03·09 두 항목만 그 좌표를 경계값 안쪽으로 미리 계산해(03 은 overflowOf 함수, 09 는 같은 계산을 moveDrag 안에 직접 풀어 써서) 클램프해 되돌린다. 포인터가 없어도 아홉 항목 전부 keydown 경로가 열려 있어 Enter 로 잡고 화살표로 옮긴 뒤 Enter 로 다시 놓을 수 있지만, 08 만은 이 잡기·놓기 대신 카드에 포커스를 두고 Enter 를 눌러 하나씩 고르는 다른 경로를 쓴다.

드래그 앤 드롭하는 법을 실무 화면에 그대로 붙일 때는 이 세 갈래(터치 충돌·좌표 클램프·키보드 경로)까지 같이 맞춰야 포인터 없이 쓰는 방문자도 같은 결과를 얻는다. 아홉 항목 전부를 담은 압축 파일을 열려면 dqbvrvq6 을 입력하면 된다.

변형 세 가지와 접근성

항목 상수 값 느낌
01 GRID_MAGNET_SNAP_PX 24 → 8 값을 늘리면 아이콘이 칸에서 멀리 있어도 미리 끌려가 붙고, 값을 줄이면 칸 바로 위까지 가야만 붙는다
03 BOUNDARY_RUBBER_MAX_PX 40 → 90 값을 줄이면 경계 밖으로 조금만 밀어도 금방 멈춰 튕기고, 값을 늘리면 한참 더 늘어난 뒤에야 멈춰 튕긴다
06 FLING_DECAY_RATE 0.92 → 0.98 값을 줄이면 손을 뗀 뒤 몇 프레임 만에 바로 멈추고, 값을 늘리면 놓은 속도 그대로 훨씬 오래 미끄러진다

접근성 처리는 항목 소스에 이미 박힌 matchMedia('(prefers-reduced-motion: reduce)') 분기를 그대로 읽은 것이다. 이 분기는 아홉 항목 전부에 있어 동작 줄임 설정을 켠 방문자에게는 스프링·관성·캐스케이드 없이 목표 상태로 바로 건너뛴다. 포인터 대신 키보드로 조작하는 경로도 아홉 항목 전부 열려 있어 Enter 로 잡고 화살표로 옮긴 뒤 Enter 로 놓을 수 있고, 08 라쏘 셀렉트는 카드마다 포커스를 옮긴 뒤 focus({ preventScroll: true }) 로 화면을 흔들지 않고 Enter 로 하나씩 고르는 별도 경로를 쓴다.

이 아홉 개를 다른 드래그 부품과 묶는 자리

이웃 카드끼리 순서를 바꾸는 화면이라면 드래그 앤 드롭 UI 리스트 — 목표 인덱스 계산을 얹은 9종 쪽 목표 인덱스 계산과 04 스왑 드래그를 나란히 두고 비교해볼 만하고, 파일을 끌어다 놓는 화면까지 같이 다뤄야 한다면 드래그 앤 드롭 파일 업로드 UI — 드롭존 9종, 파일 데이터 계산을 얹다 쪽 드롭존 판정 방식과 맞춰보는 편이 낫다.

드래그 앤 드롭 구현은 라이브러리 없이도 되는가?

아홉 항목 모두 라이브러리 없이 pointerdown·pointermove·pointerup 이벤트와 requestAnimationFrame 만으로 만들어졌고, HTML5 드래그 앤 드롭 API 인 dragstart·dataTransfer 는 어디에도 쓰지 않았다. 아이콘 하나를 옮기는 드래그 앤 드롭 아이콘 데모(01·02·03·07)든 카드 여러 장을 다루는 데모(04·08·09)든 좌표·시간을 계산하는 같은 방식 위에 얹혀 있어서, 압축 파일을 열어 index.html 그대로 페이지에 넣으면 별도 스크립트를 추가로 물릴 필요가 없다. 각도만 바꿔 도는 05 궤도 핸들처럼 회전 값을 다루는 컨트롤을 더 보고 싶다면 css 게이지 바 9종 — 손잡이가 계산으로 도는 원형 컨트롤 쪽 각도 계산과 나란히 봐도 좋다.

드래그 앤 드롭 구현을 붙이기 전에 자주 나오는 질문

드래그 앤 드롭하는 법을 라이브러리 없이 먼저 체험해볼 수 있나요?

맨 위 3×3 격자에서 아홉 항목을 전부 바로 눌러볼 수 있고, 압축 파일을 열면 항목마다 index.html 하나로 완결돼 있어 별도 설치 없이 그 자리에서 확인할 수 있다.

09 히어로 장면처럼 여러 물성을 한 화면에 합쳐도 성능에 문제가 없나요?

09 는 01 의 격자 자석·03 의 고무 경계·06 과 같은 방식의 관성 계산 세 가지를 한 보드 위에서 순서대로 검사할 뿐, 세 계산을 동시에 돌리지는 않는다 — endDrag 가 조건을 하나씩 확인해 그중 하나만 실행한다.

08 라쏘 셀렉트를 마우스 없이 키보드만으로 쓸 수 있나요?

가능하다. 사각형을 직접 그리는 대신 카드에 포커스를 옮긴 뒤 Enter 를 눌러 하나씩 고르는 방식이라, 여러 장을 고를 때는 사각형 드래그보다 느리지만 결과는 같다.

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

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