검색과 필터까지 붙인 체크리스트 앱은 이제 제법 그럴듯해 보입니다. 여기까지 오면 거의 자연스럽게 다음 욕심이 생깁니다. 최신순으로 보고 싶다, 완료된 항목은 아래로 보내고 싶다, 가나다 순으로 보고 싶다 같은 정렬 기능입니다. 문제는 이 지점부터 또 다른 종류의 혼란이 시작된다는 데 있습니다.
처음에는 정렬이 단순해 보입니다. 어차피 목록 순서만 바꾸면 되는 것 아닌가 싶기 때문입니다. 그런데 막상 붙이고 나면 검색을 지웠는데도 원래 순서가 안 돌아오거나, 완료 항목만 아래로 보냈더니 다음에는 전체 순서가 묘하게 바뀌어 있거나, 어떤 경우에는 정렬 결과를 저장해버려서 원래 순서를 잃어버리는 일이 생깁니다.
이 문제는 대개 정렬 코드가 어려워서 생기는 것이 아닙니다. 오히려 정렬이 화면을 위한 것인지, 실제 데이터 순서를 바꾸는 것인지를 구분하지 못해서 생기는 경우가 더 많습니다. 그리고 JavaScript에서 sort는 초보자가 생각하는 것보다 한 걸음 더 조심해서 다뤄야 하는 메서드이기도 합니다.
이번 글에서는 왜 정렬이 검색이나 필터보다 더 헷갈리기 쉬운지, 화면 순서와 실제 데이터 순서는 어떻게 다르게 봐야 하는지, 그리고 sort를 붙일 때 어떤 흐름으로 가야 덜 꼬이는지를 차근차근 정리해보겠습니다.
| 겉으로 보이는 문제 | 실제로 일어나는 일 | 가장 먼저 봐야 할 방향 |
|---|---|---|
| 최신순 정렬을 눌렀더니 원래 순서를 잃어버린 것 같다 | 화면용 정렬이 아니라 원본 배열 자체 순서를 바꿨을 가능성이 크다 | 정렬 대상이 원본인지 복사본인지 먼저 본다 |
| 검색 중 정렬했더니 목록이 더 헷갈린다 | 검색 결과와 원본 데이터, 정렬 결과가 한 배열 안에서 섞여 있다 | 원본 데이터와 화면용 데이터를 분리해서 본다 |
| 정렬 후 삭제했더니 엉뚱한 항목이 지워진다 | 보이는 순서나 index를 기준으로 처리하고 있다 | 수정과 삭제는 여전히 id 기준인지 확인한다 |
| 정렬 결과를 저장했더니 다음에도 그 순서만 남는다 | 보여주는 순서와 저장해야 할 실제 순서를 구분하지 못했다 | 이 정렬이 화면용인지, 진짜 데이터 순서인지 먼저 정한다 |
이번 글의 핵심 한 줄
정렬은 목록을 예쁘게 섞는 기능처럼 보이지만, 실제로는 원본 순서를 건드릴지 말지를 먼저 결정해야 하는 기능이다.
왜 정렬은 생각보다 까다로울까
검색과 필터는 기본적으로 무엇을 보여줄지 고르는 기능에 가깝습니다. 완료된 항목만 보기, 특정 단어가 들어간 항목만 보기 같은 식입니다. 반면 정렬은 보여줄 항목이 정해진 뒤, 그 순서를 어떻게 놓을지 결정하는 기능입니다. 겉보기에는 사소한 차이 같지만, 실제 코드에서는 꽤 다른 고민이 붙습니다.
예를 들어 전체 목록 10개 중 검색 결과로 3개가 남았다고 해보겠습니다. 여기서 최신순 정렬을 누르면 사용자는 “지금 보이는 3개가 최신순으로 보이길” 기대합니다. 그런데 코드를 잘못 짜면 전체 10개 목록 자체의 순서가 바뀌고, 그 바뀐 순서가 나중에도 계속 남습니다. 이때부터는 검색을 지웠을 때도 원래 익숙했던 순서로 돌아오지 않게 됩니다.
즉, 정렬은 단순히 보기 좋은 순서를 만드는 기능이 아니라, 어떤 수준의 순서를 바꿀 것인지를 먼저 정해야 하는 기능입니다. 지금 화면만 바꿀 것인지, 저장될 실제 목록 순서까지 바꿀 것인지가 섞이면 생각보다 빨리 꼬입니다.
| 기능 | 주된 역할 | 초보자가 흔히 헷갈리는 지점 |
|---|---|---|
| 검색 | 조건에 맞는 항목만 보이게 한다 | 검색 결과를 원본 목록처럼 다루기 쉽다 |
| 필터 | 전체 중 일부 상태만 골라서 보여준다 | 숨기기와 삭제를 헷갈리기 쉽다 |
| 정렬 | 보여줄 항목의 순서를 정한다 | 화면 순서를 바꾸는 것과 원본 순서를 바꾸는 것을 섞기 쉽다 |
핵심 포인트
검색과 필터는 “무엇을 보여줄까”에 가깝고, 정렬은 “어떤 순서로 보여줄까”에 가깝다. 둘은 연결돼 있지만 같은 기능은 아니다.
화면 순서와 실제 데이터 순서는 같은가
여기서 꼭 구분해야 하는 것이 있습니다. 지금 화면에 보이는 순서와 앱이 실제로 가진 데이터 순서는 같은 경우도 있지만, 다를 수도 있습니다. 그리고 정렬 기능이 붙기 시작하면 오히려 다르게 두는 편이 더 안정적인 경우가 많습니다.
예를 들어 사용자가 정렬 메뉴에서 최신순을 눌렀다고 해보겠습니다. 이때 대부분의 경우 사용자가 원하는 것은 “현재 화면에서 최신순으로 보이게 해달라”는 것입니다. 반면 사용자가 직접 항목을 끌어다가 순서를 바꿨다면 이야기가 달라집니다. 이때는 그 순서 자체가 기능이 됩니다. 다시 열어도 그 순서를 기억해주길 기대하게 되기 때문입니다.
즉, 모든 정렬이 다 같은 종류는 아닙니다. 어떤 정렬은 단순한 화면용 정렬이고, 어떤 정렬은 실제 데이터 순서 자체가 되는 기능입니다. 이 차이를 모르고 시작하면 저장 시점부터 혼란이 생깁니다.
| 상황 | 사용자 기대 | 원본 데이터를 바꿔야 하나 |
|---|---|---|
| 최신순 보기 | 지금 화면에서 최근 항목이 위에 오면 된다 | 대개 아니오 |
| 오래된순 보기 | 보기 방식만 바뀌면 된다 | 대개 아니오 |
| 완료 항목 아래로 보내기 | 보기 편하도록 화면 순서만 바뀌면 된다 | 대개 아니오 |
| 사용자가 직접 드래그해서 순서 바꾸기 | 그 순서를 다시 열어도 기억해주길 바란다 | 예 |
입문 단계의 체크리스트 앱이라면 대부분 화면용 정렬부터 붙입니다. 최신순, 오래된순, 완료된 항목 아래로 보내기 정도가 여기에 들어갑니다. 이 경우에는 원본 배열을 직접 뒤섞기보다, 보여줄 때만 순서를 바꿔서 계산하는 방식이 훨씬 덜 꼬입니다.
쉽게 말하면
정렬 버튼은 대개 안경을 바꿔 쓰는 일에 가깝고, 드래그로 순서를 바꾸는 기능은 가구 배치를 실제로 다시 하는 일에 가깝다.
sort가 특히 헷갈리는 이유
JavaScript에서 정렬을 붙일 때 특히 헷갈리는 이유는 sort의 동작 방식 때문입니다. 초보자는 보통 sort를 “정렬된 새 배열을 돌려주는 도구”처럼 상상하기 쉽습니다. 그런데 실제로는 기존 배열의 순서 자체를 바꾸는 방식으로 작동합니다. 바로 이 지점에서 검색, 필터, 저장과 연결되면 혼란이 커집니다.
아래를 보면 차이가 조금 더 분명해집니다.
const activeTodos = todos.filter(todo => !todo.done);
이 코드는 todos 자체를 바꾸지 않고, 조건에 맞는 새 배열을 만듭니다. 반면 아래는 다릅니다.
const sortedTodos = todos.sort((a, b) => b.createdAt - a.createdAt);
이 코드는 sortedTodos만 정렬된 것처럼 보이지만, 실제로는 todos의 순서까지 같이 바뀝니다. 초보자가 여기서 가장 많이 놀라는 이유도 바로 이것입니다. 변수 이름은 달라졌는데 원본도 함께 흔들렸기 때문입니다.
| 메서드 | 초보자가 흔히 느끼는 이미지 | 실제 동작 | 원본 배열이 바뀌는가 |
|---|---|---|---|
| filter | 일부만 골라서 새 목록을 만든다 | 조건에 맞는 새 배열을 반환한다 | 아니오 |
| map | 모양을 바꾼 새 목록을 만든다 | 각 항목을 변환한 새 배열을 반환한다 | 아니오 |
| sort | 정렬된 새 목록을 만드는 것처럼 보인다 | 배열 자체의 순서를 바꾼다 | 예 |
그래서 sort를 붙일 때는 한 번 더 생각해야 합니다. 지금 내가 바꾸고 싶은 것은 원본 순서인가, 화면 순서인가. 대부분의 기본 정렬은 화면 순서만 바꾸면 되기 때문에, 정렬 전에 복사본을 만드는 습관이 꽤 중요해집니다.
여기서 핵심
filter는 “골라낸 결과”를 만들고, sort는 “배열 자체의 순서”를 건드린다. 이 차이를 놓치면 검색과 정렬이 섞일 때 바로 흔들린다.
안전한 흐름: 복사본을 정렬하고 원본은 지키기
입문 단계에서 가장 무난한 방식은 이렇습니다. 원본 목록은 그대로 두고, 검색과 필터를 적용한 뒤, 마지막에 복사본을 정렬해서 화면에만 보여주는 방식입니다. 이렇게 하면 검색어를 지웠을 때도 전체 목록이 자연스럽게 돌아오고, 정렬 방식을 바꿔도 저장된 원본 자체는 흔들리지 않습니다.
예를 들면 아래처럼 흐름을 잡을 수 있습니다.
let todos = [];
let currentFilter = 'all';
let searchKeyword = '';
let currentSort = 'newest';
여기서 todos는 전체 원본 데이터입니다. currentFilter, searchKeyword, currentSort는 화면을 어떻게 보여줄지 결정하는 상태입니다. 즉, 정렬 기준도 데이터 그 자체가 아니라 보여주는 방식에 속합니다.
그다음 보여줄 목록을 계산하는 함수를 만들 수 있습니다.
function getVisibleTodos() {
const keyword = searchKeyword.trim().toLowerCase();
const filtered = todos.filter(todo => {
const matchFilter =
currentFilter === 'all'
? true
: currentFilter === 'active'
? !todo.done
: todo.done;
const matchKeyword = todo.text.toLowerCase().includes(keyword);
return matchFilter && matchKeyword;
});
if (currentSort === 'newest') {
return [...filtered].sort((a, b) => b.createdAt - a.createdAt);
}
if (currentSort === 'oldest') {
return [...filtered].sort((a, b) => a.createdAt - b.createdAt);
}
if (currentSort === 'completedLast') {
return [...filtered].sort((a, b) => {
if (a.done === b.done) return 0;
return a.done ? 1 : -1;
});
}
return filtered;
}
이 코드에서 특히 중요한 부분은 [...filtered]입니다. 이 한 줄이 filtered의 복사본을 만들어 주고, 그 복사본을 정렬하게 만듭니다. 덕분에 원본 todos는 그대로 남고, 화면에 보여줄 순서만 달라집니다.
그리고 실제 렌더링은 이 계산된 결과만 사용하면 됩니다.
function renderTodos() {
const visibleTodos = getVisibleTodos();
todoList.innerHTML = '';
visibleTodos.forEach(todo => {
// 화면에 그리기
});
}
| 구성 요소 | 역할 | 왜 도움이 되는가 |
|---|---|---|
| todos | 실제 전체 데이터 | 저장, 수정, 삭제의 기준이 흔들리지 않는다 |
| currentSort | 현재 정렬 방식 | 정렬을 화면 설정처럼 다룰 수 있다 |
| getVisibleTodos | 검색, 필터, 정렬을 적용한 화면용 목록 계산 | 조건이 한곳에 모여 있어 수정이 덜 흔들린다 |
| renderTodos | 계산된 결과를 화면에 반영 | 원본을 직접 건드리지 않고도 다양한 보기 방식을 만들 수 있다 |
한 가지 덧붙이면, 최신순이나 오래된순을 쓰려면 생성 시각을 나타내는 값이 있으면 더 자연스럽습니다. 연습용 앱에서는 id를 시간 기반으로 만든 경우 그 값을 임시로 쓸 수도 있습니다. 다만 역할을 분명하게 보고 싶다면 createdAt 같은 필드를 따로 두는 편이 나중에 읽기 쉽습니다.
입문자 기준으로 가장 무난한 원칙
검색과 필터와 정렬은 먼저 화면용 목록을 계산하는 데 쓰고, 원본 목록은 수정과 저장이 일어날 때만 직접 바꾸는 편이 안전하다.
검색, 필터, 정렬이 함께 있을 때의 순서
기능이 하나씩 붙을 때는 별로 어렵지 않아 보이는데, 검색과 필터와 정렬이 한 번에 만나면 갑자기 복잡해집니다. 이때 중요한 것은 코드 한 줄보다 흐름의 순서입니다. 보통은 아래 순서로 생각하는 편이 가장 무난합니다.
- 원본 데이터에서 시작한다.
항상 기준은 전체 목록입니다. - 검색과 필터로 보여줄 범위를 좁힌다.
무엇을 보여줄지 먼저 정합니다. - 정렬로 그 범위 안에서 순서를 정한다.
마지막에 보기 좋은 순서를 입힙니다. - 렌더링한다.
계산된 결과를 화면에 그립니다.
즉, “무엇을 보여줄까”가 먼저이고, “어떤 순서로 보여줄까”가 그다음입니다. 물론 상황에 따라 순서를 조금 다르게 짤 수도 있습니다. 다만 입문자 기준에서는 범위를 좁힌 뒤 마지막에 정렬하는 편이 읽기 쉽고, 검색 결과가 적을수록 정렬도 덜 무겁게 느껴집니다.
| 사용자 행동 | 바뀌어야 하는 것 | 바뀌면 안 되는 것 |
|---|---|---|
| 검색어 입력 | searchKeyword | 원본 todos |
| 필터 버튼 클릭 | currentFilter | 원본 todos |
| 정렬 방식 변경 | currentSort | 원본 todos |
| 항목 삭제 | 원본 todos | 보이는 순서나 index |
| 저장 | 원본 todos | 정렬된 visible 목록만 따로 저장하는 것 |
여기서도 수정과 삭제는 여전히 같은 원칙을 따릅니다. 정렬된 화면에서는 보이는 순서가 달라져 있을 뿐, 실제 항목의 정체성은 id로 유지됩니다. 그래서 검색 결과 화면이든 최신순 화면이든, 삭제와 완료 체크는 계속 id 기준으로 처리해야 안정적입니다.
정리하면
검색과 필터와 정렬은 화면을 계산하는 단계이고, 수정과 삭제와 저장은 원본을 다루는 단계다. 이 둘이 섞이면 코드가 급격히 흔들리기 시작한다.
초보자가 자주 하는 실수 6가지
정렬을 붙일 때는 비슷한 실수가 반복됩니다. 겉보기에는 사소해 보여도, 실제로는 원본 데이터와 화면용 데이터를 섞어버리는 순간부터 일이 커지는 경우가 많습니다. 아래 여섯 가지는 특히 자주 보이는 패턴입니다.
| 실수 | 겉으로 보이는 증상 | 실제로 문제인 부분 | 어떻게 보는 게 좋은가 |
|---|---|---|---|
| sort를 바로 원본 배열에 적용한다 | 정렬을 바꿀수록 원래 순서를 잃어버린다 | 화면용 정렬이 원본 순서 변경으로 이어졌다 | 복사본을 만든 뒤 정렬하는 편이 안전하다 |
| 정렬된 목록을 저장한다 | 다음에 열어도 그 정렬 결과만 남아 있다 | 보여주는 순서와 실제 저장 기준을 구분하지 못했다 | 저장은 원본 데이터 기준으로 하는지 먼저 본다 |
| 정렬 화면에서 index로 삭제한다 | 엉뚱한 항목이 지워지거나 수정된다 | 보이는 순서를 항목 정체성으로 착각했다 | 정렬 후에도 삭제와 수정은 id 기준으로 처리한다 |
| 최신순 기준이 되는 값이 없다 | 정렬은 되는데 기준이 불분명해서 결과가 어색하다 | createdAt 같은 기준 정보가 없다 | 처음부터 정렬 기준이 될 값을 분명히 두는 편이 낫다 |
| 검색과 정렬 로직이 여러 곳에 흩어져 있다 | 한쪽만 수정하면 결과가 들쭉날쭉하다 | 화면 계산 로직이 한곳에 모여 있지 않다 | 보여줄 목록 계산 함수를 따로 두는 편이 낫다 |
| 화면용 정렬과 사용자가 직접 바꾼 순서를 같은 것으로 본다 | 언제 저장하고 언제 안 저장할지가 계속 헷갈린다 | 보기 방식과 실제 순서 기능을 구분하지 못했다 | 단순 정렬인지, 사용자가 만든 순서인지 먼저 정리한다 |
특히 첫 번째와 두 번째 실수는 거의 붙어 다닙니다. 원본 배열을 sort로 바로 바꾸고, 그 결과를 저장하면 나중에는 “정렬 버튼을 누른 것”인지 “목록 자체를 다시 배치한 것”인지가 흐려집니다. 입문자에게는 이 둘을 의식적으로 떼어놓는 습관이 꽤 중요합니다.
실전에서 가장 자주 흔들리는 부분
정렬 화면에서 보이는 순서를 실제 데이터의 순서라고 믿는 순간, 수정과 삭제와 저장까지 같이 불안해지기 시작한다.
'바이브코딩 > 이론' 카테고리의 다른 글
| 12. 마우스로 옮겼는데 왜 순서가 안 남을까: 바이브코딩 드래그 앤 드롭 입문 (1) | 2026.03.21 |
|---|---|
| 11. 내가 정한 순서가 왜 안 남을까: 바이브코딩에서 order 값이 필요한 이유 (0) | 2026.03.19 |
| 9. 검색을 붙였더니 왜 목록이 꼬일까: 바이브코딩에서 원본 데이터 지키는 법 (0) | 2026.03.15 |
| 8. 기능이 늘수록 코드가 왜 금방 엉킬까: 바이브코딩에서 함수 나누는 기준 (0) | 2026.03.14 |
| 7. 완료 체크를 붙이면 왜 갑자기 꼬일까: 바이브코딩에서 done 상태 이해하기 (0) | 2026.03.13 |
