드래그 앤 드롭이 어느 정도 돌아가기 시작하면, 다음에 보이는 문제는 기능 부족이 아니라 느낌의 문제입니다. 처음에는 그럴듯하게 움직였는데, 항목 수가 늘어나고 삭제 버튼, 완료 체크, 수정, 검색, 필터까지 붙기 시작하면 드래그가 점점 버벅거립니다. 어떤 때는 마우스를 따라오지 못하고, 어떤 때는 표시선이 늦게 반응하고, 어떤 때는 드롭은 됐는데 화면이 한 박자 늦게 바뀌는 식입니다.
이 시점에서 초보자는 보통 두 가지를 먼저 의심합니다. “브라우저가 느린가?” 혹은 “AI가 코드를 이상하게 짠 건가?” 물론 그럴 수도 있습니다. 하지만 입문자 단계에서는 그보다 더 흔한 이유가 있습니다. 드래그 중에 너무 많은 일을 너무 자주 하고 있는 것입니다. 특히 드래그 관련 이벤트는 클릭보다 훨씬 자주 발생하는 편이라, 여기에 전체 렌더링이나 저장 같은 무거운 작업을 붙이면 체감이 바로 나빠집니다.
이번 글에서는 왜 리스트가 길어질수록 드래그가 불안정해지는지, 왜 특히 dragover 단계가 문제를 크게 만드는지, 그리고 초보자 기준에서는 어떤 정리부터 해야 가장 효과가 큰지 차근차근 풀어보겠습니다. 핵심은 거창한 최적화가 아닙니다. 실제 데이터가 바뀌는 순간과 잠깐 화면만 반응하면 되는 순간을 분리해서 보는 감각입니다.
| 겉으로 보이는 문제 | 실제로 일어나는 일 | 가장 먼저 봐야 할 방향 |
|---|---|---|
| 드래그가 점점 버벅인다 | 자주 발생하는 이벤트 안에서 너무 무거운 작업을 하고 있다 | 어떤 이벤트에서 실제 데이터가 바뀌는지 먼저 구분한다 |
| 표시선이 흔들리거나 늦게 반응한다 | dragover마다 전체 목록을 다시 그리거나 상태를 과하게 바꾸고 있다 | dragover에서는 가벼운 시각 상태만 다루는 편이 낫다 |
| 기능은 맞는데 코드를 조금만 고쳐도 다른 부분이 흔들린다 | 렌더링, 저장, 이벤트 연결이 한 덩어리로 묶여 있다 | 이벤트, 데이터 변경, 렌더링을 역할별로 다시 본다 |
| 항목 수가 늘수록 클릭도 둔해지는 것 같다 | 버튼마다 개별 이벤트를 너무 많이 다시 붙이고 있을 수 있다 | 클릭성 기능은 부모에서 묶어 받는 구조를 고려한다 |
이번 글의 핵심 한 줄
드래그가 느려질 때 가장 먼저 볼 것은 브라우저가 아니라, 자주 발생하는 이벤트 안에서 어떤 일을 반복하고 있는가다.
목차
- 왜 리스트가 길어지면 갑자기 느려질까
- dragover가 특히 버벅임을 만들기 쉬운 이유
- 매번 전체를 다시 그리면 생기는 문제
- 클릭 이벤트는 묶고, 드래그 상태는 가볍게 유지하기
- 초보자에게 가장 현실적인 개선 순서
- 초보자가 자주 하는 실수 6가지
- 마무리
왜 리스트가 길어지면 갑자기 느려질까
초반에는 잘 안 느껴집니다. 항목이 몇 개 안 되고, 버튼도 몇 개 안 되고, 이벤트도 적기 때문입니다. 그런데 목록이 길어지고 각 항목에 완료 체크, 삭제 버튼, 수정 버튼, 드래그 관련 처리까지 붙기 시작하면 화면이 한 번 갱신될 때 건드려야 하는 요소 수가 많아집니다. 여기에 검색과 필터, 정렬까지 같이 돌고 있으면 체감이 더 커집니다.
중요한 건 “항목이 많아서 느리다”가 정확한 설명은 아니라는 점입니다. 더 정확히 말하면 항목이 많아졌을 때도 똑같은 방식으로 전체를 자주 다시 만들고 있기 때문에 느려지는 경우가 많습니다. 특히 드래그는 마우스를 움직이는 동안 계속 반응해야 하므로, 작은 비효율도 클릭보다 훨씬 크게 느껴집니다.
| 늘어나는 것 | 왜 체감이 커지나 |
|---|---|
| 화면 요소 수 | 한 번 렌더링할 때 다시 만들거나 갱신할 대상이 많아진다 |
| 이벤트 연결 수 | 각 항목마다 클릭, 드래그 이벤트가 반복해서 붙을 수 있다 |
| 계산 횟수 | 검색, 필터, 정렬, 드롭 위치 계산이 동시에 돌면 한 번의 반응이 무거워진다 |
| 불필요한 반복 작업 | 실제 변화가 없는데도 저장, 렌더링, 표시 갱신을 계속 하면 체감이 급격히 떨어진다 |
즉, 성능 문제를 볼 때는 막연하게 “최적화해야 한다”보다 먼저 지금 어떤 일이 너무 자주 반복되는가를 보는 편이 좋습니다. 그리고 드래그 앤 드롭에서는 그 출발점이 거의 항상 dragover 입니다.
핵심 포인트
리스트가 길어질수록 문제가 되는 건 항목 수 자체보다, 자주 발생하는 이벤트 안에서 전체를 계속 다시 처리하는 구조다.
dragover가 특히 버벅임을 만들기 쉬운 이유
클릭 이벤트는 버튼을 한 번 눌렀을 때 한 번 반응합니다. 반면 dragover는 사용자가 항목 위를 지나가는 동안 매우 자주 발생합니다. 마우스를 조금만 움직여도 계속 들어오기 때문에, 이 안에 무거운 작업이 들어 있으면 바로 느려집니다.
초보자가 자주 만드는 첫 버전은 대개 이런 흐름입니다. dragover가 들어오면 현재 드롭 위치를 계산하고, 표시선을 갱신하고, 목록 전체를 다시 렌더링하고, 어떤 경우에는 저장까지 해버립니다. 기능은 맞을 수 있지만, dragover에 이런 작업이 다 몰리면 드래그가 불안정해지는 건 거의 자연스러운 결과입니다.
예를 들어 아래 같은 패턴은 처음엔 그럴듯해 보여도 dragover 안에서는 부담이 큽니다.
function handleDragOver(event, todoId, element) {
event.preventDefault();
currentDropId = todoId;
currentDropPosition = getDropPosition(event, element);
renderTodos();
}
이 코드는 dragover가 일어날 때마다 전체 목록을 다시 그립니다. 목록이 작을 때는 버틸 수 있어도, 항목이 늘어나면 표시선 하나 바꾸려고 목록 전체를 재생성하는 셈이 됩니다.
| 작업 | 왜 부담이 큰가 |
|---|---|
| 전체 render | 아직 데이터는 안 바뀌었는데 화면 전체를 계속 다시 만든다 |
| save 호출 | 실제 순서 변경은 drop 때 일어나는데, 중간 상태를 계속 기록할 이유가 없다 |
| 검색·정렬·필터 전체 재계산 후 재렌더 | 지나가는 순간마다 무거운 계산이 반복된다 |
dragover에서 해야 할 일은 생각보다 작게 잡는 편이 좋습니다. 대개는 아래 정도면 충분합니다.
- drop을 허용한다
- 현재 target과 before·after를 계산한다
- 정말 달라진 경우에만 표시선을 바꾼다
즉, dragover는 데이터를 바꾸는 단계가 아니라 드롭 의도를 가볍게 추적하는 단계로 보는 편이 훨씬 안정적입니다.
쉽게 말하면
dragover는 결과를 확정하는 순간이 아니라, 지금 어디로 들어갈지를 잠깐 보여주는 단계에 가깝다. 그래서 가볍게 움직여야 한다.
매번 전체를 다시 그리면 생기는 문제
입문 단계에서는 전체 렌더링이 가장 이해하기 쉬운 방식입니다. 데이터가 바뀌면 그냥 renderTodos를 다시 부르면 되니까요. add, delete, toggle, drop처럼 실제 데이터가 변한 뒤에는 이 방식이 꽤 잘 맞습니다. 문제는 아직 데이터가 안 바뀐 순간에도 이 방식을 그대로 쓰기 시작할 때입니다.
드래그 중에는 대부분의 시간이 “확정 전 상태”입니다. 사용자는 아직 놓지 않았고, 진짜 순서 변경도 일어나지 않았습니다. 이때 필요한 건 잠깐의 시각적 피드백이지, 목록 전체를 다시 만드는 일이 아닙니다. 그래서 드래그 관련 상태를 두 종류로 나눠서 보는 편이 좋습니다.
| 종류 | 예시 | 언제 바뀌나 | 저장 대상인가 |
|---|---|---|---|
| 실제 데이터 상태 | todos, order | drop이 완료됐을 때 | 예 |
| 임시 드래그 상태 | draggedId, currentDropId, currentDropPosition | dragstart, dragover, dragend 동안 계속 바뀐다 | 아니오 |
이 구분이 생기면 언제 전체 렌더링을 해야 하는지도 훨씬 선명해집니다.
| 이벤트 | 데이터 변경 | 저장 | 전체 렌더링 |
|---|---|---|---|
| dragstart | 아니오 | 아니오 | 대체로 아니오 |
| dragover | 아니오 | 아니오 | 대체로 아니오 |
| drop | 예 | 예 | 예 |
| delete, toggle, edit | 예 | 예 | 예 |
즉, 드래그 중에는 목록 전체를 계속 갈아엎지 않고, 실제 순서 변경이 확정되는 drop 시점에만 크게 반응하는 구조가 훨씬 안정적입니다.
function handleDrop(event) {
event.preventDefault();
if (!draggedId || !currentDropId || !currentDropPosition) return;
moveTodoByDropPosition(draggedId, currentDropId, currentDropPosition);
draggedId = null;
currentDropId = null;
currentDropPosition = null;
}
중요한 감각
드래그 중의 움직임은 대부분 임시 상태이고, drop만 실제 변경이다. 이 둘을 같은 무게로 다루면 화면이 쉽게 무거워진다.
클릭 이벤트는 묶고, 드래그 상태는 가볍게 유지하기
리스트가 길어질수록 또 하나 눈에 띄는 부분이 있습니다. 항목마다 완료 버튼, 삭제 버튼, 수정 버튼에 각각 이벤트를 붙여두면 렌더링할 때마다 그 연결도 다시 만들어질 수 있다는 점입니다. 이 방식이 틀렸다는 뜻은 아닙니다. 다만 리스트가 커지고 재렌더가 많아질수록 부담이 조금씩 쌓일 수 있습니다.
그래서 클릭성 기능은 한 단계 더 정리해볼 수 있습니다. 바로 부모 요소 하나에서 이벤트를 받아서 분기하는 방식입니다. 흔히 이벤트 위임이라고 부르지만, 입문자 입장에서는 그냥 “버튼마다 따로 받지 않고 목록 전체가 한 번 받는다” 정도로 이해해도 충분합니다.
예를 들어 삭제와 완료 체크처럼 클릭으로 끝나는 기능은 아래처럼 묶을 수 있습니다.
todoList.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
const action = button.dataset.action;
const id = Number(button.dataset.id);
if (action === 'delete') {
deleteTodo(id);
return;
}
if (action === 'toggle') {
toggleTodo(id);
return;
}
if (action === 'edit') {
startEditTodo(id);
}
});
이 구조의 장점은 꽤 분명합니다. 렌더링할 때 버튼마다 click 리스너를 다시 붙이는 대신, 목록 하나가 클릭을 받아서 그 안에서 어떤 버튼인지 구분하면 되기 때문입니다.
| 방식 | 장점 | 주의할 점 |
|---|---|---|
| 항목마다 개별 click 연결 | 처음에는 직관적이다 | 리스트가 길고 재렌더가 많아지면 반복 연결이 늘어날 수 있다 |
| 부모 하나에서 click 받기 | 구조가 덜 반복되고 수정 지점이 줄어든다 | 버튼의 data 정보와 action 구분이 필요하다 |
다만 드래그 이벤트까지 처음부터 전부 같은 방식으로 한 번에 묶으려 하면 입문자에게는 오히려 더 헷갈릴 수 있습니다. 클릭 계열은 부모 위임이 꽤 잘 맞지만, dragover와 drop은 요소 위치나 현재 요소 자체가 중요해서 첫 버전에서는 각 항목에서 받되, 그 안의 작업을 아주 가볍게 유지하는 편이 훨씬 이해하기 쉽습니다.
즉, 초보자 기준에서는 이렇게 나누면 좋습니다.
- 완료, 삭제, 수정 같은 클릭 기능: 부모에서 묶어 받기 고려
- dragstart, dragover, drop 같은 드래그 기능: 항목 단위로 두되, 전체 렌더 대신 가벼운 상태 변경만 하기
실전 감각으로 정리하면
클릭은 한곳으로 모으기 쉽고, 드래그는 자주 일어나기 때문에 가볍게 두는 편이 낫다. 둘을 똑같은 방식으로 보지 않는 것이 오히려 현실적이다.
초보자에게 가장 현실적인 개선 순서
성능 이야기가 나오면 갑자기 해야 할 일이 너무 많아 보일 수 있습니다. 하지만 입문자 단계에서는 한 번에 모든 걸 바꾸기보다, 체감 효과가 큰 것부터 손보는 편이 훨씬 낫습니다. 아래 순서대로만 정리해도 드래그 느낌이 꽤 달라질 수 있습니다.
- dragover에서 전체 render를 빼기
표시선만 바꾸고, 실제 데이터 변경과 전체 렌더는 drop 시점으로 미룹니다. - 현재 drop 대상과 위치가 진짜 바뀐 경우에만 표시선 업데이트
같은 항목의 같은 절반 위에 계속 머물고 있다면 매번 다시 바꿀 필요가 없습니다. - 저장은 drop에서만 하기
드래그 중간 상태까지 저장할 이유는 거의 없습니다. - 클릭성 버튼은 부모에서 묶어 받기 검토
완료, 삭제, 수정은 구조를 단순하게 만들 수 있습니다. - custom 모드에서만 재배치 허용
자동 정렬 모드와 성능 문제를 한 번에 섞지 않는 편이 좋습니다.
| 먼저 보기 좋은 것 | 이유 |
|---|---|
| dragover 안의 전체 렌더 제거 | 드래그 체감에 가장 직접적인 영향을 준다 |
| 표시선 갱신 최소화 | 같은 상태 반복 갱신을 줄여 준다 |
| save를 drop 시점으로 제한 | 의미 없는 중간 저장을 줄인다 |
| 클릭 이벤트 구조 정리 | 목록이 길어졌을 때 전체 구조가 덜 무거워진다 |
여기서 중요한 건 “완벽한 최적화”를 목표로 삼지 않는 것입니다. 초보자에게는 그보다 지금 이 이벤트에서 꼭 해야 하는 일만 남기는 습관이 훨씬 중요합니다. dragover에서 해야 할 일이 정말 표시선 갱신뿐이라면, 나머지는 과감히 나중 단계로 미루는 쪽이 맞습니다.
가장 현실적인 기준
성능을 개선한다는 건 무언가 대단한 기법을 쓰는 일이 아니라, 자주 일어나는 이벤트 안에서 꼭 필요한 일만 남기고 나머지를 빼내는 일에 더 가깝다.
초보자가 자주 하는 실수 6가지
리스트가 길어질 때의 버벅임은 특별한 한 가지 버그보다, 작아 보이는 비효율이 여러 군데 쌓여서 생기는 경우가 많습니다. 아래 실수들은 특히 반복해서 보이는 편입니다.
| 실수 | 겉으로 보이는 증상 | 실제로 문제인 부분 | 어떻게 보는 게 좋은가 |
|---|---|---|---|
| dragover마다 renderTodos를 호출한다 | 드래그가 전체적으로 끊기고 늦다 | 확정 전 상태에 전체 렌더를 붙였다 | drop 전까지는 가벼운 시각 상태만 다룬다 |
| dragover마다 save를 호출한다 | 불필요하게 무겁고 코드도 더 복잡해진다 | 중간 상태를 실제 데이터처럼 취급했다 | 저장은 실제 순서가 바뀐 drop 이후에만 한다 |
| 현재 drop 대상이 안 바뀌었는데도 표시를 매번 다시 준다 | 표시선이 흔들리거나 불필요하게 깜빡인다 | 같은 상태를 계속 다시 반영하고 있다 | id와 position이 진짜 바뀌었을 때만 업데이트한다 |
| 완료, 삭제, 수정 버튼을 항목마다 계속 다시 연결한다 | 구조가 반복되고 수정 포인트가 많아진다 | 클릭 이벤트 구조가 불필요하게 퍼져 있다 | 클릭 계열은 부모에서 묶는 구조를 고려한다 |
| 드래그 상태와 실제 데이터 상태를 구분하지 않는다 | 코드가 금방 엉키고 어디서 바뀌는지 감이 안 잡힌다 | 임시 상태와 저장 상태를 같은 무게로 다루고 있다 | draggedId, currentDropPosition 같은 값은 임시 상태로 분리한다 |
| 자동 정렬, 검색, 드래그 재배치를 동시에 다 허용한다 | 느릴 뿐 아니라 결과도 설명하기 어려워진다 | 여러 상태가 한 번에 겹쳐 있다 | 첫 버전은 custom 모드와 전체 보기부터 안정화한다 |
여기서 가장 먼저 손대기 좋은 건 늘 비슷합니다. dragover 안의 무거운 작업을 빼고, 표시선 업데이트를 최소화하고, drop 때만 실제 재배치와 저장을 하게 만드는 것. 이 세 가지만 정리해도 드래그 느낌이 꽤 달라지는 경우가 많습니다.
실전에서 가장 많이 흔들리는 부분
성능 문제는 대단한 알고리즘보다, “이 순간 정말 이 작업이 필요한가”를 안 따진 채 자주 일어나는 이벤트 안에 많은 일을 몰아넣을 때 더 쉽게 생긴다.
'바이브코딩 > 이론' 카테고리의 다른 글
| 17. 입력은 달라도 바뀌는 건 같다: 마우스와 터치를 하나의 재정렬 로직으로 묶는 법 (0) | 2026.04.07 |
|---|---|
| 16. 데스크톱에선 되는데 모바일에선 왜 안 될까: 터치 기반 재정렬 입문 (0) | 2026.04.02 |
| 14. 드롭은 됐는데 왜 앞뒤가 자꾸 틀릴까: 바이브코딩에서 마우스 위치 읽는 법 (0) | 2026.03.26 |
| 13. 드롭은 됐는데 왜 한 칸씩 밀릴까: 바이브코딩에서 끼워 넣기와 index 보정 (0) | 2026.03.24 |
| 12. 마우스로 옮겼는데 왜 순서가 안 남을까: 바이브코딩 드래그 앤 드롭 입문 (1) | 2026.03.21 |
