내장된 숫자가 있는 문자열을 올바르게 정렬하는 방법
숫자 정렬을 사용하여 파일 이름, 버전 번호 및 숫자가 포함된 기타 문자열을 자연스러운 순서로 정렬하세요
소개
숫자가 포함된 문자열을 정렬할 때, file1.txt, file2.txt, file10.txt가 이 순서대로 나타나기를 기대합니다. 하지만 JavaScript의 기본 문자열 비교는 file1.txt, file10.txt, file2.txt 순으로 정렬합니다. 이는 문자열이 문자별로 비교되기 때문에 발생하며, 10의 문자 1이 문자 2보다 앞에 오기 때문입니다.
이 문제는 파일 이름, 버전 번호, 주소, 제품 코드 또는 숫자가 포함된 다른 문자열을 정렬할 때마다 발생합니다. 이러한 잘못된 정렬은 사용자를 혼란스럽게 하고 데이터 탐색을 어렵게 만듭니다.
JavaScript는 이 문제를 해결하기 위한 숫자 옵션이 있는 Intl.Collator API를 제공합니다. 이 강의에서는 숫자 콜레이션이 어떻게 작동하는지, 기본 문자열 비교가 왜 실패하는지, 그리고 숫자가 포함된 문자열을 자연스러운 숫자 순서로 정렬하는 방법을 설명합니다.
숫자 콜레이션이란
숫자 콜레이션은 숫자 시퀀스를 개별 문자가 아닌 숫자로 취급하는 비교 방법입니다. 문자열을 비교할 때, 콜레이터는 숫자 시퀀스를 식별하고 그 숫자 값으로 비교합니다.
숫자 콜레이션이 비활성화된 경우, 문자별 비교에서 첫 번째 다른 위치에서 1이 2보다 앞에 오기 때문에 file10.txt가 file2.txt보다 앞에 옵니다. 콜레이터는 10이 2보다 큰 숫자를 나타낸다는 것을 고려하지 않습니다.
숫자 콜레이션이 활성화된 경우, 콜레이터는 10과 2를 완전한 숫자로 인식하고 숫자적으로 비교합니다. 10이 2보다 크기 때문에, file2.txt가 올바르게 file10.txt 앞에 옵니다.
이 동작은 사람들이 자연 정렬 또는 자연 순서라고 부르는 것을 생성하며, 숫자가 포함된 문자열이 엄격하게 알파벳순이 아닌 사람들이 기대하는 방식으로 정렬됩니다.
숫자에 대한 기본 문자열 비교가 실패하는 이유
JavaScript의 기본 문자열 비교는 사전식 정렬(lexicographic ordering)을 사용하며, 이는 유니코드 코드 포인트 값을 사용하여 왼쪽에서 오른쪽으로 문자별로 문자열을 비교합니다. 이 방식은 알파벳 텍스트에는 올바르게 작동하지만 숫자에 대해서는 예상치 못한 결과를 생성합니다.
사전식 비교가 이러한 문자열을 처리하는 방식을 살펴보겠습니다:
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
files.sort();
console.log(files);
// 출력: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
이 비교는 각 문자 위치를 독립적으로 검사합니다. file 다음에 처음으로 다른 위치에서 1과 2를 비교합니다. 1의 유니코드 값이 2보다 낮기 때문에, file1로 시작하는 모든 문자열은 뒤에 무엇이 오든 상관없이 file2로 시작하는 모든 문자열보다 앞에 위치합니다.
이로 인해 file1.txt, file10.txt, file2.txt, file20.txt 순서가 생성되며, 이는 숫자 순서에 대한 인간의 기대를 위반합니다.
numeric 옵션을 사용한 Intl.Collator 활용
Intl.Collator 생성자는 numeric 속성이 있는 옵션 객체를 받습니다. numeric: true로 설정하면 숫자 콜레이션이 활성화되어 콜레이터가 숫자 시퀀스를 숫자 값으로 비교합니다.
const collator = new Intl.Collator('en-US', { numeric: true });
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
files.sort(collator.compare);
console.log(files);
// 출력: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']
콜레이터의 compare 메서드는 첫 번째 인수가 두 번째 인수보다 앞에 와야 할 때 음수를, 동일할 때 0을, 첫 번째 인수가 두 번째 인수 뒤에 와야 할 때 양수를 반환합니다. 이는 JavaScript의 Array.sort() 메서드가 기대하는 서명과 일치합니다.
정렬된 결과는 파일을 자연스러운 숫자 순서로 배치합니다. 콜레이터는 1 < 2 < 10 < 20임을 인식하여 사람들이 기대하는 순서를 생성합니다.
혼합 영숫자 문자열 정렬
숫자 정렬은 숫자가 끝에만 있는 것이 아니라 어느 위치에나 나타나는 문자열을 처리합니다. 정렬기는 알파벳 부분은 일반적으로 비교하고 숫자 부분은 숫자로 비교합니다.
const collator = new Intl.Collator('en-US', { numeric: true });
const addresses = ['123 Oak St', '45 Oak St', '1234 Oak St', '5 Oak St'];
addresses.sort(collator.compare);
console.log(addresses);
// 출력: ['5 Oak St', '45 Oak St', '123 Oak St', '1234 Oak St']
정렬기는 각 문자열의 시작 부분에 있는 숫자 시퀀스를 식별하고 이를 숫자로 비교합니다. 사전식 비교라면 다른 순서가 나올 수 있지만, 정렬기는 5 < 45 < 123 < 1234라는 것을 인식합니다.
버전 번호 정렬하기
버전 번호는 숫자 정렬의 일반적인 사용 사례입니다. 1.2.10과 같은 소프트웨어 버전은 1.2.2 뒤에 와야 하지만, 사전식 비교는 잘못된 순서를 생성합니다.
const collator = new Intl.Collator('en-US', { numeric: true });
const versions = ['1.2.10', '1.2.2', '1.10.5', '1.2.5'];
versions.sort(collator.compare);
console.log(versions);
// 출력: ['1.2.2', '1.2.5', '1.2.10', '1.10.5']
정렬기는 각 숫자 구성 요소를 올바르게 비교합니다. 1.2.2, 1.2.5, 1.2.10 시퀀스에서 세 번째 구성 요소가 숫자적으로 증가한다는 것을 인식합니다. 1.10.5에서는 두 번째 구성 요소가 10이며, 이는 2보다 크다는 것을 인식합니다.
제품 코드 및 식별자 작업하기
제품 코드, 송장 번호 및 기타 식별자는 종종 문자와 숫자를 혼합합니다. 숫자 정렬은 이러한 항목들이 논리적 순서로 정렬되도록 보장합니다.
const collator = new Intl.Collator('en-US', { numeric: true });
const products = ['PROD-1', 'PROD-10', 'PROD-2', 'PROD-100'];
products.sort(collator.compare);
console.log(products);
// 출력: ['PROD-1', 'PROD-2', 'PROD-10', 'PROD-100']
알파벳 접두사 PROD-는 모든 문자열에서 일치하므로, 정렬기는 숫자 접미사를 비교합니다. 결과는 사전식 순서가 아닌 증가하는 숫자 순서를 반영합니다.
다양한 로케일로 정렬하기
numeric 옵션은 모든 로케일에서 작동합니다. 다양한 로케일은 알파벳 문자에 대해 서로 다른 정렬 규칙을 가질 수 있지만, 숫자 비교 동작은 일관되게 유지됩니다.
const enCollator = new Intl.Collator('en-US', { numeric: true });
const deCollator = new Intl.Collator('de-DE', { numeric: true });
const items = ['item1', 'item10', 'item2'];
console.log(items.sort(enCollator.compare));
// 출력: ['item1', 'item2', 'item10']
console.log(items.sort(deCollator.compare));
// 출력: ['item1', 'item2', 'item10']
두 로케일 모두 동일한 결과를 생성하는데, 이는 문자열이 ASCII 문자와 숫자만 포함하기 때문입니다. 문자열에 로케일별 특수 문자가 포함된 경우, 알파벳 비교는 로케일 규칙을 따르지만 숫자 비교는 일관되게 유지됩니다.
정렬 없이 문자열 비교하기
전체 배열을 정렬하지 않고도 두 문자열 간의 관계를 확인하기 위해 콜레이터의 compare 메서드를 직접 사용할 수 있습니다.
const collator = new Intl.Collator('en-US', { numeric: true });
console.log(collator.compare('file2.txt', 'file10.txt'));
// 출력: -1 (음수는 첫 번째 인수가 두 번째 인수보다 앞에 온다는 의미)
console.log(collator.compare('file10.txt', 'file2.txt'));
// 출력: 1 (양수는 첫 번째 인수가 두 번째 인수 뒤에 온다는 의미)
console.log(collator.compare('file2.txt', 'file2.txt'));
// 출력: 0 (0은 인수가 동일하다는 의미)
이는 정렬된 목록에 항목을 삽입하거나 값이 특정 범위 내에 있는지 확인하는 등 배열을 수정하지 않고 순서를 확인해야 할 때 유용합니다.
소수점 숫자의 제한 사항 이해하기
숫자 콜레이션은 숫자 시퀀스를 비교하지만, 소수점을 숫자의 일부로 인식하지 않습니다. 마침표 문자는 소수점 구분자가 아닌 일반 구분자로 취급됩니다.
const collator = new Intl.Collator('en-US', { numeric: true });
const measurements = ['0.5', '0.05', '0.005'];
measurements.sort(collator.compare);
console.log(measurements);
// 출력: ['0.005', '0.05', '0.5']
콜레이터는 각 측정값을 세 개의 별도 숫자 구성 요소로 취급합니다: 마침표 이전 부분, 마침표 자체, 마침표 이후 부분. 먼저 0과 0을 비교하고(동일), 그 다음 마침표 이후 부분을 별도의 숫자로 비교합니다: 5, 5, 5(동일). 그런 다음 두 번째 소수 자리를 비교합니다: 없음, 5, 없음. 이로 인해 소수점 숫자에 대해 잘못된 정렬이 발생합니다.
소수점 숫자를 정렬하려면 실제 숫자로 변환하여 숫자적으로 정렬하거나, 문자열 패딩을 사용하여 올바른 사전식 순서를 보장해야 합니다.
숫자 정렬과 다른 옵션 결합하기
숫자(numeric) 옵션은 sensitivity와 caseFirst와 같은 다른 정렬 옵션과 함께 작동합니다. 숫자 비교 동작을 유지하면서 대소문자와 악센트를 처리하는 방법을 제어할 수 있습니다.
const collator = new Intl.Collator('en-US', {
numeric: true,
sensitivity: 'base'
});
const items = ['Item1', 'item10', 'ITEM2'];
items.sort(collator.compare);
console.log(items);
// 출력: ['Item1', 'ITEM2', 'item10']
sensitivity: 'base' 옵션은 비교를 대소문자 구분 없이 만듭니다. 정렬기는 Item1, item1, ITEM1을 동등하게 취급하면서도 숫자 부분을 올바르게 비교합니다.
성능을 위한 정렬기 재사용
새로운 Intl.Collator 인스턴스를 생성하는 것은 로케일 데이터를 로드하고 옵션을 처리하는 작업을 포함합니다. 여러 배열을 정렬하거나 많은 비교를 수행해야 할 때는 정렬기를 한 번 생성하고 재사용하세요.
const collator = new Intl.Collator('en-US', { numeric: true });
const files = ['file1.txt', 'file10.txt', 'file2.txt'];
const versions = ['1.2.10', '1.2.2', '1.10.5'];
const products = ['PROD-1', 'PROD-10', 'PROD-2'];
files.sort(collator.compare);
versions.sort(collator.compare);
products.sort(collator.compare);
이 접근 방식은 각 정렬 작업마다 새 정렬기를 생성하는 것보다 더 효율적입니다. 많은 배열을 정렬하거나 빈번한 비교를 수행할 때 성능 차이가 크게 나타납니다.