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);
// Output: ['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);
// Output: ['Apple', 'Zoo', 'banana', 'zebra']

모든 대문자는 소문자보다 낮은 코드 포인트 값을 가집니다. 이로 인해 Apple과 Zoo가 banana보다 먼저 나타나는데, 이는 어떤 언어에서도 알파벳 순서가 아닙니다.

localeCompare가 언어 규칙에 따라 문자열을 정렬하는 방법

localeCompare() 메서드는 특정 로케일의 정렬 순서 규칙에 따라 두 문자열을 비교합니다. 첫 번째 문자열이 두 번째 문자열보다 앞에 오면 음수를, 동등하면 0을, 첫 번째 문자열이 두 번째 문자열보다 뒤에 오면 양수를 반환합니다.

const result = 'a'.localeCompare('b', 'en-US');
console.log(result);
// Output: -1 (negative means 'a' comes before '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);
// Output: ['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);
// Output: ['Anna', 'Ärla', 'Åsa', 'Bengt', 'Örjan']

독일어는 정렬 목적상 ä를 a와 동등하게 취급합니다. 이로 인해 Ärla가 스웨덴어처럼 끝이 아닌 Anna 바로 다음에 배치됩니다.

localeCompare를 사용해야 하는 경우

작은 배열을 정렬하거나 두 문자열을 비교해야 할 때는 localeCompare()를 사용하세요. collator 객체를 생성하고 관리할 필요 없이 간단한 API를 제공합니다.

const items = ['Banana', 'apple', 'Cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
console.log(sorted);
// Output: ['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}`);
}
// Output: "Anna comes before Åsa"

이 비교는 전체 배열을 정렬할 필요 없이 스웨덴어 알파벳 순서를 준수합니다.

Intl.Collator가 성능을 개선하는 방법

Intl.Collator API는 반복 사용에 최적화된 재사용 가능한 비교 함수를 생성합니다. 대용량 배열을 정렬하거나 많은 비교를 수행할 때, collator는 각 비교마다 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()와 직접 작동하는 비교 함수를 반환합니다. 화살표 함수로 래핑할 필요가 없습니다.

collator를 한 번 생성하고 여러 작업에 재사용하면 모든 비교마다 로케일 데이터를 조회하는 오버헤드를 피할 수 있습니다.

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']

동일한 collator가 새 인스턴스를 생성할 필요 없이 두 배열을 모두 처리합니다.

Intl.Collator를 사용해야 하는 경우

수백 또는 수천 개의 항목이 있는 배열을 정렬할 때는 Intl.Collator를 사용하세요. 정렬 중에 비교 함수가 여러 번 호출되기 때문에 배열 크기가 클수록 성능 이점이 증가합니다.

const collator = new Intl.Collator('en-US');
const products = [/* array with 10,000 product names */];
const sorted = products.sort(collator.compare);

수백 개 이상의 항목이 있는 배열의 경우, collator는 localeCompare()보다 몇 배 더 빠를 수 있습니다.

동일한 로케일과 옵션으로 여러 배열을 정렬해야 할 때도 Intl.Collator를 사용하세요. 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'];

// Swedish locale
const swedishSorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(swedishSorted);
// Output: ['Anna', 'Åsa', 'Ärla']

// German locale
const germanSorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(germanSorted);
// Output: ['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 sensitivity는 대소문자와 악센트를 모두 무시하고 기본 문자만 비교합니다.

const collator = new Intl.Collator('en-US', { sensitivity: 'base' });

console.log(collator.compare('a', 'A'));
// Output: 0 (equal)

console.log(collator.compare('a', 'á'));
// Output: 0 (equal)

console.log(collator.compare('a', 'b'));
// Output: -1 (different base characters)

이 모드는 a, A, á를 동일한 기본 문자를 공유하므로 동일하게 취급합니다.

accent sensitivity는 악센트를 고려하지만 대소문자는 무시합니다.

const collator = new Intl.Collator('en-US', { sensitivity: 'accent' });

console.log(collator.compare('a', 'A'));
// Output: 0 (equal, case ignored)

console.log(collator.compare('a', 'á'));
// Output: -1 (different, accent matters)

case sensitivity는 대소문자를 고려하지만 악센트는 무시합니다.

const collator = new Intl.Collator('en-US', { sensitivity: 'case' });

console.log(collator.compare('a', 'A'));
// Output: -1 (different, case matters)

console.log(collator.compare('a', 'á'));
// Output: 0 (equal, accent ignored)

variant sensitivity(기본값)는 모든 차이를 고려합니다.

const collator = new Intl.Collator('en-US', { sensitivity: 'variant' });

console.log(collator.compare('a', 'A'));
// Output: -1 (different)

console.log(collator.compare('a', 'á'));
// Output: -1 (different)

이 모드는 가장 엄격한 비교를 제공하며 모든 차이를 중요하게 취급합니다.

숫자가 포함된 문자열을 정렬하는 방법

numeric 옵션은 숫자가 포함된 문자열에 대해 숫자 정렬을 활성화합니다. 활성화되면 비교는 숫자 시퀀스를 문자별로 비교하는 대신 숫자 값으로 처리합니다.

const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];

