배열을 정렬할 때 sort() 메서드를 쓰는데, 숫자가 제대로 정렬 안 되거나 한글이 이상하게 섞이거나 객체 배열에서 특정 필드로 정렬이 안 되는 경험을 해봤을까? 대부분의 개발자들은 sort()를 잠깐 썼을 때는 문제없이 작동하는 것처럼 보이지만, 실제 운영 데이터에서는 정렬이 엉망이 된다는 사실을 모르고 있다. 이번에는 JavaScript sort()가 정확히 뭘 하는지, 왜 버그가 생기는지, 어떻게 제대로 쓰는지 완벽하게 정리해서 소개하겠다.

 

1단계. sort() 기본 동작 이해하기

JavaScript의 sort() 메서드는 기본적으로 배열의 요소를 문자열로 변환한 다음 유니코드 코드 포인트 순서로 정렬한다. 즉, 숫자든 객체든 모두 문자로 취급한다는 뜻이다.

예를 들어 [10, 2, 30, 1]을 sort()만 호출하면 [1, 10, 2, 30]이 된다. "10"이 "2"보다 먼저 오기 때문이다. 이게 sort()의 가장 흔한 버그 원인이다.

 

✗ 잘못된 코드 - sort() 콜백 없이 쓰기
const numbers = [10, 2, 30, 1, 5];
console.log(numbers.sort());
// 결과: [1, 10, 2, 30, 5] ← 틀렸다!
// 이유: 문자열 비교로 "10" < "2"가 false이므로 순서 유지

 

✓ 올바른 코드 - 비교 콜백 함수 사용
const numbers = [10, 2, 30, 1, 5];
console.log(numbers.sort((a, b) => a - b));
// 결과: [1, 2, 5, 10, 30] ← 맞다!

콜백 함수에서 a - b를 반환하면 오름차순, b - a를 반환하면 내림차순이다. 반환값이 음수면 a가 먼저, 양수면 b가 먼저, 0이면 순서 유지라는 규칙만 기억하면 된다.

 

2단계. 다양한 정렬 시나리오 - 문자열, 한글, 객체

숫자뿐만 아니라 문자열, 한글, 객체 배열도 각각 다르게 정렬해야 한다.

 

한글 정렬 - localeCompare() 사용하기

한글을 알파벳 순으로 정렬할 때는 단순 문자 비교로는 안 되고, localeCompare() 메서드를 써야 한다.

const names = ["주영", "가나", "다은", "나름"];
console.log(names.sort((a, b) => a.localeCompare(b, 'ko-KR')));
// 결과: ["가나", "나름", "다은", "주영"] ← 한글 가나다순

localeCompare()는 문자열이 같으면 0을, a가 먼저면 음수를, b가 먼저면 양수를 반환하므로 sort() 콜백에 바로 쓸 수 있다. 'ko-KR' 로케일을 명시하면 한글 글자 순서를 정확히 인식한다.

 

객체 배열 정렬 - 특정 필드 기준
const users = [
  { id: 3, name: "Alice", age: 25 },
  { id: 1, name: "Bob", age: 30 },
  { id: 2, name: "Charlie", age: 28 }
];

// id 순서로 정렬
console.log(users.sort((a, b) => a.id - b.id));
// 결과: [{id:1}, {id:2}, {id:3}]

// name 순서로 정렬
console.log(users.sort((a, b) => a.name.localeCompare(b.name)));
// 결과: [{name:"Alice"}, {name:"Bob"}, {name:"Charlie"}]

// age 역순으로 정렬
console.log(users.sort((a, b) => b.age - a.age));
// 결과: [{age:30}, {age:28}, {age:25}]

 

3단계. 실전 예제 - 쇼핑몰 상품 정렬

실제 웹사이트에서 상품을 여러 기준으로 정렬해야 하는 경우를 생각해보자.

const products = [
  { name: "노트북", price: 1500000, rating: 4.5 },
  { name: "마우스", price: 35000, rating: 4.2 },
  { name: "키보드", price: 150000, rating: 4.8 },
  { name: "모니터", price: 350000, rating: 3.9 }
];

// 가격 낮은 순
function sortByPriceAsc(products) {
  return [...products].sort((a, b) => a.price - b.price);
}

// 평점 높은 순
function sortByRatingDesc(products) {
  return [...products].sort((a, b) => b.rating - a.rating);
}

// 상품명 가나다순
function sortByNameKr(products) {
  return [...products].sort((a, b) => a.name.localeCompare(b.name, 'ko-KR'));
}

console.log(sortByPriceAsc(products));
// [{name:"마우스", price:35000}, {name:"키보드", price:150000}, ...]

console.log(sortByRatingDesc(products));
// [{name:"키보드", rating:4.8}, {name:"노트북", rating:4.5}, ...]

console.log(sortByNameKr(products));
// [{name:"경키보드"}, {name:"마우스"}, {name:"모니터"}, {name:"노트북"}]

