악센트 부호를 무시하고 문자열 비교하기
JavaScript 정규화와 Intl.Collator를 사용하여 발음 구별 부호를 무시하면서 문자열을 비교하는 방법 알아보기
소개
여러 언어로 작동하는 애플리케이션을 구축할 때, 종종 악센트 기호가 포함된 문자열을 비교해야 합니다. "cafe"를 검색하는 사용자는 "café"에 대한 결과를 찾을 수 있어야 합니다. "Jose"에 대한 사용자 이름 확인은 "José"와 일치해야 합니다. 표준 문자열 비교는 이들을 서로 다른 문자열로 취급하지만, 애플리케이션 로직은 이들을 동일하게 취급해야 합니다.
자바스크립트는 이 문제를 해결하기 위한 두 가지 접근 방식을 제공합니다. 문자열을 정규화하고 악센트 기호를 제거하거나, 내장된 콜레이션 API를 사용하여 특정 민감도 규칙으로 문자열을 비교할 수 있습니다.
악센트 기호란 무엇인가
악센트 기호는 글자 위, 아래 또는 글자를 관통하여 배치되어 발음이나 의미를 수정하는 기호입니다. 이러한 기호를 발음 구별 기호(diacritics)라고 합니다. 일반적인 예로는 "é"의 급음 부호(acute accent), "ñ"의 물결 표시(tilde), "ü"의 움라우트(umlaut)가 있습니다.
유니코드에서 이러한 문자는 두 가지 방식으로 표현될 수 있습니다. 단일 코드 포인트가 완전한 문자를 나타낼 수 있거나, 여러 코드 포인트가 기본 글자와 별도의 악센트 기호를 결합할 수 있습니다. "é" 글자는 U+00E9로 저장되거나 "e"(U+0065)와 결합 급음 부호(U+0301)로 저장될 수 있습니다.
비교에서 악센트 기호를 무시해야 하는 경우
검색 기능은 악센트에 민감하지 않은 비교의 가장 일반적인 사용 사례입니다. 악센트 기호 없이 쿼리를 입력하는 사용자는 악센트 기호가 있는 문자가 포함된 콘텐츠를 찾을 것으로 예상합니다. "Muller"에 대한 검색은 "Müller"를 찾아야 합니다.
사용자 입력 유효성 검사는 사용자 이름, 이메일 주소 또는 기타 식별자가 이미 존재하는지 확인할 때 이 기능이 필요합니다. "maria"와 "maría"에 대한 중복 계정을 방지하고자 합니다.
대소문자를 구분하지 않는 비교는 종종 동시에 악센트도 무시해야 합니다. 대소문자에 관계없이 두 문자열이 일치하는지 확인할 때, 일반적으로 악센트 차이도 무시하고자 합니다.
정규화를 사용하여 악센트 부호 제거하기
첫 번째 접근 방식은 문자열을 기본 문자와 악센트 부호가 분리된 정규화된 형태로 변환한 다음, 악센트 부호를 제거하는 것입니다.
유니코드 정규화는 문자열을 표준 형식으로 변환합니다. NFD(정준 분해) 형식은 결합된 문자를 기본 문자와 결합 표시로 분리합니다. "café" 문자열은 "cafe"와 그 뒤에 결합 악센트 문자로 변환됩니다.
정규화 후에는 정규 표현식을 사용하여 결합 표시를 제거할 수 있습니다. 유니코드 범위 U+0300에서 U+036F는 결합 발음 구별 기호를 포함합니다.
function removeAccents(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
const text1 = 'café';
const text2 = 'cafe';
const normalized1 = removeAccents(text1);
const normalized2 = removeAccents(text2);
console.log(normalized1 === normalized2); // true
console.log(normalized1); // "cafe"
이 방법을 사용하면 표준 동등 연산자를 사용하여 비교할 수 있는 악센트 부호가 없는 문자열을 얻을 수 있습니다.
대소문자를 구분하지 않고 악센트를 구분하지 않는 비교를 위해 이 방법을 소문자 변환과 결합할 수 있습니다.
function normalizeForComparison(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
}
const search = 'muller';
const name = 'Müller';
console.log(normalizeForComparison(search) === normalizeForComparison(name)); // true
이 접근 방식은 효율적인 검색을 위해 문자열의 정규화된 버전을 저장하거나 인덱싱해야 할 때 잘 작동합니다.
Intl.Collator를 사용하여 문자열 비교하기
두 번째 접근 방식은 구성 가능한 민감도 수준으로 로케일 인식 문자열 비교를 제공하는 Intl.Collator API를 사용합니다.
Intl.Collator 객체는 언어별 규칙에 따라 문자열을 비교합니다. 민감도 옵션은 문자열을 비교할 때 어떤 차이가 중요한지 제어합니다.
"base" 민감도 수준은 악센트 부호와 대소문자 차이를 모두 무시합니다. 악센트나 대소문자에서만 다른 문자열은 동일하게 간주됩니다.
const collator = new Intl.Collator('en', { sensitivity: 'base' });
console.log(collator.compare('café', 'cafe')); // 0 (동일)
console.log(collator.compare('Café', 'cafe')); // 0 (동일)
console.log(collator.compare('café', 'caff')); // -1 (첫 번째가 두 번째보다 앞에 옴)
compare 메서드는 문자열이 동일할 때 0을, 첫 번째 문자열이 두 번째보다 앞에 올 때 음수를, 첫 번째 문자열이 두 번째 뒤에 올 때 양수를 반환합니다.
이를 동등성 검사나 배열 정렬에 사용할 수 있습니다.
const collator = new Intl.Collator('en', { sensitivity: 'base' });
function areEqualIgnoringAccents(str1, str2) {
return collator.compare(str1, str2) === 0;
}
console.log(areEqualIgnoringAccents('José', 'Jose')); // true
console.log(areEqualIgnoringAccents('naïve', 'naive')); // true
정렬의 경우, compare 메서드를 직접 Array.sort에 전달할 수 있습니다.
const names = ['Müller', 'Martinez', 'Muller', 'Márquez'];
const collator = new Intl.Collator('en', { sensitivity: 'base' });
names.sort(collator.compare);
console.log(names); // 변형을 함께 그룹화
Intl.Collator API는 다양한 사용 사례에 대한 다른 민감도 수준을 제공합니다.
"accent" 수준은 대소문자를 무시하지만 악센트 차이를 존중합니다. "Café"는 "café"와 같지만 "cafe"와는 다릅니다.
const accentCollator = new Intl.Collator('en', { sensitivity: 'accent' });
console.log(accentCollator.compare('Café', 'café')); // 0 (동일)
console.log(accentCollator.compare('café', 'cafe')); // 1 (동일하지 않음)
"case" 수준은 악센트를 무시하지만 대소문자 차이를 존중합니다. "café"는 "cafe"와 같지만 "Café"와는 다릅니다.
const caseCollator = new Intl.Collator('en', { sensitivity: 'case' });
console.log(caseCollator.compare('café', 'cafe')); // 0 (동일)
console.log(caseCollator.compare('café', 'Café')); // -1 (동일하지 않음)
"variant" 수준은 모든 차이를 존중합니다. 이것이 기본 동작입니다.
const variantCollator = new Intl.Collator('en', { sensitivity: 'variant' });
console.log(variantCollator.compare('café', 'cafe')); // 1 (동일하지 않음)
정규화와 콜레이션 중 선택하기
두 방법 모두 악센트에 민감하지 않은 비교에 대해 올바른 결과를 생성하지만, 서로 다른 특성을 가지고 있습니다.
정규화 방법은 악센트 표시가 없는 새로운 문자열을 생성합니다. 정규화된 버전을 저장하거나 인덱싱해야 할 때 이 접근 방식을 사용하세요. 검색 엔진과 데이터베이스는 효율적인 조회를 위해 정규화된 텍스트를 저장하는 경우가 많습니다.
Intl.Collator 방법은 문자열을 수정하지 않고 비교합니다. 중복 확인이나 목록 정렬과 같이 문자열을 직접 비교해야 할 때 이 접근 방식을 사용하세요. 콜레이터는 단순 문자열 비교로는 처리할 수 없는 언어별 정렬 규칙을 준수합니다.
성능 고려 사항은 사용 사례에 따라 다릅니다. 콜레이터 객체를 한 번 생성하고 재사용하는 것은 여러 비교에 효율적입니다. 문자열을 한 번 정규화하고 여러 번 비교할 때는 정규화 방법이 효율적입니다.
정규화 방법은 악센트 정보를 영구적으로 제거합니다. 콜레이션 방법은 지정한 규칙에 따라 비교하면서 원래 문자열을 보존합니다.
악센트에 민감하지 않은 검색을 사용하여 배열 필터링하기
일반적인 사용 사례는 악센트 차이를 무시하고 사용자 입력을 기반으로 항목 배열을 필터링하는 것입니다.
const products = [
{ name: 'Café Latte', price: 4.50 },
{ name: 'Crème Brûlée', price: 6.00 },
{ name: 'Croissant', price: 3.00 },
{ name: 'Café Mocha', price: 5.00 }
];
function searchProducts(query) {
const collator = new Intl.Collator('en', { sensitivity: 'base' });
return products.filter(product => {
return collator.compare(product.name.slice(0, query.length), query) === 0;
});
}
console.log(searchProducts('cafe'));
// Café Latte와 Café Mocha 모두 반환
부분 문자열 매칭의 경우, 정규화 접근 방식이 더 잘 작동합니다.
function removeAccents(str) {
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
function searchProducts(query) {
const normalizedQuery = removeAccents(query.toLowerCase());
return products.filter(product => {
const normalizedName = removeAccents(product.name.toLowerCase());
return normalizedName.includes(normalizedQuery);
});
}
console.log(searchProducts('creme'));
// Crème Brûlée 반환
이 접근 방식은 정규화된 제품 이름에 정규화된 검색 쿼리가 부분 문자열로 포함되어 있는지 확인합니다.
텍스트 입력 일치 처리
사용자 입력을 기존 데이터와 비교하여 검증할 때, 혼란과 중복을 방지하기 위해 악센트에 민감하지 않은 비교가 필요합니다.
const existingUsernames = ['José', 'María', 'François'];
function isUsernameTaken(username) {
const collator = new Intl.Collator('en', { sensitivity: 'base' });
return existingUsernames.some(existing =>
collator.compare(existing, username) === 0
);
}
console.log(isUsernameTaken('jose')); // true
console.log(isUsernameTaken('Maria')); // true
console.log(isUsernameTaken('francois')); // true
console.log(isUsernameTaken('pierre')); // false
이는 사용자가 기존 계정과 악센트나 대소문자만 다른 이름으로 계정을 만드는 것을 방지합니다.
브라우저 및 환경 지원
String.prototype.normalize 메서드는 모든 최신 브라우저와 Node.js 환경에서 지원됩니다. 인터넷 익스플로러는 이 메서드를 지원하지 않습니다.
Intl.Collator API는 모든 최신 브라우저와 Node.js 버전에서 지원됩니다. 인터넷 익스플로러 11은 부분적으로 지원합니다.
두 접근 방식 모두 현재 JavaScript 환경에서 안정적으로 작동합니다. 오래된 브라우저를 지원해야 하는 경우 폴리필이나 대체 구현이 필요합니다.
악센트 제거의 한계
일부 언어에서는 발음 구별 부호를 단순한 악센트 변형이 아닌 고유한 문자를 만드는 데 사용합니다. 터키어에서 "i"와 "ı"는 서로 다른 문자입니다. 독일어에서 "ö"는 "o"에 악센트를 붙인 것이 아닌 별개의 모음입니다.
이러한 경우에 악센트를 제거하면 의미가 변경됩니다. 사용 사례와 대상 언어에 따라 악센트에 민감하지 않은 비교가 적절한지 고려하세요.
콜레이션 접근 방식은 지역별 규칙을 따르기 때문에 이러한 경우를 더 잘 처리합니다. Intl.Collator 생성자에 올바른 로케일을 지정하면 문화적으로 적절한 비교가 보장됩니다.
const turkishCollator = new Intl.Collator('tr', { sensitivity: 'base' });
const germanCollator = new Intl.Collator('de', { sensitivity: 'base' });
비교 전략을 선택할 때는 항상 애플리케이션이 지원하는 언어를 고려하세요.