정렬까지 붙인 체크리스트 앱은 이제 꽤 그럴듯합니다. 최신순, 오래된순, 완료 항목 아래로 보내기 같은 기능도 돌아갑니다. 그런데 어느 시점이 되면 정렬만으로는 부족하다는 걸 금방 느끼게 됩니다. “이 할 일은 내가 직접 맨 위에 두고 싶은데”, “중요한 항목을 손으로 순서 바꿔가며 정리하고 싶은데” 같은 요구가 생기기 때문입니다.
문제는 여기서 많은 초보자가 정렬 버튼 하나 더 만들면 될 거라고 생각한다는 점입니다. 하지만 자동 정렬과 사용자 지정 순서는 비슷해 보여도 전혀 다른 문제입니다. 최신순은 생성 시각을 기준으로 매번 계산하면 되지만, 사용자가 직접 정한 순서는 그 자체를 데이터로 기억해야 합니다. 이 차이를 놓치면 순서를 바꿔도 새로고침하면 원래대로 돌아가거나, 다른 정렬을 한 번 눌렀다가 다시 돌아오면 내가 정한 순서가 사라진 것처럼 보이게 됩니다.
이번 글에서는 왜 정렬만으로는 사용자 순서를 대신할 수 없는지, 왜 order 값 같은 별도 순서 정보가 필요한지, 그리고 초보자라면 드래그 앤 드롭보다 먼저 어떤 방식으로 순서 변경을 붙이는 편이 좋은지 차근차근 정리해보겠습니다.
| 겉으로 보이는 문제 | 실제로 일어나는 일 | 가장 먼저 봐야 할 방향 |
|---|---|---|
| 내가 위로 올린 항목이 새로고침하면 다시 내려간다 | 화면 순서만 잠깐 바꿨고, 데이터 안에는 순서 정보가 없다 | 사용자 순서를 별도 값으로 저장하는지 확인한다 |
| 최신순에서 위로 이동을 눌렀는데 아무 의미가 없는 것 같다 | 자동 정렬과 사용자 지정 순서를 같은 개념으로 다루고 있다 | 정렬 모드와 사용자 순서 모드를 구분한다 |
| 보이는 순서를 바꿨는데 삭제하면 엉뚱한 항목이 지워진다 | 화면 위치를 항목 정체성으로 착각하고 있다 | 수정과 삭제는 계속 id 기준인지 본다 |
| 순서 바꾸기 기능을 붙였더니 코드가 더 헷갈린다 | 자동 정렬, 검색, 필터, 사용자 순서가 한데 섞였다 | 지금 바꾸는 것이 화면용 순서인지 실제 저장 순서인지 먼저 정한다 |
이번 글의 핵심 한 줄
자동 정렬은 규칙으로 순서를 계산하는 기능이고, 사용자 지정 순서는 그 순서 자체를 데이터로 기억하는 기능이다.
정렬과 사용자 지정 순서는 왜 다른가
이전 글에서 다룬 최신순, 오래된순, 완료 항목 아래로 보내기 같은 기능은 기본적으로 규칙 기반 정렬입니다. 어떤 기준이 있으면 그 기준에 따라 매번 화면 순서를 다시 계산하면 됩니다. 예를 들어 최신순이라면 생성 시각이 큰 항목이 위로 오면 되고, 완료 항목 아래로 보내기라면 done 값이 false인 항목이 먼저 오면 됩니다.
반면 사용자가 직접 순서를 바꾸는 기능은 다릅니다. 이 경우에는 “가장 최근에 만든 항목”이 중요한 것이 아니라, 내가 직접 어느 항목을 몇 번째 자리에 두었는가가 중요합니다. 이건 자동으로 계산되는 값이 아니라, 저장해둬야 하는 값입니다.
| 구분 | 무엇을 기준으로 움직이나 | 새로고침 후에도 기억해야 하나 | 대표 예시 |
|---|---|---|---|
| 자동 정렬 | 생성 시각, 완료 여부, 글자순 같은 규칙 | 기준만 기억하면 된다 | 최신순, 오래된순, 완료 항목 아래로 보내기 |
| 사용자 지정 순서 | 사용자가 직접 정한 위치 | 순서 자체를 저장해야 한다 | 중요한 할 일 맨 위로 올리기, 손으로 순서 재배치하기 |
이 차이를 모르면 아래 같은 상황이 자주 생깁니다. 사용자가 항목을 위로 올렸는데 화면은 잠깐 바뀝니다. 그런데 다음에 최신순 버튼을 눌렀다가 다시 돌아오면 그 순서가 사라진 것처럼 보입니다. 사실은 사라진 게 아니라, 애초에 사용자 순서를 저장하지 않았기 때문에 자동 정렬 규칙이 다시 덮어쓴 셈입니다.
중요한 포인트
최신순은 createdAt 같은 규칙으로 매번 계산할 수 있지만, 내가 직접 정한 순서는 규칙이 아니라 기록이다.
id만으로는 순서를 기억할 수 없는 이유
앞선 글에서 id는 아주 중요하다고 설명했습니다. 맞습니다. id는 삭제, 수정, 완료 체크처럼 “누구를 바꿀 것인가”를 정확히 찾기 위해 필요합니다. 그런데 id만 있다고 해서 “어디에 둘 것인가”까지 해결되지는 않습니다. 바로 이 지점에서 order 값이 따로 필요해집니다.
예를 들어 아래처럼 항목이 있다고 해보겠습니다.
{
id: 1710000000001,
text: '장보기',
done: false,
createdAt: 1710000000001
}
이 객체는 이 항목이 누구인지, 언제 만들어졌는지, 완료됐는지까지는 알 수 있습니다. 하지만 사용자가 이 항목을 지금 목록에서 몇 번째에 두고 싶은지는 알 수 없습니다. 즉, id는 항목의 정체성이고, 순서는 또 다른 정보입니다.
| 값 | 무엇을 뜻하나 | 왜 필요한가 |
|---|---|---|
| id | 이 항목이 누구인지 구분하는 번호표 | 삭제, 수정, 완료 체크 같은 작업 대상을 정확히 찾기 위해 |
| createdAt | 언제 만들어졌는지 나타내는 값 | 최신순, 오래된순 같은 자동 정렬 기준에 쓰기 위해 |
| order | 사용자가 정한 화면상 순서 | 맨 위로 올리기, 직접 순서 바꾸기 같은 사용자 지정 정렬을 기억하기 위해 |
입문자 입장에서는 이 셋이 처음엔 비슷해 보일 수 있습니다. 특히 id가 시간 기반 숫자라면 더 그렇습니다. 하지만 역할은 분명히 다릅니다. id는 “누구인가”, createdAt은 “언제 생겼는가”, order는 “어디에 보여야 하는가”입니다.
쉽게 말하면
id는 주민등록번호에 가깝고, createdAt은 출생 시각에 가깝고, order는 줄을 설 때 몇 번째에 세울지에 가깝다.
order 값은 정확히 무엇이고 어디에 붙는가
order 값은 말 그대로 사용자 지정 순서를 나타내는 숫자입니다. 가장 단순하게는 1, 2, 3처럼 시작해도 충분합니다. 중요한 건 숫자를 예쁘게 만드는 것이 아니라, 항목마다 현재 위치를 설명할 수 있어야 한다는 점입니다.
예를 들어 체크리스트 데이터를 아래처럼 가질 수 있습니다.
[
{
id: 1710000000001,
text: '장보기',
done: false,
createdAt: 1710000000001,
order: 1
},
{
id: 1710000000002,
text: '운동하기',
done: false,
createdAt: 1710000000002,
order: 2
},
{
id: 1710000000003,
text: '메일 보내기',
done: true,
createdAt: 1710000000003,
order: 3
}
]
이 구조가 있으면 최신순 정렬이 필요할 때는 createdAt을 기준으로 볼 수 있고, 사용자가 직접 정한 순서대로 보여주고 싶을 때는 order를 기준으로 볼 수 있습니다. 즉, 정렬 기준이 하나만 있는 게 아니라 상황에 따라 여러 기준을 가질 수 있게 됩니다.
처음에는 order 값을 아주 단순하게 다루는 편이 좋습니다.
- 새 항목을 맨 아래에 추가한다면 가장 큰 order 값보다 1 큰 값을 붙인다.
- 위로 이동하면 바로 앞 항목과 order 값을 바꾼다.
- 아래로 이동하면 바로 뒤 항목과 order 값을 바꾼다.
여기서 중요한 건 createdAt과 order를 섞지 않는 것입니다. 생성 시각은 과거를 설명하고, order는 현재 화면 위치를 설명합니다. 둘은 우연히 비슷한 숫자처럼 보여도 역할이 다릅니다.
| 상황 | order 값 처리 | 왜 이렇게 하는가 |
|---|---|---|
| 새 항목 추가 | 현재 최대값 + 1 | 맨 아래에 자연스럽게 붙기 쉽다 |
| 위로 이동 | 바로 앞 항목과 값 교환 | 전체를 다시 계산하지 않아도 된다 |
| 아래로 이동 | 바로 뒤 항목과 값 교환 | 직관적이고 구현이 단순하다 |
입문자에게 가장 무난한 방식
처음 버전에서는 order를 1, 2, 3처럼 단순한 정수로 두고, 위로 이동과 아래로 이동에서 서로 값을 바꾸는 방식으로 시작하는 편이 이해하기 쉽다.
가장 단순한 방법: 위로/아래로 이동부터 만들기
사용자 지정 순서를 붙일 때 많은 사람이 곧바로 드래그 앤 드롭부터 떠올립니다. 하지만 초보자라면 처음부터 마우스로 끌어 놓는 동작까지 한 번에 붙이기보다, 위로 이동과 아래로 이동 버튼부터 만드는 편이 훨씬 낫습니다. 이유는 단순합니다. 순서를 바꾸는 핵심은 마우스 동작이 아니라 데이터 안의 order 값이 어떻게 바뀌는지 이해하는 것이기 때문입니다.
가장 기본적인 흐름은 이렇습니다.
- 현재 목록을 order 기준으로 정렬해서 본다.
- 움직이려는 항목이 몇 번째인지 찾는다.
- 바로 앞이나 뒤 항목과 order 값을 바꾼다.
- 저장하고 다시 그린다.
아래는 이 흐름을 보여주는 가장 단순한 예시입니다.
function getOrderedTodos() {
return [...todos].sort((a, b) => a.order - b.order);
}
function swapOrder(firstId, secondId) {
const first = todos.find(todo => todo.id === firstId);
const second = todos.find(todo => todo.id === secondId);
if (!first || !second) return;
const temp = first.order;
first.order = second.order;
second.order = temp;
saveTodos();
renderTodos();
}
function moveTodoUp(id) {
const ordered = getOrderedTodos();
const index = ordered.findIndex(todo => todo.id === id);
if (index <= 0) return;
const current = ordered[index];
const prev = ordered[index - 1];
swapOrder(current.id, prev.id);
}
function moveTodoDown(id) {
const ordered = getOrderedTodos();
const index = ordered.findIndex(todo => todo.id === id);
if (index === -1 || index >= ordered.length - 1) return;
const current = ordered[index];
const next = ordered[index + 1];
swapOrder(current.id, next.id);
}
이 코드에서 핵심은 두 가지입니다. 첫째, 화면에 보이는 순서를 기준으로 움직이더라도 실제 대상은 항상 id로 찾는다는 점입니다. 둘째, 배열 위치를 직접 믿기보다 order 값을 서로 바꿔서 순서를 설명하게 만든다는 점입니다.
렌더링 쪽에서는 현재 정렬 모드가 사용자 순서일 때 order를 기준으로 보여주면 됩니다. 예를 들어 정렬 모드를 이렇게 둘 수 있습니다.
let currentSort = 'custom';
그리고 화면 계산 함수에서는 이렇게 나눌 수 있습니다.
function getVisibleTodos() {
if (currentSort === 'custom') {
return [...todos].sort((a, b) => a.order - b.order);
}
if (currentSort === 'newest') {
return [...todos].sort((a, b) => b.createdAt - a.createdAt);
}
if (currentSort === 'oldest') {
return [...todos].sort((a, b) => a.createdAt - b.createdAt);
}
return todos;
}
| 모드 | 화면 기준 | 위로/아래로 이동 버튼이 의미 있나 |
|---|---|---|
| custom | order 값 | 예 |
| newest | createdAt 내림차순 | 보통 아니오 |
| oldest | createdAt 오름차순 | 보통 아니오 |
이 표에서 중요한 점이 하나 있습니다. 사용자 순서 모드일 때만 순서 이동이 자연스럽다는 점입니다. 최신순이나 오래된순 모드에서는 화면이 createdAt 기준으로 다시 정렬되기 때문에, 위로 이동 버튼을 눌러도 사용자가 기대한 변화가 잘 안 보일 수 있습니다. 그래서 처음 버전에서는 사용자 순서 모드에서만 이동 버튼을 보여주는 편이 훨씬 덜 헷갈립니다.
초보자에게 특히 좋은 접근
드래그 앤 드롭보다 먼저 위로 이동, 아래로 이동 버튼을 붙여보면 순서 변경의 핵심이 이벤트가 아니라 order 데이터라는 점이 훨씬 분명해진다.
order 값을 저장하고 다시 불러오는 흐름
사용자 지정 순서는 결국 저장돼야 합니다. 화면에서 잠깐만 올라갔다 내려갔다 하는 것이 아니라, 다음에 다시 열었을 때도 그 순서가 남아 있어야 진짜 기능이 됩니다. 그래서 order 값은 다른 데이터와 마찬가지로 저장 대상에 포함돼야 합니다.
새 항목을 추가할 때는 보통 맨 아래에 붙이는 방식이 가장 단순합니다. 이때는 현재 가장 큰 order 값을 찾아서 1을 더해주면 됩니다.
function getNextOrder() {
if (todos.length === 0) return 1;
return Math.max(...todos.map(todo => todo.order)) + 1;
}
function addTodo(text) {
const trimmed = text.trim();
if (!trimmed) return;
todos.push({
id: Date.now(),
text: trimmed,
done: false,
createdAt: Date.now(),
order: getNextOrder()
});
saveTodos();
renderTodos();
}
그리고 페이지를 다시 열었을 때는 저장된 todos 안에 order 값이 그대로 들어 있으므로, 사용자 순서 모드에서는 다시 order 기준으로 정렬해서 보여주면 됩니다. 이 흐름이 있으면 순서를 바꾼 뒤 새로고침해도 같은 배치가 유지됩니다.
| 시점 | 무슨 일이 일어나야 하나 | 왜 필요한가 |
|---|---|---|
| 항목 추가 | 새 order 값을 넣는다 | 처음 들어온 항목도 사용자 순서 체계 안에 들어가야 한다 |
| 위로/아래로 이동 | 서로의 order 값을 바꾸고 저장한다 | 화면 변화가 다음 실행에도 유지되게 한다 |
| 페이지 재실행 | 저장된 order를 읽고 그 기준으로 다시 정렬한다 | 사용자 지정 순서가 복원된다 |
| 정렬 모드 변경 | 화면 기준만 바꾼다 | 최신순과 사용자 순서를 서로 구분할 수 있다 |
여기서 하나 더 현실적인 조언을 덧붙이면, 첫 버전에서는 검색이나 필터가 걸린 상태에서 순서 변경을 막는 편이 낫습니다. 가능은 하지만, 입문자 기준에서는 설명이 갑자기 어려워집니다. 왜냐하면 화면에 안 보이는 항목이 중간에 끼어 있을 수 있어서 “위로 이동”이 전체 순서 안에서는 어디를 뜻하는지 다시 생각해야 하기 때문입니다. 처음에는 전체 보기 + 사용자 순서 모드에서만 이동 버튼을 허용하는 쪽이 구현도 단순하고, 동작도 더 예측 가능해집니다.
실전 팁
처음 버전에서는 custom 모드이면서 전체 보기일 때만 순서 변경을 허용해도 충분하다. 검색과 필터가 섞인 상태에서의 재배치는 한 단계 뒤에 다뤄도 늦지 않다.
초보자가 자주 하는 실수 6가지
사용자 지정 순서를 붙일 때도 비슷한 실수가 반복됩니다. 특히 정렬 글까지 따라왔던 사람이라면 자동 정렬과 order 개념을 섞기 쉬운데, 여기서 정확히 선을 그어두면 훨씬 덜 흔들립니다.
| 실수 | 겉으로 보이는 증상 | 실제로 문제인 부분 | 어떻게 보는 게 좋은가 |
|---|---|---|---|
| 배열 index를 순서 정보처럼 쓴다 | 필터나 정렬 후 순서 변경이 꼬인다 | 화면 위치와 저장 순서를 같은 것으로 봤다 | index 대신 order 값을 별도로 두는 편이 안전하다 |
| DOM 순서만 바꾼다 | 그 순간엔 움직인 것처럼 보이지만 새로고침하면 원래대로다 | 데이터 안의 order 값은 그대로다 | 순서 변경은 화면이 아니라 데이터에서 먼저 일어나야 한다 |
| createdAt과 order를 같은 값처럼 다룬다 | 최신순과 사용자 순서가 자꾸 섞인다 | 생성 시각과 현재 위치는 역할이 다르다 | createdAt은 자동 정렬, order는 사용자 순서로 분리한다 |
| 순서 바꾼 뒤 저장을 빼먹는다 | 새로고침하면 다시 예전 순서다 | 화면 변화가 기록으로 이어지지 않았다 | 교환 후 저장, 그리고 다시 렌더링까지 한 묶음으로 본다 |
| 사용자 순서 모드가 아닌데 이동 버튼을 그대로 둔다 | 위로 이동을 눌러도 화면상 의미가 안 보인다 | 자동 정렬이 order 결과를 다시 덮어쓴다 | custom 모드에서만 이동 버튼을 보여주는 편이 낫다 |
| 검색이나 필터 중에도 그대로 순서 변경을 허용한다 | 숨겨진 항목 때문에 이동 결과가 예상과 다르다 | 보이는 목록과 전체 순서를 동시에 다루게 됐다 | 첫 버전에서는 전체 보기에서만 재배치하는 편이 훨씬 단순하다 |
여기서 특히 많이 헷갈리는 것은 다섯 번째 실수입니다. 최신순 모드에서 위로 이동 버튼을 눌러도, 화면은 여전히 createdAt 기준으로 정렬되어 있기 때문에 사용자는 “왜 안 움직이지?”라고 느낄 수 있습니다. 실제로는 order 값이 바뀌었더라도 최신순 규칙이 더 강하게 적용돼서 화면에서 안 보이는 것뿐입니다. 그래서 사용자 순서를 바꾸는 기능은 사용자 순서 모드와 함께 있어야 의미가 분명합니다.
실전에서 가장 많이 흔들리는 부분
순서를 바꾼다는 말이 “지금 화면을 바꾸는 것”인지 “다음에도 유지될 실제 배치를 바꾸는 것”인지 섞이기 시작하면 코드도 같이 헷갈려진다.
'바이브코딩 > 이론' 카테고리의 다른 글
| 13. 드롭은 됐는데 왜 한 칸씩 밀릴까: 바이브코딩에서 끼워 넣기와 index 보정 (0) | 2026.03.24 |
|---|---|
| 12. 마우스로 옮겼는데 왜 순서가 안 남을까: 바이브코딩 드래그 앤 드롭 입문 (1) | 2026.03.21 |
| 10. 정렬을 붙였더니 왜 원래 순서가 사라질까: 바이브코딩에서 sort가 헷갈리는 이유 (0) | 2026.03.17 |
| 9. 검색을 붙였더니 왜 목록이 꼬일까: 바이브코딩에서 원본 데이터 지키는 법 (0) | 2026.03.15 |
| 8. 기능이 늘수록 코드가 왜 금방 엉킬까: 바이브코딩에서 함수 나누는 기준 (0) | 2026.03.14 |
