악센트 기호를 무시하고 문자열 비교하기

JavaScript 정규화 및 Intl.Collator를 사용하여 분음 부호를 무시하고 문자열을 비교하는 방법 알아보기

소개

여러 언어를 지원하는 애플리케이션을 구축할 때 악센트 기호가 포함된 문자열을 비교해야 하는 경우가 많습니다. "cafe"를 검색하는 사용자는 "café"에 대한 결과를 찾아야 합니다. "Jose"에 대한 사용자 이름 확인은 "José"와 일치해야 합니다. 표준 문자열 비교는 이들을 서로 다른 문자열로 취급하지만, 애플리케이션 로직에서는 이들을 동일하게 처리해야 합니다.

JavaScript는 이 문제를 해결하기 위한 두 가지 접근 방식을 제공합니다. 문자열을 정규화하고 악센트 기호를 제거하거나, 내장 콜레이션 API를 사용하여 특정 민감도 규칙으로 문자열을 비교할 수 있습니다.

악센트 기호란

악센트 기호는 발음이나 의미를 수정하기 위해 문자 위, 아래 또는 관통하여 배치되는 기호입니다. 이러한 기호를 분음 부호라고 합니다. 일반적인 예로는 "é"의 예각 악센트, "ñ"의 틸데, "ü"의 움라우트가 있습니다.

유니코드에서 이러한 문자는 두 가지 방식으로 표현될 수 있습니다. 단일 코드 포인트가 완전한 문자를 나타낼 수 있거나, 여러 코드 포인트가 기본 문자와 별도의 악센트 기호를 결합할 수 있습니다. 문자 "é"는 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 객체는 언어별 규칙에 따라 문자열을 비교합니다. sensitivity 옵션은 문자열을 비교할 때 어떤 차이가 중요한지 제어합니다.

"base" 민감도 수준은 악센트 기호와 대소문자 차이를 모두 무시합니다. 악센트나 대문자 표기만 다른 문자열은 동일한 것으로 간주됩니다.

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

console.log(collator.compare('café', 'cafe')); // 0 (equal)
console.log(collator.compare('Café', 'cafe')); // 0 (equal)
console.log(collator.compare('café', 'caff')); // -1 (first comes before second)

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); // Groups variants together

Intl.Collator API는 다양한 사용 사례를 위한 다른 민감도 수준을 제공합니다.

"accent" 수준은 대소문자를 무시하지만 악센트 차이는 구분합니다. "Café"는 "café"와 같지만 "cafe"와는 다릅니다.

const accentCollator = new Intl.Collator('en', { sensitivity: 'accent' });
console.log(accentCollator.compare('Café', 'café')); // 0 (equal)
console.log(accentCollator.compare('café', 'cafe')); // 1 (not equal)

"case" 수준은 악센트를 무시하지만 대소문자 차이는 구분합니다. "café"는 "cafe"와 같지만 "Café"와는 다릅니다.

const caseCollator = new Intl.Collator('en', { sensitivity: 'case' });
console.log(caseCollator.compare('café', 'cafe')); // 0 (equal)
console.log(caseCollator.compare('café', 'Café')); // -1 (not equal)

"variant" 수준은 모든 차이를 구분합니다. 이것이 기본 동작입니다.

const variantCollator = new Intl.Collator('en', { sensitivity: 'variant' });
console.log(variantCollator.compare('café', 'cafe')); // 1 (not equal)

정규화와 콜레이션 중 선택하기

두 방법 모두 악센트를 구분하지 않는 비교에서 올바른 결과를 생성하지만, 서로 다른 특성을 가지고 있습니다.

정규화 방법은 악센트 기호가 없는 새로운 문자열을 생성합니다. 정규화된 버전을 저장하거나 인덱싱해야 할 때 이 접근 방식을 사용하세요. 검색 엔진과 데이터베이스는 효율적인 조회를 위해 정규화된 텍스트를 저장하는 경우가 많습니다.

Intl.Collator 메서드는 문자열을 수정하지 않고 비교합니다. 중복 확인이나 목록 정렬과 같이 문자열을 직접 비교해야 할 때 이 접근 방식을 사용하세요. 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'));
// Returns both Café Latte and 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'));
// Returns 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 환경에서 지원됩니다. Internet Explorer는 이 메서드를 지원하지 않습니다.

Intl.Collator API는 모든 최신 브라우저와 Node.js 버전에서 지원됩니다. Internet Explorer 11은 부분적으로 지원합니다.

두 접근 방식 모두 현재 JavaScript 환경에서 안정적으로 작동합니다. 구형 브라우저를 지원해야 하는 경우 폴리필이나 대체 구현이 필요합니다.

악센트 제거의 한계

일부 언어는 단순한 악센트 변형이 아닌 별개의 문자를 만들기 위해 분음 부호를 사용합니다. 터키어에서 "i"와 "ı"는 서로 다른 문자입니다. 독일어에서 "ö"는 악센트가 붙은 "o"가 아닌 별개의 모음입니다.

이러한 경우 악센트를 제거하면 의미가 변경됩니다. 사용 사례와 대상 언어에 대해 악센트를 구분하지 않는 비교가 적절한지 고려하십시오.

collation 접근 방식은 로케일별 규칙을 따르기 때문에 이러한 경우를 더 잘 처리합니다. Intl.Collator 생성자에서 올바른 로케일을 지정하면 문화적으로 적절한 비교가 보장됩니다.

const turkishCollator = new Intl.Collator('tr', { sensitivity: 'base' });
const germanCollator = new Intl.Collator('de', { sensitivity: 'base' });

비교 전략을 선택할 때 애플리케이션이 지원하는 언어를 항상 고려하십시오.