주의할 점은 sort()가 원본 배열을 변경한다는 것이다. 원본을 보존하려면 스프레드 연산자 [...]를 사용해서 복사본을 만든 후 정렬해야 한다.

 

4단계. 주의사항 - 자주 하는 실수들

 

✗ 실수 1: 원본 배열 변경
const original = [3, 1, 2];
const sorted = original.sort((a, b) => a - b);
console.log(original); // [1, 2, 3] ← 원본이 변경됨!
console.log(sorted === original); // true ← 같은 참조!

만약 원본을 유지해야 한다면 스프레드 연산자나 slice()로 복사하고 정렬하자.

 

✓ 올바른 방법
const original = [3, 1, 2];
const sorted = [...original].sort((a, b) => a - b);
console.log(original); // [3, 1, 2] ← 원본 유지
console.log(sorted); // [1, 2, 3]

 

✗ 실수 2: null이나 undefined 처리 안 하기
const data = [{ age: 25 }, { age: null }, { age: 30 }];
data.sort((a, b) => a.age - b.age);
// NaN 발생, 정렬 불안정

 

✓ 올바른 방법
const data = [{ age: 25 }, { age: null }, { age: 30 }];
data.sort((a, b) => {
  const ageA = a.age ?? Infinity; // null/undefined는 맨 뒤로
  const ageB = b.age ?? Infinity;
  return ageA - ageB;
});
// 결과: [{age:25}, {age:30}, {age:null}]

 

✗ 실수 3: 숫자 문자열 정렬
const prices = ["100", "2", "30"];
console.log(prices.sort()); // ["100", "2", "30"] ← 문자순!

 

✓ 올바른 방법
const prices = ["100", "2", "30"];
console.log(prices.sort((a, b) => Number(a) - Number(b)));
// ["2", "30", "100"]

 

✗ 실수 4: 복합 정렬 조건 누락
// 먼저 카테고리로, 같으면 가격으로 정렬하고 싶을 때
const products = [
  { category: "전자", price: 100 },
  { category: "의류", price: 50 },
  { category: "전자", price: 80 }
];

products.sort((a, b) => a.category.localeCompare(b.category));
// 카테고리만 정렬되고 같은 카테고리 내 가격은 섞임

 

✓ 올바른 방법
products.sort((a, b) => {
  const categoryCompare = a.category.localeCompare(b.category);
  // 카테고리가 같으면 가격으로 정렬
  return categoryCompare !== 0 ? categoryCompare : a.price - b.price;
});
// 결과: [{카테고리:"의류", price:50}, {카테고리:"전자", price:80}, {카테고리:"전자", price:100}]

 

5단계. 성능 최적화 - 대용량 배열 정렬

수만 개 이상의 요소를 정렬할 때 콜백 함수에서 복잡한 연산을 하면 성능이 급격히 떨어진다. 객체 배열에서 문자열 필드로 정렬할 때 매번 localeCompare()를 호출하면 오버헤드가 크다.

// ✗ 비효율적: 정렬 중에 매번 객체 접근과 문자열 비교
const hugeList = Array(100000).fill().map((_, i) => ({
  id: i,
  name: String.fromCharCode(Math.random() * 26 + 65) // 랜덤 문자
}));

hugeList.sort((a, b) => a.name.localeCompare(b.name, 'ko-KR'));
// 시간 측정 시 약 200ms 이상 소요

 

// ✓ 효율적: 사전 변환 후 정렬 (Schwartzian Transform)
const startTime = performance.now();
const transformed = hugeList.map((item, index) => ({
  item,
  sortKey: item.name // 정렬 키를 미리 계산
}));

transformed.sort((a, b) => a.sortKey.localeCompare(b.sortKey, 'ko-KR'));

const result = transformed.map(t => t.item);
const endTime = performance.now();
console.log(`정렬 시간: ${endTime - startTime}ms`);
// 약 50ms 정도로 단축

대용량 데이터는 정렬 키를 미리 계산해서 임시 배열에 저장한 후 정렬하면, 정렬 중에 반복적인 계산을 피할 수 있어 훨씬 빠르다.

 

핵심 정리

JavaScript sort()는 콜백 함수 없이 쓰면 문자열 기준으로 정렬되므로 숫자가 망가진다. 숫자는 a - b, 문자는 localeCompare(), 객체는 특정 필드를 지정해서 정렬해야 한다. 특히 한글 정렬은 반드시 localeCompare()에 'ko-KR' 로케일을 명시해야 올바른 순서를 얻을 수 있다. 원본 배열 변경을 피하려면 스프레드 연산자로 복사하고, 대용량 데이터는 정렬 키를 미리 계산해서 성능을 최적화하자. 이 글에서 다룬 콜백 함수 패턴과 null 처리, 복합 조건 정렬을 반복 연습하면 배열 정렬으로 인한 버그를 거의 모두 막을 수 있을 것이다.