// Default sorting (wrong order)
const defaultSorted = [...files].sort();
console.log(defaultSorted);
// Output: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']

// Numeric sorting (correct order)
const collator = new Intl.Collator('en-US', { numeric: true });
const numericSorted = files.sort(collator.compare);
console.log(numericSorted);
// Output: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']

숫자 정렬이 없으면 문자열은 문자 단위로 정렬됩니다. 문자열 102보다 앞에 옵니다. 첫 번째 문자 1의 코드 포인트가 2보다 낮기 때문입니다.

숫자 정렬이 활성화되면 collator는 10를 숫자 10으로, 2를 숫자 2로 인식합니다. 이렇게 하면 210보다 앞에 오는 예상된 정렬 순서가 생성됩니다.

이 옵션은 파일 이름, 버전 번호 또는 텍스트와 숫자가 혼합된 문자열을 정렬할 때 유용합니다.

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);
// Output: ['v1.2', 'v1.3', 'v1.10', 'v1.20']

대소문자 우선순위 제어 방법

caseFirst 옵션은 대소문자만 다른 문자열을 비교할 때 대문자 또는 소문자 중 어느 것이 먼저 정렬될지 결정합니다. 세 가지 값을 허용합니다: upper, lower, 또는 false.

const words = ['apple', 'Apple', 'APPLE'];

// Uppercase first
const upperFirst = new Intl.Collator('en-US', { caseFirst: 'upper' });
const upperSorted = [...words].sort(upperFirst.compare);
console.log(upperSorted);
// Output: ['APPLE', 'Apple', 'apple']

// Lowercase first
const lowerFirst = new Intl.Collator('en-US', { caseFirst: 'lower' });
const lowerSorted = [...words].sort(lowerFirst.compare);
console.log(lowerSorted);
// Output: ['apple', 'Apple', 'APPLE']

// Default (locale-dependent)
const defaultCase = new Intl.Collator('en-US', { caseFirst: 'false' });
const defaultSorted = [...words].sort(defaultCase.compare);
console.log(defaultSorted);
// Output depends on locale

false 값은 로케일의 기본 대소문자 순서를 사용합니다. 대부분의 로케일은 기본 민감도 설정을 사용할 때 대소문자만 다른 문자열을 동일하게 취급합니다.

이 옵션은 sensitivity 옵션이 대소문자 차이를 중요하게 허용할 때만 효과가 있습니다.

정렬 시 구두점 무시 방법

ignorePunctuation 옵션은 문자열을 비교할 때 구두점을 건너뛰도록 collator에 지시합니다. 이는 구두점이 포함되거나 포함되지 않을 수 있는 제목이나 구문을 정렬할 때 유용할 수 있습니다.

const titles = [
  'The Old Man',
  'The Old-Man',
  'The Oldman',
];

// Default (punctuation matters)
const defaultCollator = new Intl.Collator('en-US');
const defaultSorted = [...titles].sort(defaultCollator.compare);
console.log(defaultSorted);
// Output: ['The Old Man', 'The Old-Man', 'The Oldman']

// Ignore punctuation
const noPunctCollator = new Intl.Collator('en-US', { ignorePunctuation: true });
const noPunctSorted = [...titles].sort(noPunctCollator.compare);
console.log(noPunctSorted);
// Output: ['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})`);
});

이 코드는 브라우저에서 사용자의 로케일을 감지하고 그에 따라 이름을 정렬합니다. 독일 사용자는 독일어 규칙으로 정렬된 목록을 보고, 스웨덴 사용자는 스웨덴어 규칙으로 정렬된 목록을 봅니다.

로케일 전환을 통한 정렬

애플리케이션에서 사용자가 언어를 전환할 수 있는 경우 로케일이 변경될 때 collator를 업데이트하세요.

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);
}

// User switches to Swedish
setLocale('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan'];
console.log(sortItems(names));
// Output: ['Anna', 'Åsa', 'Örjan']

// User switches to German
setLocale('de-DE');
const germanNames = ['Über', 'Uhr', 'Udo'];
console.log(sortItems(germanNames));
// Output: ['Udo', 'Uhr', 'Über']

이 패턴은 정렬된 목록이 사용자가 선택한 언어와 일치하도록 업데이트되도록 보장합니다.

localeCompare와 Intl.Collator 중 선택하기

빠른 일회성 비교가 필요하거나 100개 미만의 항목이 있는 작은 배열을 정렬할 때 localeCompare()를 사용하세요. 더 간단한 구문이 읽기 쉽고 작은 데이터셋에서는 성능 차이가 미미합니다.

const items = ['banana', 'Apple', 'cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));

대용량 배열을 정렬하거나, 많은 비교를 수행하거나, 동일한 로케일과 옵션으로 여러 배열을 정렬할 때 Intl.Collator를 사용하세요. collator를 한 번 생성하고 재사용하면 더 나은 성능을 제공합니다.

const collator = new Intl.Collator('en-US', { sensitivity: 'base', numeric: true });

const products = [/* large array */];
const sorted = products.sort(collator.compare);

두 접근 방식 모두 동일한 결과를 생성합니다. 선택은 성능 요구 사항과 코드 구성 선호도에 따라 달라집니다.