@dnd-kit + Zustand로 완성한 DOCK 기능: 자연스러운 드래그 앤 드롭과 낙관적 UI의 조화

LinkDropper·2025년 9월 26일

Link Dropper

목록 보기
12/15
post-thumbnail

✨ 들어가며

링크 드라퍼(Link Dropper) 서비스에 DOCK 기능을 추가하면서, 단순한 링크 고정보다 중요한 것은 사용자 경험(UX) 이었습니다.
링크를 고정하는 동작이 ‘자연스럽고 예측 가능’하게 이루어지도록 하기 위해선, 시각적 피드백, 즉각적인 반응, 실패 대비 처리 등 다양한 요소가 필요했죠.

이번 글에서는 @dnd-kitZustand를 이용해 DnD 인터랙션을 어떻게 설계하고, 낙관적 UI를 어떤 방식으로 구현했는지를 예시 코드와 함께 공유합니다.


🎯 기능 설계의 핵심 포인트

문제 정의

  • 고정된 링크 영역(DOCK)과 최근 본 링크 목록 간의 이동이 필요하다.
  • 사용자가 링크를 드래그할 때 자연스럽게 느껴져야 하며, UX가 끊기면 안 된다.
  • 순서 변경, 고정 해제 등 다양한 액션을 드래그로 구현해야 한다.

주요 과제

과제해결 전략
드래그 도중의 시각 피드백DragOverlay 활용
드래그가 실수로 발생하는 문제최소 거리 제약 설정
API 응답 지연에 의한 UX 끊김낙관적 UI로 선반영 후 실패 시 롤백
상태의 신뢰성 유지snapshot 기반 복구 로직 설계

🧩 DnD 컨텍스트 설계

DnD 컨텍스트를 따로 Provider로 만들고, 이 안에서 draggable 요소들과 drop zone들을 관리합니다.
여기서 중요한 것은 item.id명명 규칙이에요.
recent-{id} vs dock-{id} 패턴으로 id를 정하면, 드래그 중 어디서 왔고 어디로 가는지를 쉽게 판단할 수 있습니다.

const items = useMemo(() => [
  ...recentLinks.map(link => ({ id: `recent-${link.id}` })),
  ...dockItems.map(item => ({ id: `dock-${item.id}` })),
  { id: "dock-drop-zone" },
], [recentLinks, dockItems]);

👉 이 구조는 이후 onDragEnd에서 drop 위치를 판별하는 기준이 되며, if 조건문이 훨씬 명확해집니다.


⚙️ 드래그 동작 중의 시각 피드백: DragOverlay

드래그 중인 요소를 사용자에게 보여주는 시각적 피드백은 필수적 UX 요소입니다.

<DragOverlay>
  {activeItem ? (
    <div className="drag-overlay">
      <LinkThumbnail link={activeItem.link} />
    </div>
  ) : null}
</DragOverlay>
  • 사용자는 지금 어떤 링크를 드래그 중인지 인식할 수 있음
  • 오버레이는 별도의 z-index로 띄워서 다른 UI와 겹치지 않게 처리

이 구성은 단순하지만 드래그가 "진짜 되는 것처럼 보이게 만드는 데" 큰 역할을 합니다.


🔄 상태 관리: 왜 Zustand인가?

기존의 React state 혹은 Redux가 아닌 Zustand를 사용한 이유는 다음과 같습니다:

  • 간결한 코드로 전역 상태를 관리 가능
  • snapshot 같은 일시적 저장 구조를 쉽게 구현할 수 있음
  • React Context 없이도 작동하며, DnD처럼 상태 변화가 자주 발생하는 구조에 적합

상태 구조 설계

interface DockState {
  dockItems: DockItem[];
  snapshot: DockItem[];
  setDockItems: (items: DockItem[]) => void;
  snapshotCurrent: () => void;
}
  • snapshot: 실패 시 복구할 수 있도록 이전 상태를 임시 저장
  • setDockItems: 상태 갱신
  • snapshotCurrent: 낙관적 UI 이전에 꼭 호출해야 하는 액션

🚀 낙관적 UI — 빠른 피드백을 위한 선택

드래그 후 링크를 DOCK으로 옮기는 순간, UI가 반응이 없다면 사용자 입장에서는 버그처럼 느껴질 수 있습니다.
이를 해결하기 위해 "먼저 UI를 갱신하고, 나중에 서버에 반영하는" 낙관적 UI 패턴을 도입했습니다.

async function handleDropToDock(link: Link, insertAfterId: number | null) {
  snapshotCurrent(); // 현재 상태 백업

  // 1. UI 선반영
  const reordered = computeNewOrder(dockItems, link, insertAfterId);
  setDockItems(reordered);

  try {
    // 2. 서버에 반영
    await api.addToDock(link.id);
    await api.updateDockOrder(reordered);
  } catch {
    // 3. 실패 시 롤백
    setDockItems(snapshot);
  }
}

낙관적 UI를 구현할 땐 항상 롤백 경로를 열어두는 게 중요합니다.
여기서 snapshot이 큰 역할을 하죠.


🧠 드래그 종료 처리: onDragEnd

드래그 종료 이벤트에는 모든 분기처리 로직이 집약되어 있습니다.
DnD 흐름을 정리하면 다음과 같습니다:

function handleDragEnd(event: DragEndEvent) {
  const { active, over } = event;
  if (!over) return;

  const activeId = active.id;
  const overId = over.id;

  if (activeId.startsWith("recent-") && overId === "dock-drop-zone") {
    const linkId = activeId.replace("recent-", "");
    onDropToDock(findLinkById(linkId), null);
    return;
  }

  if (activeId.startsWith("dock-") && overId.startsWith("dock-")) {
    onReorderDockItems(/* ... */);
  }
}
  • ID 패턴을 이용해 출발지와 도착지를 명확히 파악
  • 서로 다른 행동을 조건 분기로 구분 (추가 vs 정렬 변경)

🧪 결과: 기능 이상의 경험

✅ 사용성 개선 포인트

  • 즉각적인 피드백: 드래그만 하면 바로 반응
  • 자연스러운 애니메이션과 UI 흐름
  • API 실패 대응까지 고려된 안정성

🏁 마무리하며

이번 DOCK 기능 구현은 작아 보이지만 깊은 고민이 담긴 작업이었습니다.
@dnd-kit의 유연함과 Zustand의 간결함, 그리고 낙관적 UI의 UX적 강점을 조화롭게 적용한 좋은 경험이었고,
이런 방식이 여러분의 프로젝트에도 영감을 줄 수 있기를 바랍니다.


🧪 링크 드라퍼, 지금 베타 테스트 중입니다

링크 드라퍼는 단순한 저장 툴이 아닙니다.
정리하고, 수정하고, 다시 꺼내보게 만드는 링크 관리 도구를 지향하고 있습니다.

• 🔗 빠르고 간편한 링크 저장
• 🧠 저장한 링크를 폴더별로 정리
• 🌐 폴더를 친구에게 공유 가능
• ⚡ 크롬 익스텐션 원클릭 저장

👉 링크 드라퍼 사용하러 가기
👉 크롬 웹스토어에서 설치하기

💬 카카오톡 채널 추가하고 소식 받기

서비스 업데이트
기능 꿀팁
카카오톡 채널을 통해 빠르게 받아보세요!
👉 카카오톡 채널 추가하기

profile
“기록하는 습관을 도구로 만들다 — 두 개발자의 링크 드라퍼 구축기”

0개의 댓글