대소문자 차이를 무시하고 문자열을 비교하는 방법
로케일 인식 비교를 사용하여 언어 간 대소문자 구분 없는 매칭을 올바르게 처리하세요
소개
대소문자를 구분하지 않는 문자열 비교는 웹 애플리케이션에서 자주 나타납니다. 사용자는 검색 쿼리를 대소문자 혼합으로 입력하거나, 일관되지 않은 대문자 사용으로 사용자 이름을 입력하거나, 문자 대소문자를 고려하지 않고 양식을 작성합니다. 애플리케이션은 사용자가 대문자, 소문자 또는 혼합 대소문자로 입력하는지 여부와 관계없이 이러한 입력을 올바르게 매칭해야 합니다.
간단한 접근 방식은 두 문자열을 모두 소문자로 변환하고 비교하는 것입니다. 이는 영어 텍스트에서는 작동하지만 국제 애플리케이션에서는 실패합니다. 언어마다 대문자와 소문자 간 변환 규칙이 다릅니다. 영어에서 작동하는 비교 방법은 터키어, 독일어, 그리스어 또는 기타 언어에서 잘못된 결과를 생성할 수 있습니다.
JavaScript는 모든 언어에서 대소문자를 구분하지 않는 비교를 올바르게 처리하기 위해 Intl.Collator API를 제공합니다. 이 레슨에서는 단순한 소문자 변환이 실패하는 이유, 로케일 인식 비교가 작동하는 방식, 그리고 각 접근 방식을 언제 사용해야 하는지 설명합니다.
toLowerCase를 사용한 단순한 접근 방식
비교 전에 두 문자열을 모두 소문자로 변환하는 것은 대소문자를 구분하지 않는 매칭에 가장 일반적인 접근 방식입니다:
const str1 = "Hello";
const str2 = "HELLO";
console.log(str1.toLowerCase() === str2.toLowerCase());
// true
이 패턴은 ASCII 텍스트와 영어 단어에서 작동합니다. 비교는 동일한 문자의 대문자 및 소문자 버전을 동일하게 처리합니다.
퍼지 검색에 이 접근 방식을 사용할 수 있습니다:
const query = "apple";
const items = ["Apple", "Banana", "APPLE PIE", "Orange"];
const matches = items.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
console.log(matches);
// ["Apple", "APPLE PIE"]
필터는 대소문자에 관계없이 검색 쿼리를 포함하는 모든 항목을 찾습니다. 이는 대문자 사용을 고려하지 않고 쿼리를 입력하는 사용자에게 예상되는 동작을 제공합니다.
국제 텍스트에서 단순한 접근 방식이 실패하는 이유
toLowerCase() 메서드는 유니코드 규칙에 따라 텍스트를 변환하지만, 이러한 규칙은 모든 언어에서 동일하게 작동하지 않습니다. 가장 유명한 예는 터키어 i 문제입니다.
영어에서는 소문자 i가 대문자 I로 변환됩니다. 터키어에는 두 개의 구별되는 문자가 있습니다:
- 점이 있는 소문자
i는 점이 있는 대문자İ로 변환됩니다 - 점이 없는 소문자
ı는 점이 없는 대문자I로 변환됩니다
이러한 차이로 인해 대소문자를 구분하지 않는 비교가 제대로 작동하지 않습니다:
const word1 = "file";
const word2 = "FILE";
// In English locale (correct)
console.log(word1.toLowerCase() === word2.toLowerCase());
// true
// In Turkish locale (incorrect)
console.log(word1.toLocaleLowerCase("tr") === word2.toLocaleLowerCase("tr"));
// false - "file" becomes "fıle"
터키어 규칙을 사용하여 FILE를 소문자로 변환할 때, I는 ı(점 없음)가 되어 fıle를 생성합니다. 이는 file(점이 있는 i)와 일치하지 않으므로, 문자열이 동일한 단어를 나타내더라도 비교는 false를 반환합니다.
다른 언어들도 유사한 문제가 있습니다. 독일어에는 ß 문자가 있으며 이는 SS로 대문자화됩니다. 그리스어에는 시그마의 여러 소문자 형태(σ와 ς)가 있으며 둘 다 Σ로 대문자화됩니다. 단순한 대소문자 변환으로는 이러한 언어별 규칙을 올바르게 처리할 수 없습니다.
대소문자를 구분하지 않는 비교를 위해 base sensitivity와 함께 Intl.Collator 사용하기
Intl.Collator API는 구성 가능한 민감도를 가진 로케일 인식 문자열 비교를 제공합니다. sensitivity 옵션은 비교 중 어떤 차이가 중요한지 제어합니다.
대소문자를 구분하지 않는 비교의 경우 sensitivity: "base"를 사용하세요:
const collator = new Intl.Collator("en", { sensitivity: "base" });
console.log(collator.compare("Hello", "hello"));
// 0 (strings are equal)
console.log(collator.compare("Hello", "HELLO"));
// 0 (strings are equal)
console.log(collator.compare("Hello", "Héllo"));
// 0 (strings are equal, accents ignored too)
Base sensitivity는 대소문자와 악센트 차이를 모두 무시합니다. 기본 문자만 중요합니다. 이 민감도 수준에서 문자열이 동등할 때 비교는 0을 반환합니다.
이 접근 방식은 터키어 i 문제를 올바르게 처리합니다:
const collator = new Intl.Collator("tr", { sensitivity: "base" });
console.log(collator.compare("file", "FILE"));
// 0 (correctly matches)
console.log(collator.compare("file", "FİLE"));
// 0 (correctly matches, even with dotted İ)
collator는 터키어 대소문자 폴딩 규칙을 자동으로 적용합니다. 두 비교 모두 입력에 어떤 대문자 I가 나타나든 문자열을 동등한 것으로 인식합니다.
sensitivity 옵션과 함께 localeCompare 사용하기
localeCompare() 메서드는 대소문자를 구분하지 않는 비교를 수행하는 대체 방법을 제공합니다. Intl.Collator와 동일한 옵션을 허용합니다:
const str1 = "Hello";
const str2 = "HELLO";
console.log(str1.localeCompare(str2, "en", { sensitivity: "base" }));
// 0 (strings are equal)
이는 base sensitivity와 함께 Intl.Collator를 사용하는 것과 동일한 결과를 생성합니다. 비교는 대소문자 차이를 무시하고 동등한 문자열에 대해 0을 반환합니다.
배열 필터링에서 이를 사용할 수 있습니다:
const query = "apple";
const items = ["Apple", "Banana", "APPLE PIE", "Orange"];
const matches = items.filter(item =>
item.localeCompare(query, "en", { sensitivity: "base" }) === 0 ||
item.toLowerCase().includes(query.toLowerCase())
);
console.log(matches);
// ["Apple"]
그러나 localeCompare()는 지정된 sensitivity 수준에서 정확히 일치하는 경우에만 0을 반환합니다. includes()와 같은 부분 일치를 지원하지 않습니다. 부분 문자열 검색의 경우 여전히 소문자 변환을 사용하거나 더 정교한 검색 알고리즘을 구현해야 합니다.
기본 민감도와 악센트 민감도 중 선택하기
sensitivity 옵션은 문자열 비교의 다양한 측면을 제어하는 네 가지 값을 허용합니다:
기본 민감도
기본 민감도는 대소문자와 악센트를 모두 무시합니다:
const collator = new Intl.Collator("en", { sensitivity: "base" });
console.log(collator.compare("cafe", "café"));
// 0 (accents ignored)
console.log(collator.compare("cafe", "Café"));
// 0 (case and accents ignored)
console.log(collator.compare("cafe", "CAFÉ"));
// 0 (case and accents ignored)
이는 가장 관대한 일치를 제공합니다. 악센트 문자를 입력할 수 없거나 편의상 생략하는 사용자도 올바른 일치 결과를 얻을 수 있습니다.
악센트 민감도
악센트 민감도는 대소문자를 무시하지만 악센트는 고려합니다:
const collator = new Intl.Collator("en", { sensitivity: "accent" });
console.log(collator.compare("cafe", "café"));
// -1 (accents matter)
console.log(collator.compare("cafe", "Café"));
// -1 (accents matter, case ignored)
console.log(collator.compare("Café", "CAFÉ"));
// 0 (case ignored, accents match)
이는 대소문자 차이는 무시하면서 악센트가 있는 문자와 없는 문자를 다르게 취급합니다. 악센트 차이는 중요하지만 대소문자 차이는 중요하지 않을 때 사용하세요.
사용 사례에 적합한 민감도 선택하기
대부분의 대소문자 구분 없는 비교 요구사항에서 기본 민감도가 최상의 사용자 경험을 제공합니다:
- 사용자가 악센트 없이 쿼리를 입력하는 검색 기능
- 대소문자가 중요하지 않은 사용자 이름 매칭
- 최대한의 유연성을 원하는 퍼지 검색
Smith와smith가 일치해야 하는 폼 유효성 검사
다음과 같은 경우 악센트 민감도를 사용하세요:
- 언어에서 악센트 문자를 구분해야 하는 경우
- 데이터에 서로 다른 의미를 가진 악센트가 있는 버전과 없는 버전이 모두 포함된 경우
- 대소문자 구분 없이 악센트를 인식하는 비교가 필요한 경우
includes를 사용한 대소문자 구분 없는 검색 수행
Intl.Collator API는 완전한 문자열을 비교하지만 부분 문자열 매칭을 제공하지 않습니다. 대소문자를 구분하지 않는 검색의 경우 여전히 로케일 인식 비교를 다른 접근 방식과 결합해야 합니다.
한 가지 옵션은 부분 문자열 검색에 toLowerCase()를 사용하되 국제 텍스트에 대한 제한 사항을 수용하는 것입니다:
function caseInsensitiveIncludes(text, query, locale = "en") {
return text.toLowerCase().includes(query.toLowerCase());
}
const text = "The Quick Brown Fox";
console.log(caseInsensitiveIncludes(text, "quick"));
// true
국제 텍스트를 올바르게 처리하는 보다 정교한 검색을 위해서는 가능한 부분 문자열 위치를 반복하고 각 비교에 collator를 사용해야 합니다:
function localeAwareIncludes(text, query, locale = "en") {
const collator = new Intl.Collator(locale, { sensitivity: "base" });
for (let i = 0; i <= text.length - query.length; i++) {
const substring = text.slice(i, i + query.length);
if (collator.compare(substring, query) === 0) {
return true;
}
}
return false;
}
const text = "The Quick Brown Fox";
console.log(localeAwareIncludes(text, "quick"));
// true
이 접근 방식은 올바른 길이의 가능한 모든 부분 문자열을 확인하고 각각에 대해 로케일 인식 비교를 사용합니다. 국제 텍스트를 올바르게 처리하지만 단순한 includes()보다 성능이 떨어집니다.
Intl.Collator 사용 시 성능 고려사항
Intl.Collator 인스턴스를 생성하려면 로케일 데이터를 로드하고 옵션을 처리해야 합니다. 여러 비교를 수행해야 하는 경우 collator를 한 번 생성하고 재사용하세요:
// Inefficient: creates collator for every comparison
function badCompare(items, target) {
return items.filter(item =>
new Intl.Collator("en", { sensitivity: "base" }).compare(item, target) === 0
);
}
// Efficient: creates collator once, reuses it
function goodCompare(items, target) {
const collator = new Intl.Collator("en", { sensitivity: "base" });
return items.filter(item =>
collator.compare(item, target) === 0
);
}
효율적인 버전은 필터링 전에 collator를 한 번 생성합니다. 각 비교는 동일한 인스턴스를 사용하여 반복적인 초기화 오버헤드를 방지합니다.
빈번한 비교를 수행하는 애플리케이션의 경우 애플리케이션 시작 시 collator 인스턴스를 생성하고 코드베이스 전체에서 사용할 수 있도록 내보내세요:
// utils/collation.js
export const caseInsensitiveCollator = new Intl.Collator("en", {
sensitivity: "base"
});
export const accentInsensitiveCollator = new Intl.Collator("en", {
sensitivity: "accent"
});
// In your application code
import { caseInsensitiveCollator } from "./utils/collation";
const isMatch = caseInsensitiveCollator.compare(input, expected) === 0;
이 패턴은 성능을 극대화하고 애플리케이션 전체에서 일관된 비교 동작을 유지합니다.
toLowerCase와 Intl.Collator 사용 시기
텍스트 콘텐츠를 제어하고 ASCII 문자만 포함된다는 것을 알고 있는 영어 전용 애플리케이션의 경우, toLowerCase()는 허용 가능한 결과를 제공합니다:
// Acceptable for English-only, ASCII-only text
const isMatch = str1.toLowerCase() === str2.toLowerCase();
이 접근 방식은 간단하고 빠르며 대부분의 개발자에게 익숙합니다. 애플리케이션이 실제로 국제 텍스트를 처리하지 않는다면 로케일 인식 비교의 추가 복잡성이 가치를 제공하지 못할 수 있습니다.
국제 애플리케이션이나 사용자가 모든 언어로 텍스트를 입력하는 애플리케이션의 경우, 적절한 민감도로 Intl.Collator를 사용하세요:
// Required for international text
const collator = new Intl.Collator(userLocale, { sensitivity: "base" });
const isMatch = collator.compare(str1, str2) === 0;
이렇게 하면 사용자가 말하거나 입력하는 언어에 관계없이 올바른 동작이 보장됩니다. Intl.Collator 사용에 따른 작은 성능 비용은 잘못된 비교를 피하기 위해 충분히 가치가 있습니다.
현재 애플리케이션이 영어만 지원하더라도 처음부터 로케일 인식 비교를 사용하면 향후 국제화가 더 쉬워집니다. 새로운 언어에 대한 지원을 추가해도 비교 로직을 변경할 필요가 없습니다.
대소문자 구분 없는 비교의 실용적 사용 사례
대소문자 구분 없는 비교는 다양한 일반적인 시나리오에서 나타납니다:
사용자 이름 및 이메일 매칭
사용자는 일관되지 않은 대소문자로 사용자 이름과 이메일 주소를 입력합니다:
const collator = new Intl.Collator("en", { sensitivity: "base" });
function findUserByEmail(users, email) {
return users.find(user =>
collator.compare(user.email, email) === 0
);
}
const users = [
{ email: "[email protected]", name: "John" },
{ email: "[email protected]", name: "Jane" }
];
console.log(findUserByEmail(users, "[email protected]"));
// { email: "[email protected]", name: "John" }
이렇게 하면 사용자가 이메일 주소를 어떻게 대문자로 표기하든 관계없이 사용자를 찾을 수 있습니다.
검색 자동완성
자동완성 제안은 대소문자 구분 없이 부분 입력과 일치해야 합니다:
const collator = new Intl.Collator("en", { sensitivity: "base" });
function getSuggestions(items, query) {
const queryLower = query.toLowerCase();
return items.filter(item =>
item.toLowerCase().startsWith(queryLower)
);
}
const items = ["Apple", "Apricot", "Banana", "Cherry"];
console.log(getSuggestions(items, "ap"));
// ["Apple", "Apricot"]
이렇게 하면 사용자가 입력하는 대소문자에 관계없이 제안을 제공합니다.
태그 및 카테고리 매칭
사용자는 일관된 대소문자 표기 없이 콘텐츠에 태그나 카테고리를 할당합니다:
const collator = new Intl.Collator("en", { sensitivity: "base" });
function hasTag(item, tag) {
return item.tags.some(itemTag =>
collator.compare(itemTag, tag) === 0
);
}
const article = {
title: "My Article",
tags: ["JavaScript", "Tutorial", "Web Development"]
};
console.log(hasTag(article, "javascript"));
// true
이렇게 하면 대소문자 차이에 관계없이 태그를 매칭합니다.