빌드노트
숫자를 sort했는데 10이 2보다 앞에 온다
숫자 배열의 기본 sort가 문자열 순서를 사용하는 이유와 원본 변경을 실제 출력으로 확인한다. 숫자 비교 함수, 복사 후 정렬, toSorted의 선택 기준을 설명하고 숫자 문자열·NaN의 변환과 검증 경계를 구분한다.
JavaScript에서 [2, 10, 1].sort()를 실행하면 [1, 10, 2]가 된다. 일반 배열의 기본 sort()는 숫자 크기가 아니라 문자열로 바꿔 비교한 순서를 사용한다. 유한한 number 값의 오름차순에는 (a, b) => a - b를 넘긴다. 원래 순서도 필요하다면 [...values].sort((a, b) => a - b)처럼 복사한 배열을 정렬해야 한다. 비교 함수를 추가해도 sort()의 원본 변경은 그대로다.
숫자 비교를 넣으면 음수까지 어떤 순서가 될까
MDN의 sort 설명 (새 탭에서 열림)은 비교 함수를 생략하면 문자열의 UTF-16 코드 단위 순서로 비교한다고 설명한다. 이 입력에서 문자열 "10"의 첫 글자 "1"이 "2"보다 앞이어서 10이 2 앞에 놓인다. 비교에 문자열을 쓴다는 뜻이지, 배열 안 숫자들이 문자열로 교체된다는 뜻은 아니다.
다음은 실제 앱 대신 합성 배열로 확인한 코드다. Node.js가 설치된 빈 폴더에서 sort-demo.mjs 파일로 저장하고 node sort-demo.mjs를 실행한다. 아래 출력은 Windows의 Node.js 22.23.2에서 얻었다. node 명령을 찾지 못한다면 먼저 Node.js 실행 환경을 준비한다.
const values = [2, 10, 1, -2, -10];
console.log("default", JSON.stringify([...values].sort()));
console.log("numeric", JSON.stringify([...values].sort((a, b) => a - b)));
console.log("source", JSON.stringify(values));
default [-10,-2,1,10,2]
numeric [-10,-2,1,2,10]
source [2,10,1,-2,-10]
비교 함수는 두 값의 앞뒤를 반환값의 부호로 알려 준다. 음수면 a가 먼저, 양수면 b가 먼저이며 0이면 같은 순서로 취급한다. 2 - 10은 음수이므로 2가 앞선다. -2 - (-10)은 양수이므로 -10이 앞선다. 이 규칙은 ECMAScript의 sort 명세 (새 탭에서 열림)에서 확인할 수 있다.
위 예제는 두 정렬 모두 복사본에서 실행했다. 자기 코드에서 결과가 다르면 정렬 직전 값뿐 아니라 typeof도 확인한다. 특히 toSorted()로 바꿨더라도 비교 함수를 생략하면 숫자 순서 문제는 남는다.
반환값을 다른 변수에 받아도 원본은 바뀐다
다음 코드는 앞 예제와 별도로 실행한다. const는 배열 변수의 재대입을 막지만 배열 내용의 변경까지 막지는 않는다.
const original = [2, 10, 1];
const sorted = original.sort((a, b) => a - b);
console.log(sorted === original);
console.log(JSON.stringify(original));
true
[1,2,10]
true는 sorted와 original이 같은 배열을 가리킨다는 뜻이다. 다른 곳에서 이 배열을 참조하고 있었다면 그곳에서도 바뀐 순서를 읽는다. MDN의 반환값 설명 (새 탭에서 열림)도 정렬된 원본 배열의 참조를 반환한다고 명시한다. 새 변수 이름만으로 복사본이 생기지는 않는다.
화면 상태나 정렬 전 목록을 보존해야 한다면 다음처럼 먼저 복사한다. 이 코드도 새로 실행한다.
const original = [2, 10, 1];
const sorted = [...original].sort((a, b) => a - b);
console.log(sorted === original);
console.log(JSON.stringify(original));
console.log(JSON.stringify(sorted));
false
[2,10,1]
[1,2,10]
지원되는 환경에서는 정렬 줄을 const sorted = original.toSorted((a, b) => a - b);로 써도 된다. 같은 입력으로 실행했을 때 참조 비교는 false, 원본은 [2,10,1], 결과는 [1,2,10]이었다. 반면 비교 함수 없는 original.toSorted()의 결과는 [1,10,2]였다. MDN의 toSorted 설명 (새 탭에서 열림)은 새 배열 반환과 기본 비교 규칙을 구분한다.
자기 실행 환경에서 typeof Array.prototype.toSorted가 "function"인지 확인한다. 지원하지 않으면 위의 복사 후 sort() 방식을 쓸 수 있다. 이번 Node에서는 지원을 확인했으며, 독자의 브라우저 지원 여부는 별도로 확인해야 한다. 두 방식은 배열을 얕게 복사한다. 숫자 목록의 순서를 보존하는 데는 충분하지만, 안에 든 객체까지 별도로 복제하는 기능은 아니다.
숫자 문자열이 정렬된다고 입력 검증까지 끝난 것은 아니다
다음 두 줄도 같은 Node에서 실행했다. 출력의 따옴표는 문자열인 원소를 구별한다.
console.log([2, "10", 1, "-2"].sort((a, b) => a - b));
console.log([2, NaN, 1].sort((a, b) => a - b));
[ '-2', 1, 2, '10' ]
[ 2, NaN, 1 ]
첫 줄은 뺄셈 중 숫자 문자열이 수치로 변환돼 순서는 맞아 보인다. 하지만 결과의 "-2"와 "10"은 여전히 문자열이다. 별도로 ['2', '10', '1', '-2', '-10']을 같은 함수로 정렬한 결과도 ['-10', '-2', '1', '2', '10']으로, 모든 원소의 타입은 string이었다. 비교할 때의 변환과 저장할 값의 변환은 별개다.
둘째 줄에서는 1이 2 뒤에 남았다. 이 실행에서 비교한 NaN - 2와 1 - NaN은 모두 NaN이었다. ECMAScript의 CompareArrayElements (새 탭에서 열림)는 비교 함수의 결과가 NaN이면 0으로 취급한다. 예외가 나지 않았다는 이유로 유효한 숫자 정렬이 끝났다고 판단할 수 없다.
숫자 문자열과 NaN을 함께 넣은 [2, "10", NaN, 1, "-2"]도 실행했다. 관찰 결과는 [2, "10", NaN, "-2", 1]이었다. 이 순서나 NaN의 위치를 다른 엔진에서도 보장하는 결과로 사용하면 안 된다. 비교 규칙이 일관되지 않을 때 정렬 순서는 구현에 맡겨진다는 명세의 조건 (새 탭에서 열림) 때문이다. NaN을 만나면 0을 반환하도록 감싸는 것도 무효 입력을 해결하는 정책이 되지는 않는다.
입력이 이미 숫자여야 한다면 정렬 전에 타입과 유효성을 검사한다. 다음은 이 글처럼 빈 슬롯 없는 일반 배열에서 유한한 number만 허용하는 예다.
const values = [2, NaN, 1];
if (!values.every(Number.isFinite)) {
throw new TypeError("finite numbers only");
}
const sorted = [...values].sort((a, b) => a - b);
이 입력은 TypeError: finite numbers only로 정렬 전에 멈췄다. [2, "10", 1]과 [2, Infinity, 1]도 거절됐고, [2, 10, 1]은 통과해 [1, 2, 10]이 됐다. Number.isFinite 설명 (새 탭에서 열림)에 따르면 이 검사는 문자열을 숫자로 자동 변환하지 않으며 NaN과 무한대도 거절한다. 무효 값을 버리거나 0으로 바꾸지는 않는다.
입력 계약이 숫자 문자열을 허용한다면 변환은 별도 단계로 정한다. ['2', '10', '1'].map(Number)는 [2, 10, 1]이 되었고, 위 검사를 통과한 뒤 수치 정렬할 수 있었다. 다만 Number('oops')는 NaN, Number('')는 0이었다. Number의 변환 규칙 (새 탭에서 열림)에도 이 차이가 설명돼 있다. 빈 값을 누락으로 다루는 폼이라면 변환 전에 빈 문자열을 거절해야 한다. 변환 뒤 유한수 검사만 하면 그 빈 값은 정상 숫자 0과 구별되지 않는다.
이번 실행은 빈 슬롯 없는 작은 일반 배열을 Node.js 22.23.2에서 비교한 것이다. 실제 화면 상태 갱신, 다른 브라우저 엔진, 객체·BigInt·희소 배열은 검증하지 않았으며 성능도 측정하지 않았다. 자기 코드에서는 정렬 직전 입력의 타입과 무효 값 처리부터 정하고, 그다음 원본을 바꿔도 되는지에 따라 sort()와 복사 후 정렬을 선택한다.