지난 글에서는 드래그 앤 드롭의 첫 버전으로, 잡은 항목과 놓은 항목의 자리를 서로 바꾸는 방식을 다뤘습니다. 이 방식은 이해하기 쉽고 구현도 비교적 단순합니다. 하지만 실제로 써보면 금방 아쉬운 점이 보입니다. 내가 원하는 건 “이 항목을 저 항목 자리와 교환”하는 것이 아니라, 목록 사이에 자연스럽게 끼워 넣는 것인 경우가 더 많기 때문입니다.
예를 들어 A, B, C, D, E 순서가 있을 때 B를 E 쪽으로 옮기고 싶다고 해보겠습니다. 자리 바꾸기 방식이라면 B와 E가 교환됩니다. 결과는 A, E, C, D, B 같은 모양이 되기 쉽습니다. 그런데 사용자가 실제로 기대하는 건 보통 이런 게 아닙니다. 대부분은 A, C, D, B, E처럼 B가 E 앞쪽으로 들어가고, 나머지가 한 칸씩 밀리는 결과를 더 자연스럽게 느낍니다.
문제는 여기서부터입니다. 자리 바꾸기는 두 항목만 보면 되지만, 끼워 넣기는 기존 위치에서 한 번 빼고, 새 위치에 다시 넣는 과정이 필요합니다. 그러다 보니 index, 즉 현재 배열에서 몇 번째 자리에 있는지를 나타내는 값이 갑자기 중요해집니다. 그리고 바로 여기서 초보자가 많이 헷갈리는 index 보정이 등장합니다.
이번 글에서는 왜 끼워 넣기가 자리 바꾸기보다 더 까다로운지, index 보정이 정확히 무슨 뜻인지, 그리고 첫 버전에서는 어떤 규칙으로 구현하는 편이 가장 덜 흔들리는지 차근차근 정리해보겠습니다.
| 겉으로 보이는 문제 | 실제로 일어나는 일 | 가장 먼저 봐야 할 방향 |
|---|---|---|
| 드롭은 되는데 결과가 예상보다 한 칸씩 어긋난다 | 원래 위치에서 항목을 빼낸 뒤 target 위치가 달라졌는데 그대로 계산했다 | source index와 target index를 removal 이후 기준으로 다시 보는지 확인한다 |
| 자연스럽게 끼워 넣고 싶은데 계속 자리만 바뀐다 | swap 방식으로만 처리하고 있다 | 교환과 삽입을 다른 문제로 구분해서 본다 |
| 순서는 바뀌는데 저장 후 다시 열면 이상하다 | 중간 배열만 바꿨고 order 값을 다시 정리하지 않았다 | 재배치 후 order를 다시 매기고 저장하는지 본다 |
| 바로 아래 항목에 드롭했는데 아무 변화가 없어 보인다 | 드롭 규칙이 “대상 앞에 넣기”인데, 바로 다음 항목 앞은 사실 현재 위치와 같을 수 있다 | 첫 버전에서 before 규칙을 쓰는지, after 규칙까지 동시에 하려는지 먼저 구분한다 |
이번 글의 핵심 한 줄
자리 바꾸기는 두 항목의 순서를 교환하는 문제이고, 끼워 넣기는 하나를 빼서 다른 위치에 다시 넣는 문제다. 그래서 index 보정이 필요해진다.
목차
- 자리 바꾸기만으로는 왜 부족할까
- 끼워 넣기는 무엇이 다른가
- index 보정은 왜 필요한가
- 가장 단순한 구현 흐름: 꺼내고, 넣고, order 다시 매기기
- 첫 버전에서 정해야 할 규칙
- 초보자가 자주 하는 실수 6가지
- 마무리
자리 바꾸기만으로는 왜 부족할까
자리 바꾸기 방식은 첫 단계로 아주 좋습니다. 드래그 이벤트가 무엇인지 이해하기 쉽고, dragged 항목과 target 항목의 관계도 명확하기 때문입니다. 두 항목의 order 값만 바꾸면 바로 결과가 눈에 보입니다. 디버깅도 비교적 쉽습니다.
그런데 실제 사용 흐름에서는 점점 이 방식이 거칠게 느껴집니다. 사용자는 보통 “저 항목 자리에 가라”보다 “저 항목 앞이나 뒤에 들어가라”를 더 자연스럽게 기대하기 때문입니다. 즉, 사람은 순서를 바꿀 때 교환보다 삽입에 더 가까운 동작을 머릿속에 그립니다.
| 상황 | 자리 바꾸기 결과 | 사용자가 흔히 기대하는 결과 |
|---|---|---|
| A, B, C, D, E에서 B를 E 쪽으로 옮김 | A, E, C, D, B | A, C, D, B, E |
| A, B, C, D에서 D를 B 위로 옮김 | A, D, C, B | A, D, B, C |
이 차이는 사소해 보여도 큽니다. 교환 방식은 드래그 대상과 목표 대상 둘만 신경 쓰면 되지만, 삽입 방식은 그 사이에 있던 항목들이 함께 밀리기 때문입니다. 그래서 더 자연스럽지만, 동시에 더 조심해서 계산해야 합니다.
핵심 포인트
자리 바꾸기는 “둘의 위치를 바꾼다”는 단순한 규칙이고, 끼워 넣기는 “하나를 빼고 나머지를 한 칸씩 밀면서 재배치한다”는 규칙이다.
끼워 넣기는 무엇이 다른가
끼워 넣기는 말 그대로 하나를 꺼내서 다른 자리 사이에 다시 넣는 방식입니다. 이 말만 보면 간단해 보이지만, 실제 배열 관점에서는 두 단계가 꼭 필요합니다.
- 먼저 원래 자리에서 항목을 제거한다.
- 그다음 새 위치에 다시 삽입한다.
이 두 단계를 분리해서 이해하는 순간부터 흐름이 조금 더 선명해집니다. 예를 들어 아래처럼 생각해볼 수 있습니다.
원래 순서
A, B, C, D, E
B를 E 앞에 넣고 싶다
B를 원래 자리에서 뺀다
A, C, D, E
E 앞에 다시 넣는다
A, C, D, B, E
즉, 끼워 넣기는 눈으로 볼 때는 한 번의 마우스 동작처럼 보이지만, 데이터에서는 remove + insert라는 두 단계로 설명됩니다. 그리고 հենց 이 사이에서 index 값이 달라지기 때문에 보정이 필요해집니다.
| 방식 | 데이터에서 실제로 하는 일 | 초보자에게 느껴지는 난도 |
|---|---|---|
| 자리 바꾸기 | 두 항목의 order 값만 교환한다 | 비교적 단순하다 |
| 끼워 넣기 | 하나를 빼고, 그 결과 배열에서 다시 삽입한다 | index 흐름을 함께 봐야 해서 더 까다롭다 |
여기서 초보자가 제일 먼저 챙기면 좋은 감각은 이것입니다. 끼워 넣기는 화면 효과가 아니라 배열 재배치다. 이 관점을 잡아두면 드롭 위치가 이상하게 한 칸 어긋날 때도, “이벤트가 이상한가?”보다 “빼낸 뒤의 배열 기준으로 다시 계산했나?”를 먼저 떠올릴 수 있습니다.
쉽게 말하면
교환은 두 사람의 자리를 맞바꾸는 것이고, 삽입은 한 사람을 줄에서 빼서 다른 위치에 다시 세우는 것이다.
index 보정은 왜 필요한가
이제 가장 많이 헷갈리는 부분으로 들어가겠습니다. index는 배열에서 현재 몇 번째 위치인지를 나타내는 값입니다. 보통 0부터 시작합니다. 예를 들어 A, B, C, D, E가 있으면 A는 0, B는 1, C는 2, D는 3, E는 4라고 볼 수 있습니다.
문제는 항목 하나를 먼저 빼는 순간, 그 뒤에 있던 항목들의 index가 전부 한 칸씩 당겨진다는 점입니다. 바로 이 변화 때문에 source index와 target index를 그대로 믿으면 한 칸 어긋난 결과가 나오기 쉽습니다.
예를 들어 B를 E 앞에 넣고 싶다고 해보겠습니다.
| 단계 | 배열 상태 | 설명 |
|---|---|---|
| 처음 | A(0), B(1), C(2), D(3), E(4) | source index는 1, target index는 4다 |
| B 제거 후 | A(0), C(1), D(2), E(3) | 원래 E의 index 4는 이제 3이 된다 |
| 삽입 시점 | A, C, D, B, E | source가 target보다 앞에 있었으므로 insert index를 한 칸 줄여야 한다 |
즉, source index가 target index보다 작다면, 먼저 source를 제거하는 순간 target 위치가 왼쪽으로 한 칸 당겨집니다. 그래서 첫 버전의 “target 앞에 넣기” 규칙에서는 아래처럼 보는 편이 가장 이해하기 쉽습니다.
sourceIndex < targetIndex 이면
insertIndex = targetIndex - 1
sourceIndex > targetIndex 이면
insertIndex = targetIndex
이게 바로 index 보정입니다. “원래 target 위치”를 그대로 믿지 말고, source를 먼저 제거한 뒤 달라진 배열 기준으로 한 번 더 생각하는 것입니다.
반대로 D를 B 앞에 넣는 경우는 조금 다릅니다.
| 단계 | 배열 상태 | 설명 |
|---|---|---|
| 처음 | A(0), B(1), C(2), D(3), E(4) | source index는 3, target index는 1이다 |
| D 제거 후 | A(0), B(1), C(2), E(3) | target B는 여전히 1 위치다 |
| 삽입 시점 | A, D, B, C, E | source가 target보다 뒤에 있었으므로 insert index는 그대로 targetIndex다 |
이 흐름이 잡히면 “왜 한 칸 어긋났지?”라는 문제가 훨씬 덜 막막해집니다. 드래그 자체가 어려운 게 아니라, 배열에서 하나를 뺐을 때 나머지 index가 변한다는 사실을 놓친 경우가 대부분이기 때문입니다.
처음엔 이렇게 외워도 충분하다
위에서 아래로 내릴 때는 target이 한 칸 당겨질 수 있고, 아래에서 위로 올릴 때는 보통 target 위치를 그대로 써도 된다.
가장 단순한 구현 흐름: 꺼내고, 넣고, order 다시 매기기
이제 첫 버전에서 가장 무난한 구현 흐름을 정리해보겠습니다. 핵심은 복잡한 수학이 아니라, 정렬된 복사본 하나를 만들고, source를 꺼낸 뒤, 보정된 위치에 다시 넣고, order를 1부터 다시 매기는 것입니다.
여기서 중요한 이유가 하나 있습니다. 자리 바꾸기에서는 두 항목의 order만 바꿔도 됐지만, 끼워 넣기에서는 중간 항목들이 함께 밀립니다. 그래서 order를 부분적으로만 바꾸려 하기보다, 새 순서대로 다시 1, 2, 3, 4처럼 재정렬하는 편이 훨씬 이해하기 쉽고 버그도 줄어듭니다.
function moveTodoBefore(draggedId, targetId) {
const ordered = [...todos].sort((a, b) => a.order - b.order);
const sourceIndex = ordered.findIndex(todo => todo.id === draggedId);
const targetIndex = ordered.findIndex(todo => todo.id === targetId);
if (sourceIndex === -1 || targetIndex === -1) return;
if (sourceIndex === targetIndex) return;
const next = [...ordered];
const removed = next.splice(sourceIndex, 1)[0];
const insertIndex =
sourceIndex < targetIndex
? targetIndex - 1
: targetIndex;
next.splice(insertIndex, 0, removed);
todos = next.map((todo, index) => {
return {
...todo,
order: index + 1
};
});
saveTodos();
renderTodos();
}
이 코드를 초보자 관점에서 다시 풀면 아래 순서입니다.
- 현재 custom 순서대로 정렬된 목록을 하나 만든다.
- 잡은 항목과 대상 항목이 각각 몇 번째인지 찾는다.
- 잡은 항목을 먼저 빼낸다.
- 위에서 아래로 내리는 경우에는 target index를 한 칸 줄인다.
- 그 위치에 다시 넣는다.
- 새 배열 순서대로 order를 1부터 다시 매긴다.
- 저장하고 다시 그린다.
| 구성 | 왜 도움이 되나 |
|---|---|
| 복사본에서 작업 | 원본 흐름을 바로 망가뜨리지 않고 계산을 분리해서 볼 수 있다 |
| remove 후 insert | 끼워 넣기의 본질을 가장 정직하게 보여준다 |
| order 다시 매기기 | 부분 수정보다 읽기 쉽고 저장도 안정적이다 |
이 방식은 겉보기에 조금 투박할 수 있습니다. 하지만 초보자에게는 오히려 장점이 큽니다. “왜 이 항목이 여기로 갔는지”를 설명하기가 훨씬 쉽기 때문입니다. 처음부터 너무 영리한 계산을 쓰기보다, 배열을 꺼내고 다시 넣는 흐름을 눈으로 따라갈 수 있는 편이 낫습니다.
첫 구현에서 가장 안정적인 원칙
삽입 재정렬은 부분적으로 몇 개 값만 만지기보다, 최종 배열을 만든 뒤 order를 처음부터 다시 붙이는 편이 훨씬 덜 흔들린다.
첫 버전에서 정해야 할 규칙
끼워 넣기 방식은 생각보다 규칙이 중요합니다. 코드가 헷갈리는 이유 중 하나는, 개발자가 아직 “드롭이 정확히 무슨 뜻인지”를 정하지 않았는데 구현부터 시작하는 경우가 많기 때문입니다. 첫 버전에서는 일부러 규칙을 좁게 잡는 편이 낫습니다.
입문자 기준으로는 아래 정도가 가장 무난합니다.
- 드롭한 대상의 앞에 넣는다.
처음에는 before 규칙 하나만 정하는 편이 단순합니다. - custom 모드에서만 허용한다.
최신순, 오래된순 같은 자동 정렬 모드에서는 끼워 넣기 결과가 바로 보이지 않을 수 있습니다. - 전체 보기에서만 허용한다.
검색이나 필터가 걸린 상태에서는 숨겨진 항목 때문에 결과 해석이 더 어려워집니다. - 같은 항목 위에 드롭하면 아무 일도 하지 않는다.
불필요한 저장을 막을 수 있습니다.
여기서 한 가지는 미리 알고 가는 편이 좋습니다. “대상 앞에 넣기” 규칙을 쓰면, 어떤 경우에는 사용자가 움직였다고 느끼는데 결과가 눈에 거의 안 보일 수 있습니다. 예를 들어 B를 바로 다음 항목 C 앞에 놓는다면, 실제 결과는 그대로 B, C가 될 수 있습니다. 이건 버그라기보다 규칙이 단순해서 생기는 자연스러운 결과입니다.
| 규칙 | 왜 이렇게 두는가 |
|---|---|
| target 앞에 넣기만 지원 | before와 after를 동시에 처리하려 들면 조건이 갑자기 늘어난다 |
| custom 모드 한정 | 자동 정렬이 order 결과를 덮어쓰는 혼란을 줄일 수 있다 |
| 전체 보기 한정 | 숨겨진 항목까지 고려한 재배치는 한 단계 더 어렵다 |
| 같은 항목 드롭은 무시 | 의미 없는 변경과 저장을 줄일 수 있다 |
처음 버전이 조금 단순해 보여도 괜찮습니다. 드래그 앤 드롭에서는 “자연스러운 느낌”보다 먼저, 규칙이 분명한가가 더 중요합니다. 그래야 나중에 위쪽 절반은 앞에 넣고 아래쪽 절반은 뒤에 넣는 식으로 확장할 때도 어디서부터 바꿔야 할지 보입니다.
실전 팁
첫 버전에서는 규칙이 다소 투박해도 괜찮다. “무슨 뜻으로 드롭을 해석할 것인가”가 먼저 분명해야 다음 단계도 덜 흔들린다.
초보자가 자주 하는 실수 6가지
끼워 넣기 방식으로 넘어오면 자주 나오는 실수도 조금 달라집니다. 드래그 이벤트 자체보다, 제거와 삽입 사이의 배열 변화에서 흔들리는 경우가 많습니다.
| 실수 | 겉으로 보이는 증상 | 실제로 문제인 부분 | 어떻게 보는 게 좋은가 |
|---|---|---|---|
| swap 함수 그대로 재사용한다 | 기대한 것과 달리 항목이 서로 교환만 된다 | 삽입 문제를 교환 문제처럼 다뤘다 | remove 후 insert 흐름으로 바꿔서 생각한다 |
| source를 뺀 뒤에도 target index를 그대로 쓴다 | 결과가 한 칸 어긋난다 | index 보정이 빠졌다 | source가 앞에 있었는지 뒤에 있었는지 먼저 본다 |
| DOM 순서만 바꾸고 order는 안 바꾼다 | 그 순간엔 맞지만 다시 열면 원래대로다 | 데이터 순서가 저장되지 않았다 | 재배치 후 order를 다시 매기고 저장한다 |
| 부분적으로 몇 개 order만 수정한다 | 몇 번 옮기다 보면 order 값이 이해하기 어려워진다 | 중간 항목 이동을 충분히 반영하지 못했다 | 첫 버전에서는 전체 배열 기준으로 order를 다시 매기는 편이 낫다 |
| 자동 정렬 모드나 검색 상태에서도 그대로 허용한다 | 결과가 예상과 다르게 보이거나 일부 목록만 움직인 것처럼 느껴진다 | 화면 계산 규칙과 실제 순서 변경이 섞였다 | custom 모드와 전체 보기에서 먼저 안정화한다 |
| 바로 다음 항목 위에 드롭했을 때 변화가 없으면 버그라고 생각한다 | 동작이 실패한 것처럼 느껴진다 | before 규칙에서는 같은 결과가 나올 수 있다 | 드롭 규칙을 먼저 분명히 정하고 본다 |
특히 마지막 실수는 실제로 꽤 자주 나옵니다. 드래그 앤 드롭은 눈에 보이는 행동이라서, 사용자는 항상 뭔가 극적인 변화가 있어야 한다고 기대하기 쉽습니다. 하지만 첫 버전의 “대상 앞에 넣기” 규칙에서는 어떤 드롭은 사실상 같은 결과가 될 수 있습니다. 이건 구현 실패라기보다 규칙이 단순해서 생기는 자연스러운 제한입니다.
실전에서 가장 많이 흔들리는 부분
삽입 재정렬은 이벤트보다 배열 계산이 핵심이다. 드롭이 됐는데 결과가 이상하면, 대부분 drag 이벤트보다 index 보정과 order 재계산 쪽을 먼저 보는 편이 빠르다.
'바이브코딩 > 이론' 카테고리의 다른 글
| 15. 리스트가 길어지면 왜 드래그가 버벅일까: 바이브코딩에서 렌더링과 이벤트 정리 (0) | 2026.03.31 |
|---|---|
| 14. 드롭은 됐는데 왜 앞뒤가 자꾸 틀릴까: 바이브코딩에서 마우스 위치 읽는 법 (0) | 2026.03.26 |
| 12. 마우스로 옮겼는데 왜 순서가 안 남을까: 바이브코딩 드래그 앤 드롭 입문 (1) | 2026.03.21 |
| 11. 내가 정한 순서가 왜 안 남을까: 바이브코딩에서 order 값이 필요한 이유 (0) | 2026.03.19 |
| 10. 정렬을 붙였더니 왜 원래 순서가 사라질까: 바이브코딩에서 sort가 헷갈리는 이유 (0) | 2026.03.17 |
