
링크 드라퍼(Link Dropper) 서비스에 DOCK 기능을 추가하면서, 단순한 링크 고정보다 중요한 것은 사용자 경험(UX) 이었습니다.
링크를 고정하는 동작이 ‘자연스럽고 예측 가능’하게 이루어지도록 하기 위해선, 시각적 피드백, 즉각적인 반응, 실패 대비 처리 등 다양한 요소가 필요했죠.
이번 글에서는 @dnd-kit과 Zustand를 이용해 DnD 인터랙션을 어떻게 설계하고, 낙관적 UI를 어떤 방식으로 구현했는지를 예시 코드와 함께 공유합니다.
| 과제 | 해결 전략 |
|---|---|
| 드래그 도중의 시각 피드백 | DragOverlay 활용 |
| 드래그가 실수로 발생하는 문제 | 최소 거리 제약 설정 |
| API 응답 지연에 의한 UX 끊김 | 낙관적 UI로 선반영 후 실패 시 롤백 |
| 상태의 신뢰성 유지 | snapshot 기반 복구 로직 설계 |
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 조건문이 훨씬 명확해집니다.
드래그 중인 요소를 사용자에게 보여주는 시각적 피드백은 필수적 UX 요소입니다.
<DragOverlay>
{activeItem ? (
<div className="drag-overlay">
<LinkThumbnail link={activeItem.link} />
</div>
) : null}
</DragOverlay>
이 구성은 단순하지만 드래그가 "진짜 되는 것처럼 보이게 만드는 데" 큰 역할을 합니다.
기존의 React state 혹은 Redux가 아닌 Zustand를 사용한 이유는 다음과 같습니다:
interface DockState {
dockItems: DockItem[];
snapshot: DockItem[];
setDockItems: (items: DockItem[]) => void;
snapshotCurrent: () => void;
}
snapshot: 실패 시 복구할 수 있도록 이전 상태를 임시 저장setDockItems: 상태 갱신snapshotCurrent: 낙관적 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이 큰 역할을 하죠.
드래그 종료 이벤트에는 모든 분기처리 로직이 집약되어 있습니다.
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(/* ... */);
}
}
이번 DOCK 기능 구현은 작아 보이지만 깊은 고민이 담긴 작업이었습니다.
@dnd-kit의 유연함과 Zustand의 간결함, 그리고 낙관적 UI의 UX적 강점을 조화롭게 적용한 좋은 경험이었고,
이런 방식이 여러분의 프로젝트에도 영감을 줄 수 있기를 바랍니다.
링크 드라퍼는 단순한 저장 툴이 아닙니다.
정리하고, 수정하고, 다시 꺼내보게 만드는 링크 관리 도구를 지향하고 있습니다.
• 🔗 빠르고 간편한 링크 저장
• 🧠 저장한 링크를 폴더별로 정리
• 🌐 폴더를 친구에게 공유 가능
• ⚡ 크롬 익스텐션 원클릭 저장
👉 링크 드라퍼 사용하러 가기
👉 크롬 웹스토어에서 설치하기
서비스 업데이트
기능 꿀팁
카카오톡 채널을 통해 빠르게 받아보세요!
👉 카카오톡 채널 추가하기