새 포매터를 생성하는 대신 포매터를 재사용해야 하는 이유는 무엇인가요?
Intl 포매터 생성은 비용이 많이 들지만, 동일한 포매터 인스턴스를 재사용하면 성능이 향상됩니다
소개
JavaScript에서 Intl 포매터를 생성할 때, 브라우저는 포매터 인스턴스를 설정하기 위해 비용이 많이 드는 작업을 수행합니다. 옵션을 파싱하고, 디스크에서 로케일 데이터를 로드하고, 포맷팅을 위한 내부 데이터 구조를 구축합니다. 값을 포맷해야 할 때마다 새 포매터를 생성하면, 이러한 비용이 많이 드는 작업을 불필요하게 반복하게 됩니다.
포매터 인스턴스를 재사용하면 이러한 반복 작업이 제거됩니다. 포매터를 한 번 생성하고 여러 번 사용합니다. 이 패턴은 루프, 자주 호출되는 함수, 많은 값을 포맷하는 고성능 코드에서 특히 중요합니다.
새 포매터를 생성하는 것과 기존 포매터를 재사용하는 것 사이의 성능 차이는 상당할 수 있습니다. 일반적인 시나리오에서 포매터를 재사용하면 포맷 시간을 수백 밀리초에서 단 몇 밀리초로 줄일 수 있습니다.
포매터 생성이 비용이 많이 드는 이유
Intl 포매터를 생성하는 것은 브라우저 내부에서 발생하는 여러 비용이 많이 드는 작업을 포함합니다.
첫째, 브라우저는 제공된 옵션을 파싱하고 검증합니다. 로케일 식별자가 유효한지, 숫자 옵션이 범위 내에 있는지, 호환되지 않는 옵션이 결합되지 않았는지 확인합니다. 이 검증에는 문자열 파싱 및 조회 작업이 필요합니다.
둘째, 브라우저는 로케일 협상을 수행합니다. 요청된 로케일을 가져와 브라우저가 지원하는 로케일 중에서 가장 적합한 일치 항목을 찾습니다. 여기에는 로케일 식별자 비교 및 폴백 규칙 적용이 포함됩니다.
셋째, 브라우저는 로케일별 데이터를 로드합니다. 날짜 포매터에는 해당 로케일의 월 이름, 요일 이름, 포맷 패턴이 필요합니다. 숫자 포매터에는 그룹화 규칙, 소수점 구분 기호, 숫자 문자가 필요합니다. 이 데이터는 브라우저의 내부 로케일 데이터베이스에서 가져오며 메모리에 로드되어야 합니다.
넷째, 브라우저는 포매팅을 위한 내부 데이터 구조를 구축합니다. 포매팅 패턴을 효율적인 표현으로 컴파일하고 값을 처리하기 위한 상태 머신을 설정합니다. 이러한 구조는 포매터의 수명 동안 유지됩니다.
이 모든 작업은 포매터를 생성할 때마다 발생합니다. 포매터를 생성하고 한 번 사용한 후 폐기하면 모든 설정 작업이 낭비됩니다.
성능 차이
포매터를 재생성하는 것의 성능 영향은 많은 값을 포매팅할 때 가시화됩니다.
포매터를 재사용하지 않고 날짜 목록을 포매팅하는 코드를 고려해 보겠습니다.
const dates = [
new Date('2024-01-15'),
new Date('2024-02-20'),
new Date('2024-03-10')
];
// Creates a new formatter for each date
dates.forEach(date => {
const formatted = date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
console.log(formatted);
});
toLocaleDateString() 메서드는 포매팅하는 각 날짜에 대해 내부적으로 새로운 DateTimeFormat 인스턴스를 생성합니다. 세 개의 날짜에 대해 세 개의 포매터를 생성합니다. 천 개의 날짜에 대해서는 천 개의 포매터를 생성합니다.
하나의 포매터를 생성하고 재사용하는 코드와 비교해 보겠습니다.
const dates = [
new Date('2024-01-15'),
new Date('2024-02-20'),
new Date('2024-03-10')
];
// Create formatter once
const formatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
// Reuse formatter for each date
dates.forEach(date => {
const formatted = formatter.format(date);
console.log(formatted);
});
이 코드는 하나의 포매터를 생성하고 세 번 사용합니다. 천 개의 날짜에 대해서도 여전히 하나의 포매터를 생성하고 천 번 사용합니다.
이러한 접근 방식 간의 시간 차이는 포매팅하는 값의 수에 따라 증가합니다. 천 개의 포매터를 생성하여 천 개의 날짜를 포매팅하는 것은 재사용된 하나의 포매터로 포매팅하는 것보다 50배 이상 오래 걸릴 수 있습니다.
모듈 스코프에서 포매터 재사용하기
포매터를 재사용하는 가장 간단한 방법은 모듈 스코프에서 한 번 생성하고 모듈 전체에서 사용하는 것입니다.
// Create formatter at module scope
const dateFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
function formatDate(date) {
return dateFormatter.format(date);
}
function formatDates(dates) {
return dates.map(date => dateFormatter.format(date));
}
// All functions share the same formatter instance
console.log(formatDate(new Date()));
console.log(formatDates([new Date(), new Date()]));
이 패턴은 코드 전체에서 값을 동일한 방식으로 포매팅할 때 잘 작동합니다. 포매터는 애플리케이션의 수명 동안 존재하며, 필요한 모든 함수가 동일한 인스턴스를 사용할 수 있습니다.
동일한 패턴이 숫자 포매터, 목록 포매터 및 기타 모든 Intl 포매터에도 적용됩니다.
const numberFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
const listFormatter = new Intl.ListFormat('en-US', {
style: 'long',
type: 'conjunction'
});
function formatPrice(amount) {
return numberFormatter.format(amount);
}
function formatNames(names) {
return listFormatter.format(names);
}
함수에서 포매터 재사용
코드의 여러 부분에서 서로 다른 포매팅 옵션이 필요한 경우, 함수 내부에 포매터를 생성하고 클로저를 활용하여 보존할 수 있습니다.
function createDateFormatter() {
const formatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
return function formatDate(date) {
return formatter.format(date);
};
}
const formatDate = createDateFormatter();
// The formatter is created once when you call createDateFormatter
// Each call to formatDate reuses the same formatter
console.log(formatDate(new Date('2024-01-15')));
console.log(formatDate(new Date('2024-02-20')));
console.log(formatDate(new Date('2024-03-10')));
이 패턴은 재사용되는 구성된 포매터를 생성하되 포매터 자체를 노출하고 싶지 않을 때 유용합니다.
포매터 재사용이 가장 중요한 경우
포매터 재사용은 특정 시나리오에서 가장 큰 이점을 제공합니다.
첫 번째 시나리오는 루프입니다. 루프 내부에서 값을 포매팅하는 경우, 각 반복마다 새 포매터를 생성하면 비용이 반복 횟수만큼 배가됩니다.
// Inefficient: creates N formatters
for (let i = 0; i < 1000; i++) {
const formatted = new Intl.NumberFormat('en-US').format(i);
processValue(formatted);
}
// Efficient: creates 1 formatter
const formatter = new Intl.NumberFormat('en-US');
for (let i = 0; i < 1000; i++) {
const formatted = formatter.format(i);
processValue(formatted);
}
두 번째 시나리오는 자주 호출되는 함수입니다. 함수가 값을 포매팅하고 여러 번 호출되는 경우, 포매터 재사용은 각 호출마다 포매터를 재생성하는 것을 방지합니다.
// Inefficient: creates formatter on every call
function formatCurrency(amount) {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
return formatter.format(amount);
}
// Efficient: creates formatter once
const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
function formatCurrency(amount) {
return currencyFormatter.format(amount);
}
세 번째 시나리오는 대규모 데이터셋 처리입니다. 수백 또는 수천 개의 값을 포매팅할 때, 포매터 생성의 설정 비용이 전체 시간의 상당 부분을 차지하게 됩니다.
// Inefficient for large datasets
function processRecords(records) {
return records.map(record => ({
date: new Intl.DateTimeFormat('en-US').format(record.date),
amount: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(record.amount)
}));
}
// Efficient for large datasets
const dateFormatter = new Intl.DateTimeFormat('en-US');
const amountFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
function processRecords(records) {
return records.map(record => ({
date: dateFormatter.format(record.date),
amount: amountFormatter.format(record.amount)
}));
}
이러한 시나리오에서 포매터를 재사용하면 포매팅 작업에 소요되는 시간이 줄어들고 애플리케이션 응답성이 향상됩니다.
다양한 옵션으로 포매터 캐싱
여러 가지 옵션 조합으로 포매터를 사용해야 하는 경우, 구성에 따라 포매터를 캐싱할 수 있습니다.
const formatterCache = new Map();
function getNumberFormatter(locale, options) {
// Create a cache key from locale and options
const key = JSON.stringify({ locale, options });
// Return cached formatter if it exists
if (formatterCache.has(key)) {
return formatterCache.get(key);
}
// Create new formatter and cache it
const formatter = new Intl.NumberFormat(locale, options);
formatterCache.set(key, formatter);
return formatter;
}
// First call creates and caches formatter
const formatter1 = getNumberFormatter('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter1.format(42.50));
// Second call reuses cached formatter
const formatter2 = getNumberFormatter('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter2.format(99.99));
// Different options create and cache a new formatter
const formatter3 = getNumberFormatter('en-US', { style: 'percent' });
console.log(formatter3.format(0.42));
이 패턴을 사용하면 코드의 여러 부분에서 서로 다른 포매팅 구성이 필요한 경우에도 포매터 재사용의 이점을 얻을 수 있습니다.
최신 브라우저 최적화
최신 JavaScript 엔진은 Intl 포매터 생성을 최적화하여 성능 비용을 줄였습니다. 오늘날 포매터 생성은 이전 브라우저보다 빠릅니다.
그러나 포매터 재사용은 여전히 모범 사례로 남아 있습니다. 최적화가 있더라도 포매터를 생성하는 것은 기존 포매터에서 format() 메서드를 호출하는 것보다 여전히 비용이 더 많이 듭니다. 비용 차이는 예전보다 작아졌지만 여전히 존재합니다.
고성능 코드, 루프에서 실행되는 코드, 대규모 데이터셋을 처리하는 코드에서 포매터 재사용은 계속해서 측정 가능한 이점을 제공합니다. 포매터 생성의 최적화가 포매터 재사용의 가치를 제거하지는 않습니다.
핵심 요점
Intl 포매터 생성은 브라우저가 옵션을 파싱하고, 로케일 협상을 수행하며, 로케일 데이터를 로드하고, 내부 데이터 구조를 구축해야 하기 때문에 비용이 많이 듭니다. 이 작업은 포매터를 생성할 때마다 발생합니다.
포매터 인스턴스를 재사용하면 이 작업을 반복하지 않아도 됩니다. 포매터를 한 번 생성하고 format() 메서드를 여러 번 호출합니다. 이를 통해 포매팅 작업에 소요되는 시간이 줄어듭니다.
포매터 재사용은 루프, 자주 호출되는 함수, 대규모 데이터셋을 처리하는 코드에서 가장 중요합니다. 이러한 시나리오에서 포매터 생성 비용은 전체 실행 시간의 상당 부분을 차지할 수 있습니다.
가장 간단한 재사용 패턴은 모듈 스코프에서 포매터를 생성하는 것입니다. 더 복잡한 시나리오의 경우 클로저를 사용하거나 구성 옵션을 기반으로 캐싱할 수 있습니다.