선호하는 로케일을 지원하지 않을 때 대체 로케일 처리 방법
사용자가 지원하지 않는 로케일을 선호할 때 자동으로 지원되는 언어를 선택하는 방법
소개
모든 웹 애플리케이션이 전 세계의 모든 언어를 지원하는 것은 아닙니다. 사용자가 애플리케이션에서 지원하지 않는 언어를 선호할 경우, 오류 메시지나 번역되지 않은 텍스트 대신 가장 적절한 다른 언어로 콘텐츠를 표시하는 대체 메커니즘이 필요합니다.
로케일 대체(fallback)는 선호하는 로케일을 사용할 수 없을 때 대안이 되는 로케일을 선택하는 과정을 말합니다. JavaScript의 Intl API는 여러 로케일 옵션을 받아 지원하는 첫 번째 로케일을 자동으로 선택해 줍니다. 이를 통해 사용자가 선호하는 로케일이 정확히 없더라도 애플리케이션이 항상 올바르게 포맷된 콘텐츠를 보여줄 수 있습니다.
이 강의에서는 JavaScript에서 로케일 대체가 어떻게 동작하는지, 이를 효과적으로 구현하는 방법, 특수한 로케일 지원 요구가 있는 애플리케이션에서 커스텀 대체 로직을 만드는 방법을 설명합니다.
지원하지 않는 로케일의 문제점
Intl API에 로케일 식별자를 전달하면, 자바스크립트 런타임이 그 로케일을 지원해야만 콘텐츠를 올바르게 포맷할 수 있습니다. 예를 들어, 노르웨이어 뉘노시크(Nynorsk) 포맷을 요청했는데 런타임이 노르웨이어 복몰(Bokmål)만 지원한다면, 포매터는 이를 유연하게 처리할 수 있어야 합니다.
대체처리가 없다면, 애플리케이션은 지원하지 않는 로케일을 만날 때 콘텐츠를 표시하지 못하거나 번역되지 않은 원본 텍스트를 보여주게 됩니다. 비주류 언어 변형을 사용하는 지역의 이용자들은 깨진 인터페이스를 경험하게 됩니다.
예를 들어, 캐나다 프랑스어를 사용하는 사용자가 있다고 가정해봅시다. 앱에서 유럽 프랑스어만 지원한다면, 완전히 실패하는 것보다는 포매터가 유럽 프랑스어 규칙을 적용하는 편이 훨씬 나은 사용자 경험을 제공합니다. 완벽하지 않더라도, 아무런 현지화도 없는 것보다는 훨씬 낫습니다.
Intl API가 자동으로 폴백을 처리하는 방식
모든 Intl 생성자는 단일 로케일 문자열 또는 로케일 문자열의 배열을 받을 수 있습니다. 배열을 전달하면 런타임이 순서대로 각 로케일을 평가하여 지원하는 첫 번째 로케일을 사용합니다.
const locales = ["fr-CA", "fr-FR", "en-US"];
const formatter = new Intl.DateTimeFormat(locales);
const date = new Date("2025-03-15");
console.log(formatter.format(date));
// Uses fr-CA if available
// Falls back to fr-FR if fr-CA is not available
// Falls back to en-US if neither French variant is available
런타임은 배열을 왼쪽에서 오른쪽으로 검사합니다. 캐나다 프랑스어를 지원하면 그 로케일을 사용하고, 지원하지 않으면 유럽 프랑스어를 시도합니다. 두 프랑스어 변형 모두 사용할 수 없다면 미국 영어로 폴백합니다.
이런 자동 폴백 기능 덕분에 특정 로케일의 지원 여부를 직접 확인하거나, 오류 처리를 신경 쓸 필요가 없습니다. Intl API는 반드시 지원되는 로케일을 선택하거나 시스템 기본 로케일로 폴백하는 것을 보장합니다.
여러 로케일 옵션 제공하기
폴백을 구현하는 가장 간단한 방법은 선호 순서대로 로케일 배열을 전달하는 것입니다. 이 방법은 DateTimeFormat, NumberFormat, Collator 등 모든 Intl 생성자에서 사용할 수 있습니다.
const locales = ["es-MX", "es-ES", "es", "en"];
const numberFormatter = new Intl.NumberFormat(locales, {
style: "currency",
currency: "USD"
});
console.log(numberFormatter.format(1234.56));
// Uses Mexican Spanish if available
// Falls back to European Spanish
// Falls back to generic Spanish
// Falls back to English as final option
이 패턴은 점진적으로 대체되는 경로를 제공합니다. 사용자는 선호하는 방언이 지원된다면 그 언어로, 그렇지 않으면 더 넓은 언어 변형, 마지막으로 일반적인 폴백 언어로 콘텐츠를 받게 됩니다.
순서가 중요합니다. 런타임은 지원되는 첫 번째 로케일을 선택하므로, 가장 구체적이고 선호하는 로케일을 맨 앞에 두어야 합니다.
로케일 매칭 방식 이해하기
여러 개의 로케일을 제공하면, JavaScript 런타임은 로케일 매칭 알고리즘을 사용해 가장 적합한 옵션을 선택합니다. 이 알고리즘은 요청한 로케일 목록과 런타임이 지원하는 로케일 목록을 비교합니다.
요청한 로케일이 지원되는 로케일과 정확히 일치하면 런타임은 해당 로케일을 즉시 사용합니다. 만약 정확히 일치하는 로케일이 없다면, 언어와 지역 코드를 기준으로 관련된 로케일을 선택할 수 있습니다.
예를 들어, en-AU (호주 영어)를 요청했지만 런타임이 en-US 와 en-GB만 지원하는 경우, 완전히 다른 언어로 전환하는 대신 이러한 영어 변형 중 하나를 선택하게 됩니다.
const locales = ["en-AU", "en"];
const formatter = new Intl.DateTimeFormat(locales);
const resolvedLocale = formatter.resolvedOptions().locale;
console.log(resolvedLocale);
// Might show "en-US" or "en-GB" depending on the runtime
// The runtime selected a supported English variant
resolvedOptions() 메서드는 포매터가 실제로 사용하는 로케일을 반환합니다. 이를 통해 폴백(fallback) 이후 어떤 로케일이 선택되었는지 확인할 수 있습니다.
지원되는 로케일 확인하기
supportedLocalesOf() 정적 메서드는 특정 Intl 생성자가 목록 중 어떤 로케일을 지원하는지 확인합니다. 이 메서드는 지원되는 로케일만 포함된 배열을 반환합니다.
const requestedLocales = ["fr-CA", "fr-FR", "de-DE", "ja-JP"];
const supportedLocales = Intl.DateTimeFormat.supportedLocalesOf(requestedLocales);
console.log(supportedLocales);
// Output depends on runtime support
// Example: ["fr-FR", "de-DE", "ja-JP"]
// Canadian French was not supported, others were
이 메서드는 요청된 로케일 중에서 런타임이 기본값 없이 실제로 사용할 수 있는 것만 필터링합니다. 지원되지 않는 로케일은 반환된 배열에서 제외됩니다.
이 메서드를 사용하여 포매터를 만들기 전에 지원 여부를 확인하거나, 사용자에게 제공되는 언어 옵션을 표시할 수 있습니다.
const availableLocales = ["en-US", "es-MX", "fr-FR", "de-DE", "ja-JP"];
const supported = Intl.NumberFormat.supportedLocalesOf(availableLocales);
console.log("This runtime supports:", supported);
// Shows which of your application locales work in this environment
각 Intl 생성자마다 자체 supportedLocalesOf() 메서드가 있는데, 국제화 기능마다 지원되는 로케일이 다를 수 있기 때문입니다. 예를 들어, 어떤 런타임은 숫자 포매팅에 프랑스어를 지원하지만, 텍스트 세분화에는 지원하지 않을 수 있습니다.
로케일 식별자로 폴백 체인 구축하기
애플리케이션이 지원하는 로케일을 알고 있다면, 점차 덜 구체적으로 변하는 폴백 체인을 만들 수 있습니다. 이 패턴은 전체 로케일 식별자로 시작해서 일치하는 값을 찾을 때까지 구성 요소를 하나씩 제거합니다.
function buildFallbackChain(locale) {
const chain = [locale];
const parts = locale.split("-");
if (parts.length > 1) {
chain.push(parts[0]);
}
chain.push("en");
return chain;
}
const fallbacks = buildFallbackChain("zh-Hans-CN");
console.log(fallbacks);
// ["zh-Hans-CN", "zh", "en"]
const formatter = new Intl.DateTimeFormat(fallbacks);
// Tries Simplified Chinese for China
// Falls back to generic Chinese
// Falls back to English
이 함수는 로케일 식별자에서 언어 코드를 추출하고 마지막에 영어 폴백을 추가해 폴백 체인을 만듭니다. 애플리케이션에서 지원하는 로케일에 따라 더 정교한 폴백 규칙을 추가하도록 이 로직을 확장할 수 있습니다.
여러 언어 변형을 지원하는 애플리케이션의 경우, 바로 영어로 전환하기 전에 관련 방언으로 먼저 폴백하도록 설정할 수 있습니다.
function buildSmartFallbackChain(locale) {
const chain = [locale];
if (locale.startsWith("es-")) {
chain.push("es-MX", "es-ES", "es");
} else if (locale.startsWith("fr-")) {
chain.push("fr-FR", "fr-CA", "fr");
} else if (locale.startsWith("zh-")) {
chain.push("zh-Hans-CN", "zh-Hant-TW", "zh");
}
const parts = locale.split("-");
if (parts.length > 1 && !chain.includes(parts[0])) {
chain.push(parts[0]);
}
if (!chain.includes("en")) {
chain.push("en");
}
return chain;
}
const fallbacks = buildSmartFallbackChain("es-AR");
console.log(fallbacks);
// ["es-AR", "es-MX", "es-ES", "es", "en"]
// Tries Argentinian Spanish
// Falls back to Mexican Spanish
// Falls back to European Spanish
// Falls back to generic Spanish
// Falls back to English
이 방식은 사용자가 영어로 전환되기 전에, 우선 자신의 언어와 관련된 방언의 콘텐츠를 볼 수 있도록 해줍니다.
로케일 일치 알고리즘 선택하기
Intl API는 두 가지 로케일 일치 알고리즘(lookup과 best fit)을 지원합니다. 포매터를 생성할 때 localeMatcher 옵션으로 사용할 알고리즘을 지정할 수 있습니다.
lookup 알고리즘은 BCP 47 Lookup 명세를 따릅니다. 로케일 식별자를 체계적으로 비교하여, 첫 번째로 정확히 일치하는 로케일을 선택하는 엄격한 매칭 방식입니다.
const locales = ["de-DE", "en-US"];
const formatter = new Intl.NumberFormat(locales, {
localeMatcher: "lookup"
});
console.log(formatter.resolvedOptions().locale);
// Uses strict lookup matching rules
best fit 알고리즘은 런타임에서 자체 매칭 로직을 사용하여 로케일을 선택하게 합니다. 이 알고리즘은 사용자의 니즈를 가장 잘 충족하는 로케일을 똑똑하게 선택할 수 있습니다. 반드시 정확히 일치하지 않아도 됩니다.
const locales = ["de-DE", "en-US"];
const formatter = new Intl.NumberFormat(locales, {
localeMatcher: "best fit"
});
console.log(formatter.resolvedOptions().locale);
// Uses runtime's best fit algorithm
// Might select a related locale more intelligently
기본 알고리즘은 best fit입니다. 다양한 자바스크립트 런타임에서 더 나은 결과를 제공하기 때문에 대부분의 애플리케이션에서 기본값을 사용하는 것이 좋습니다. 예측 가능하고 엄격한 매칭이 필요할 때만 lookup을 사용하세요.
브라우저 언어 설정을 활용한 자동 폴백
navigator.languages 속성은 사용자의 선호 언어를 우선순위 배열로 반환합니다. 이 배열을 Intl 생성자에 직접 전달하면, 브라우저 설정에 따라 자동으로 폴백이 적용됩니다.
const formatter = new Intl.DateTimeFormat(navigator.languages);
const date = new Date("2025-03-15");
console.log(formatter.format(date));
// Automatically uses user's preferred supported language
이 방식은 폴백 로직을 브라우저가 모두 처리하게 해줍니다. 사용자의 첫 번째 선호 언어가 지원되지 않으면, Intl API가 자동으로 두 번째, 세 번째 언어를 차례로 시도합니다.
이 패턴은 수동으로 폴백 체인을 만들지 않고, 사용자의 모든 언어 선호를 존중하고자 할 때 효과적으로 작동합니다.
console.log(navigator.languages);
// ["fr-CA", "fr", "en-US", "en"]
const numberFormatter = new Intl.NumberFormat(navigator.languages, {
style: "currency",
currency: "USD"
});
console.log(numberFormatter.format(1234.56));
// Tries Canadian French first
// Falls back through the user's complete preference list
Intl API는 배열 내의 각 언어를 평가하여 지원하는 첫 번째 언어를 선택합니다. 다양한 사용자 선호를 처리할 때 매우 견고한 솔루션입니다.
애플리케이션 지원과 사용자 설정 통합하기
지원하는 로케일이 제한적인 애플리케이션의 경우, 포매터를 생성하기 전에 사용자 설정에서 지원하는 로케일만 필터링할 수 있습니다. 이를 통해 애플리케이션이 처리할 수 있는 로케일만 사용하게 됩니다.
const supportedLocales = ["en-US", "es-MX", "fr-FR", "de-DE"];
function findBestLocale(userPreferences, appSupported) {
const preferences = userPreferences.map(pref => {
const parts = pref.split("-");
return [pref, parts[0]];
}).flat();
for (const pref of preferences) {
if (appSupported.includes(pref)) {
return pref;
}
}
return appSupported[0];
}
const userLocale = findBestLocale(navigator.languages, supportedLocales);
const formatter = new Intl.DateTimeFormat(userLocale);
console.log(formatter.resolvedOptions().locale);
// Selected locale matches both user preference and app support
이 함수는 사용자 설정과 지원 로케일 사이에서 첫 번째로 일치하는 값을 찾으며, 더 넓은 범위로 매칭하기 위해 지역 코드 없는 언어 코드도 시도합니다.
더 정교한 매칭을 원한다면, 현재 런타임에서 Intl API가 사용자의 설정 중 어떤 로케일을 지원하는지 확인할 수 있습니다.
const supportedLocales = ["en-US", "es-MX", "fr-FR", "de-DE"];
function findBestSupportedLocale(userPreferences, appSupported) {
const runtimeSupported = Intl.DateTimeFormat.supportedLocalesOf(appSupported);
for (const pref of userPreferences) {
if (runtimeSupported.includes(pref)) {
return pref;
}
const lang = pref.split("-")[0];
const match = runtimeSupported.find(s => s.startsWith(lang));
if (match) {
return match;
}
}
return runtimeSupported[0] || "en";
}
const userLocale = findBestSupportedLocale(navigator.languages, supportedLocales);
const formatter = new Intl.DateTimeFormat(userLocale);
이 방법을 사용하면 선택된 로케일이 애플리케이션과 자바스크립트 런타임 모두에서 지원됨을 보장할 수 있습니다.
로케일이 매칭되지 않을 때 처리 방법
요청한 로케일 중 지원되는 것이 없다면, Intl API는 시스템의 기본 로케일로 자동으로 전환합니다. 이 기본값은 운영 체제, 브라우저 또는 Node.js 설정에 따라 다릅니다.
const unsupportedLocales = ["non-existent-locale"];
const formatter = new Intl.DateTimeFormat(unsupportedLocales);
console.log(formatter.resolvedOptions().locale);
// Shows the system default locale
// Might be "en-US" or another locale depending on the system
시스템 기본값 덕분에 포매터는 잘못되거나 지원되지 않는 로케일 목록이라도 항상 동작합니다. 로케일 문제로 인해 애플리케이션이 오류를 발생시키지 않습니다.
시스템 기본값 대신 특정 값으로 항상 대체되도록 하려면, 로케일 배열의 마지막에 en 또는 en-US처럼 널리 지원되는 로케일을 추가하세요.
const locales = ["xyz-INVALID", "en"];
const formatter = new Intl.DateTimeFormat(locales);
console.log(formatter.resolvedOptions().locale);
// Will use "en" since the first locale is invalid
// Guaranteed fallback to English instead of system default
이 방식은 다양한 환경에서 애플리케이션의 동작을 더 예측 가능하게 만듭니다.
실제 서비스용 애플리케이션을 위한 실용적인 패턴
프로덕션 애플리케이션을 개발할 때는 여러 단계의 폴백 전략을 결합해 다양한 사용자 환경에서도 견고하게 로케일을 처리할 수 있도록 하세요.
일반적인 패턴은 사용자 설정, 앱에서 지원하는 로케일, 그리고 반드시 동작할 최종 폴백까지 포함한 포매터 체인을 만드는 것입니다.
class LocaleManager {
constructor(supportedLocales) {
this.supportedLocales = supportedLocales;
this.defaultLocale = "en-US";
}
buildLocaleChain(userPreference) {
const chain = [];
if (userPreference) {
chain.push(userPreference);
const lang = userPreference.split("-")[0];
if (lang !== userPreference) {
chain.push(lang);
}
}
chain.push(...navigator.languages);
chain.push(...this.supportedLocales);
chain.push(this.defaultLocale);
const unique = [...new Set(chain)];
return unique;
}
createDateFormatter(userPreference, options = {}) {
const locales = this.buildLocaleChain(userPreference);
return new Intl.DateTimeFormat(locales, options);
}
createNumberFormatter(userPreference, options = {}) {
const locales = this.buildLocaleChain(userPreference);
return new Intl.NumberFormat(locales, options);
}
}
const manager = new LocaleManager(["en-US", "es-MX", "fr-FR", "de-DE"]);
const dateFormatter = manager.createDateFormatter("pt-BR");
console.log(dateFormatter.resolvedOptions().locale);
// Uses comprehensive fallback chain
// Tries Portuguese for Brazil
// Falls back to Portuguese
// Falls back through navigator.languages
// Falls back through supported locales
// Guaranteed to use en-US if nothing else matches
이 클래스는 로케일 폴백 로직을 캡슐화하여 전체 애플리케이션에서 일관된 동작을 보장합니다.
런타임에 사용자의 로케일 변경에 반응해야 하는 애플리케이션의 경우, 로케일 매니저를 이벤트 리스너와 함께 사용하세요.
class LocaleAwareFormatter {
constructor(supportedLocales) {
this.supportedLocales = supportedLocales;
this.updateFormatters();
window.addEventListener("languagechange", () => {
this.updateFormatters();
});
}
updateFormatters() {
const locales = [...navigator.languages, ...this.supportedLocales, "en"];
this.dateFormatter = new Intl.DateTimeFormat(locales);
this.numberFormatter = new Intl.NumberFormat(locales);
}
formatDate(date) {
return this.dateFormatter.format(date);
}
formatNumber(number) {
return this.numberFormatter.format(number);
}
}
const formatter = new LocaleAwareFormatter(["en-US", "es-MX", "fr-FR"]);
console.log(formatter.formatDate(new Date()));
// Automatically updates when user changes language preferences
이 패턴은 브라우저 언어 변경에 맞춰 항상 동기화되는 포매터를 생성하므로, 애플리케이션이 사용자의 최신 선호도에 따라 콘텐츠를 표시할 수 있습니다.
요약
로케일 폴백을 사용하면 사용자가 명시적으로 지원하지 않는 로케일을 선호할 때도 애플리케이션이 올바르게 포맷된 콘텐츠를 표시할 수 있습니다. Intl API는 로케일 선호 배열을 받아 자동으로 폴백을 처리하며, 가장 먼저 지원되는 옵션을 선택합니다.
핵심 개념:
- Intl 생성자에 로케일 배열을 전달하면 자동 폴백 적용
- 런타임이 배열에서 지원하는 첫 번째 로케일을 선택함
- 사용 가능한 로케일 확인에는
supportedLocalesOf()사용 - 구체적인 로케일에서 일반적인 로케일 순서로 폴백 체인을 구축
- 매칭 알고리즘은
localeMatcher옵션으로 제어 - 자동 사용자 선호 처리는
navigator.languages직접 전달 - 영어처럼 널리 지원되는 최종 폴백을 항상 포함
- 일치하는 로케일이 없으면 런타임은 시스템 기본값으로 폴백
대부분의 애플리케이션에서는 로케일 배열을 사용한 자동 폴백을 활용하세요. 특정 순서로 로케일을 시도해야 할 경우 커스텀 폴백 로직을 구현하면 됩니다.