JavaScript에서 로케일별로 문자열을 알파벳순으로 정렬하는 방법
Intl.Collator와 localeCompare()를 사용하여 모든 언어에 맞게 문자열을 올바르게 정렬하세요
소개
JavaScript에서 문자열 배열을 정렬할 때, 기본 동작은 UTF-16 코드 단위 값으로 문자열을 비교합니다. 이는 기본 ASCII 텍스트에는 잘 작동하지만, 이름, 제품 제목 또는 악센트 문자, 비 라틴 스크립트 또는 대소문자가 혼합된 텍스트를 정렬할 때는 실패합니다.
언어마다 알파벳 순서에 대한 규칙이 다릅니다. 스웨덴어는 å, ä, ö를 z 뒤 알파벳 끝에 배치합니다. 독일어는 대부분의 상황에서 ä를 a와 동등하게 취급합니다. 프랑스어는 특정 비교 모드에서 악센트를 무시합니다. 이러한 언어적 규칙은 사람들이 자신의 언어로 정렬된 목록을 어떻게 보기를 기대하는지 결정합니다.
JavaScript는 로케일을 인식하는 문자열 정렬을 위한 두 가지 API를 제공합니다. String.prototype.localeCompare() 메서드는 간단한 비교를 처리합니다. Intl.Collator API는 대규모 배열을 정렬할 때 더 나은 성능을 제공합니다. 이 강의에서는 두 가지 방법이 어떻게 작동하는지, 언제 각각을 사용해야 하는지, 그리고 다양한 언어에 대한 정렬 동작을 어떻게 구성하는지 설명합니다.
국제 텍스트에 대해 기본 정렬이 실패하는 이유
기본 Array.sort() 메서드는 UTF-16 코드 단위 값으로 문자열을 비교합니다. 이는 대문자가 항상 소문자보다 먼저 오고, 악센트 문자는 z 뒤에 정렬된다는 것을 의미합니다.
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort();
console.log(sorted);
// 출력: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']
이 출력은 스웨덴어에서는 잘못된 것입니다. 스웨덴어에서 å, ä, ö는 알파벳 끝에 속하는 별도의 문자입니다. 올바른 순서는 Anna, 그 다음 Bengt, 그 다음 Åsa, Ärla, Örjan 순이어야 합니다.
이 문제는 기본 정렬이 언어적 의미가 아닌 코드 포인트 값을 비교하기 때문에 발생합니다. 문자 Å는 코드 포인트 U+00C5를 가지며, 이는 z의 코드 포인트(U+007A)보다 큽니다. JavaScript는 스웨덴어 사용자들이 Å를 알파벳에서 특정 위치를 가진 별도의 문자로 간주한다는 것을 알 수 없습니다.
대소문자 혼합은 또 다른 문제를 일으킵니다.
const words = ['zebra', 'Apple', 'banana', 'Zoo'];
const sorted = words.sort();
console.log(sorted);
// 출력: ['Apple', 'Zoo', 'banana', 'zebra']
모든 대문자는 소문자보다 낮은 코드 포인트 값을 가집니다. 이로 인해 Apple과 Zoo가 banana 앞에 나타나게 되는데, 이는 어떤 언어에서도 알파벳 순서가 아닙니다.
localeCompare가 언어적 규칙에 따라 문자열을 정렬하는 방법
localeCompare() 메서드는 특정 로케일의 정렬 순서 규칙에 따라 두 문자열을 비교합니다. 첫 번째 문자열이 두 번째 문자열보다 앞에 오면 음수를, 동등하면 0을, 첫 번째 문자열이 두 번째 문자열 뒤에 오면 양수를 반환합니다.
const result = 'a'.localeCompare('b', 'en-US');
console.log(result);
// 출력: -1 (음수는 'a'가 'b'보다 앞에 온다는 의미)
localeCompare()를 비교 함수로 전달하여 Array.sort()와 함께 직접 사용할 수 있습니다.
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(sorted);
// 출력: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']
스웨덴 로케일은 Anna와 Bengt를 먼저 배치합니다. 이는 표준 라틴 문자를 사용하기 때문입니다. 그 다음에는 특수 스웨덴 문자가 있는 Åsa, Ärla, Örjan이 마지막에 옵니다.
동일한 목록을 독일 로케일로 정렬하면 다른 결과가 나옵니다.
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(sorted);
// 출력: ['Anna', 'Ärla', 'Åsa', 'Bengt', 'Örjan']
독일어는 정렬 목적으로 ä를 a와 동등하게 취급합니다. 이로 인해 Ärla가 스웨덴어처럼 끝이 아닌 Anna 바로 다음에 위치합니다.
localeCompare를 사용해야 하는 경우
작은 배열을 정렬하거나 두 문자열을 비교해야 할 때 localeCompare()를 사용하세요. 콜레이터 객체를 생성하고 관리할 필요 없이 간단한 API를 제공합니다.
const items = ['Banana', 'apple', 'Cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
console.log(sorted);
// 출력: ['apple', 'Banana', 'Cherry']
이 접근 방식은 몇 십 개의 항목이 있는 배열에 적합합니다. 작은 데이터셋에서는 성능 영향이 무시할 수 있을 정도입니다.
전체 배열을 정렬하지 않고도 한 문자열이 다른 문자열보다 앞에 오는지 확인하기 위해 localeCompare()를 사용할 수도 있습니다.
const firstName = 'Åsa';
const secondName = 'Anna';
if (firstName.localeCompare(secondName, 'sv-SE') < 0) {
console.log(`${firstName} comes before ${secondName}`);
} else {
console.log(`${secondName} comes before ${firstName}`);
}
// 출력: "Anna comes before Åsa"
이 비교는 전체 배열을 정렬할 필요 없이 스웨덴어 알파벳 순서를 준수합니다.
Intl.Collator가 성능을 향상시키는 방법
'Intl.Collator' API는 반복 사용에 최적화된 재사용 가능한 비교 함수를 생성합니다. 대규모 배열을 정렬하거나 많은 비교를 수행할 때, 콜레이터는 각 비교마다 'localeCompare()'를 호출하는 것보다 훨씬 빠릅니다.
const collator = new Intl.Collator('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort(collator.compare);
console.log(sorted);
// Output: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']
'collator.compare' 속성은 'Array.sort()'와 직접 작동하는 비교 함수를 반환합니다. 화살표 함수로 래핑할 필요가 없습니다.
콜레이터를 한 번 생성하고 여러 작업에 재사용하면 모든 비교마다 로케일 데이터를 조회하는 오버헤드를 방지할 수 있습니다.
const collator = new Intl.Collator('de-DE');
const germanCities = ['München', 'Berlin', 'Köln', 'Hamburg'];
const sortedCities = germanCities.sort(collator.compare);
const germanNames = ['Müller', 'Schmidt', 'Schröder', 'Fischer'];
const sortedNames = germanNames.sort(collator.compare);
console.log(sortedCities);
// Output: ['Berlin', 'Hamburg', 'Köln', 'München']
console.log(sortedNames);
// Output: ['Fischer', 'Müller', 'Schmidt', 'Schröder']
동일한 콜레이터가 새 인스턴스를 생성할 필요 없이 두 배열을 모두 처리합니다.
Intl.Collator를 사용해야 하는 경우
수백 또는 수천 개의 항목이 있는 배열을 정렬할 때 'Intl.Collator'를 사용하세요. 정렬 중에 비교 함수가 여러 번 호출되기 때문에 배열 크기가 커질수록 성능 이점이 증가합니다.
const collator = new Intl.Collator('en-US');
const products = [/* 10,000개의 제품 이름이 있는 배열 */];
const sorted = products.sort(collator.compare);
수백 개 이상의 항목이 있는 배열의 경우, 콜레이터는 'localeCompare()'보다 몇 배 더 빠를 수 있습니다.
또한 동일한 로케일과 옵션으로 여러 배열을 정렬해야 할 때 'Intl.Collator'를 사용하세요. 콜레이터를 한 번 생성하고 재사용하면 반복적인 로케일 데이터 조회가 제거됩니다.
const collator = new Intl.Collator('fr-FR');
const firstNames = ['Amélie', 'Bernard', 'Émilie', 'François'];
const lastNames = ['Dubois', 'Martin', 'Lefèvre', 'Bernard'];
const sortedFirstNames = firstNames.sort(collator.compare);
const sortedLastNames = lastNames.sort(collator.compare);
이 패턴은 테이블 뷰나 여러 정렬된 목록을 표시하는 다른 인터페이스를 구축할 때 잘 작동합니다.
로케일 지정 방법
localeCompare()와 Intl.Collator 모두 첫 번째 인수로 로케일 식별자를 받습니다. 이 식별자는 BCP 47 형식을 사용하며, 일반적으로 언어 코드와 선택적 지역 코드를 결합합니다.
const names = ['Åsa', 'Anna', 'Ärla'];
// 스웨덴 로케일
const swedishSorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(swedishSorted);
// 출력: ['Anna', 'Åsa', 'Ärla']
// 독일 로케일
const germanSorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(germanSorted);
// 출력: ['Anna', 'Ärla', 'Åsa']
로케일은 어떤 정렬 규칙이 적용될지 결정합니다. 스웨덴어와 독일어는 å와 ä에 대한 규칙이 다르므로 서로 다른 정렬 순서가 생성됩니다.
브라우저에서 사용자의 기본 로케일을 사용하려면 로케일을 생략할 수 있습니다.
const collator = new Intl.Collator();
const names = ['Åsa', 'Anna', 'Ärla'];
const sorted = names.sort(collator.compare);
이 접근 방식은 특정 로케일을 하드코딩하지 않고 사용자의 언어 환경설정을 존중합니다. 정렬 순서는 사용자의 브라우저 설정에 따라 사용자가 예상하는 것과 일치합니다.
대체 옵션을 제공하기 위해 로케일 배열을 전달할 수도 있습니다.
const collator = new Intl.Collator(['sv-SE', 'sv', 'en-US']);
API는 배열에서 지원되는 첫 번째 로케일을 사용합니다. 스웨덴의 스웨덴어를 사용할 수 없는 경우, 일반 스웨덴어를 시도한 다음 미국 영어로 대체합니다.
대소문자 구분 제어 방법
sensitivity 옵션은 비교 시 대소문자와 악센트 차이를 처리하는 방법을 결정합니다. 이 옵션은 base, accent, case, variant 네 가지 값을 허용합니다.
base 감도는 대소문자와 악센트를 모두 무시하고 기본 문자만 비교합니다.
const collator = new Intl.Collator('en-US', { sensitivity: 'base' });
console.log(collator.compare('a', 'A'));
// 출력: 0 (동일)
console.log(collator.compare('a', 'á'));
// 출력: 0 (동일)
console.log(collator.compare('a', 'b'));
// 출력: -1 (다른 기본 문자)
이 모드는 a, A, á를 동일한 기본 문자를 공유하기 때문에 동일하게 취급합니다.
accent 감도는 악센트를 고려하지만 대소문자는 무시합니다.
const collator = new Intl.Collator('en-US', { sensitivity: 'accent' });
console.log(collator.compare('a', 'A'));
// 출력: 0 (동일, 대소문자 무시)
console.log(collator.compare('a', 'á'));
// 출력: -1 (다름, 악센트 중요)
case 감도는 대소문자를 고려하지만 악센트는 무시합니다.
const collator = new Intl.Collator('en-US', { sensitivity: 'case' });
console.log(collator.compare('a', 'A'));
// 출력: -1 (다름, 대소문자 중요)
console.log(collator.compare('a', 'á'));
// 출력: 0 (동일, 악센트 무시)
variant 감도(기본값)는 모든 차이를 고려합니다.
const collator = new Intl.Collator('en-US', { sensitivity: 'variant' });
console.log(collator.compare('a', 'A'));
// 출력: -1 (다름)
console.log(collator.compare('a', 'á'));
// 출력: -1 (다름)
이 모드는 가장 엄격한 비교를 제공하며, 모든 차이를 중요하게 취급합니다.
내장된 숫자가 있는 문자열을 정렬하는 방법
숫자가 포함된 문자열에 대해 숫자 정렬을 활성화하는 numeric 옵션이 있습니다. 이 옵션이 활성화되면, 비교 시 숫자 시퀀스를 문자별로 비교하는 대신 숫자 값으로 처리합니다.
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];
// 기본 정렬 (잘못된 순서)
const defaultSorted = [...files].sort();
console.log(defaultSorted);
// 출력: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']
// 숫자 정렬 (올바른 순서)
const collator = new Intl.Collator('en-US', { numeric: true });
const numericSorted = files.sort(collator.compare);
console.log(numericSorted);
// 출력: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']
숫자 정렬 없이는 문자열이 문자별로 정렬됩니다. 문자열 10은 첫 번째 문자 1이 2보다 코드 포인트가 낮기 때문에 2 앞에 옵니다.
숫자 정렬이 활성화되면, 콜레이터는 10을 숫자 10으로, 2를 숫자 2로 인식합니다. 이로 인해 2가 10 앞에 오는 예상된 정렬 순서가 생성됩니다.
이 옵션은 파일 이름, 버전 번호 또는 텍스트와 숫자가 혼합된 모든 문자열을 정렬할 때 유용합니다.
const versions = ['v1.10', 'v1.2', 'v1.20', 'v1.3'];
const collator = new Intl.Collator('en-US', { numeric: true });
const sorted = versions.sort(collator.compare);
console.log(sorted);
// 출력: ['v1.2', 'v1.3', 'v1.10', 'v1.20']
대소문자 우선 순위 제어 방법
대소문자만 다른 문자열을 비교할 때 대문자나 소문자 중 어느 것이 먼저 정렬될지 결정하는 caseFirst 옵션이 있습니다. 이 옵션은 upper, lower, 또는 false 세 가지 값을 허용합니다.
const words = ['apple', 'Apple', 'APPLE'];
// 대문자 우선
const upperFirst = new Intl.Collator('en-US', { caseFirst: 'upper' });
const upperSorted = [...words].sort(upperFirst.compare);
console.log(upperSorted);
// 출력: ['APPLE', 'Apple', 'apple']
// 소문자 우선
const lowerFirst = new Intl.Collator('en-US', { caseFirst: 'lower' });
const lowerSorted = [...words].sort(lowerFirst.compare);
console.log(lowerSorted);
// 출력: ['apple', 'Apple', 'APPLE']
// 기본값 (로케일에 따라 다름)
const defaultCase = new Intl.Collator('en-US', { caseFirst: 'false' });
const defaultSorted = [...words].sort(defaultCase.compare);
console.log(defaultSorted);
// 출력은 로케일에 따라 다름
false 값은 로케일의 기본 대소문자 순서를 사용합니다. 대부분의 로케일은 기본 민감도 설정을 사용할 때 대소문자만 다른 문자열을 동일하게 취급합니다.
이 옵션은 sensitivity 옵션이 대소문자 차이를 중요하게 처리하도록 허용할 때만 효과가 있습니다.
정렬 시 구두점을 무시하는 방법
ignorePunctuation 옵션은 문자열을 비교할 때 구두점을 건너뛰도록 콜레이터에 지시합니다. 이는 구두점이 포함되거나 포함되지 않을 수 있는 제목이나 문구를 정렬할 때 유용할 수 있습니다.
const titles = [
'The Old Man',
'The Old-Man',
'The Oldman',
];
// 기본값 (구두점이 중요함)
const defaultCollator = new Intl.Collator('en-US');
const defaultSorted = [...titles].sort(defaultCollator.compare);
console.log(defaultSorted);
// 출력: ['The Old Man', 'The Old-Man', 'The Oldman']
// 구두점 무시
const noPunctCollator = new Intl.Collator('en-US', { ignorePunctuation: true });
const noPunctSorted = [...titles].sort(noPunctCollator.compare);
console.log(noPunctSorted);
// 출력: ['The Old Man', 'The Old-Man', 'The Oldman']
구두점을 무시할 때, 비교는 "Old-Man"의 하이픈이 존재하지 않는 것처럼 취급하여 모든 문자열이 "TheOldMan"인 것처럼 비교합니다.
다양한 국가의 사용자 이름 정렬하기
전 세계 사용자의 이름을 정렬할 때는 사용자가 선호하는 로케일을 사용하여 언어적 기대를 존중하세요.
const userLocale = navigator.language;
const collator = new Intl.Collator(userLocale);
const users = [
{ name: 'Müller', country: 'Germany' },
{ name: 'Martin', country: 'France' },
{ name: 'Andersson', country: 'Sweden' },
{ name: 'García', country: 'Spain' },
];
const sorted = users.sort((a, b) => collator.compare(a.name, b.name));
sorted.forEach(user => {
console.log(`${user.name} (${user.country})`);
});
이 코드는 브라우저에서 사용자의 로케일을 감지하고 그에 따라 이름을 정렬합니다. 독일 사용자는 독일 규칙에 따라 정렬된 목록을 보고, 스웨덴 사용자는 스웨덴 규칙에 따라 정렬된 목록을 봅니다.
로케일 전환을 통한 정렬
애플리케이션에서 사용자가 언어를 전환할 수 있는 경우, 로케일이 변경될 때 콜레이터를 업데이트하세요.
let currentLocale = 'en-US';
let collator = new Intl.Collator(currentLocale);
function setLocale(newLocale) {
currentLocale = newLocale;
collator = new Intl.Collator(currentLocale);
}
function sortItems(items) {
return items.sort(collator.compare);
}
// 사용자가 스웨덴어로 전환
setLocale('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan'];
console.log(sortItems(names));
// 출력: ['Anna', 'Åsa', 'Örjan']
// 사용자가 독일어로 전환
setLocale('de-DE');
const germanNames = ['Über', 'Uhr', 'Udo'];
console.log(sortItems(germanNames));
// 출력: ['Udo', 'Uhr', 'Über']
이 패턴은 정렬된 목록이 사용자가 선택한 언어에 맞게 업데이트되도록 보장합니다.
localeCompare와 Intl.Collator 중 선택하기
localeCompare()는 빠른 일회성 비교가 필요하거나 100개 미만의 항목이 있는 작은 배열을 정렬할 때 사용하세요. 더 간단한 구문은 읽기 쉽고 작은 데이터셋에서는 성능 차이가 무시할 만합니다.
const items = ['banana', 'Apple', 'cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
Intl.Collator는 대규모 배열을 정렬하거나, 많은 비교를 수행하거나, 동일한 로케일과 옵션으로 여러 배열을 정렬할 때 사용하세요. 콜레이터를 한 번 생성하고 재사용하면 더 나은 성능을 제공합니다.
const collator = new Intl.Collator('en-US', { sensitivity: 'base', numeric: true });
const products = [/* 대규모 배열 */];
const sorted = products.sort(collator.compare);
두 접근 방식 모두 동일한 결과를 생성합니다. 선택은 성능 요구 사항과 코드 구성 선호도에 따라 달라집니다.