새로운 포맷터를 생성하는 대신 포맷터를 재사용해야 하는 이유는 무엇인가요?

Intl 포맷터 생성은 비용이 많이 들지만, 동일한 포맷터 인스턴스를 재사용하면 성능이 향상됩니다

소개

JavaScript에서 Intl 포맷터를 생성할 때, 브라우저는 포맷터 인스턴스를 설정하기 위해 비용이 많이 드는 작업을 수행합니다. 옵션을 파싱하고, 디스크에서 로케일 데이터를 로드하며, 포맷팅을 위한 내부 데이터 구조를 구축합니다. 값을 포맷팅할 때마다 새 포맷터를 생성하면, 이러한 비용이 많이 드는 작업을 불필요하게 반복하게 됩니다.

포맷터 인스턴스를 재사용하면 이러한 반복 작업을 제거할 수 있습니다. 포맷터를 한 번 생성하고 여러 번 사용하는 것입니다. 이 패턴은 특히 루프, 자주 호출되는 함수, 그리고 많은 값을 포맷팅하는 고성능 코드에서 중요합니다.

새 포맷터를 생성하는 것과 기존 포맷터를 재사용하는 것 사이의 성능 차이는 상당할 수 있습니다. 일반적인 시나리오에서, 포맷터를 재사용하면 포맷팅 시간을 수백 밀리초에서 몇 밀리초로 줄일 수 있습니다.

포맷터 생성이 비용이 많이 드는 이유

Intl 포맷터를 생성하는 것은 브라우저 내부에서 여러 비용이 많이 드는 작업을 포함합니다.

첫째, 브라우저는 제공한 옵션을 파싱하고 검증합니다. 로케일 식별자가 유효한지, 숫자 옵션이 범위 내에 있는지, 그리고 호환되지 않는 옵션이 결합되지 않았는지 확인합니다. 이 검증은 문자열 파싱과 조회 작업을 필요로 합니다.

둘째, 브라우저는 로케일 협상을 수행합니다. 요청한 로케일을 가져와 브라우저가 지원하는 로케일 중에서 가장 적합한 일치 항목을 찾습니다. 이는 로케일 식별자를 비교하고 폴백 규칙을 적용하는 작업을 포함합니다.

셋째, 브라우저는 로케일별 데이터를 로드합니다. 날짜 포맷터는 해당 로케일의 월 이름, 요일 이름 및 포맷팅 패턴이 필요합니다. 숫자 포맷터는 그룹화 규칙, 소수점 구분 기호 및 숫자 문자가 필요합니다. 이 데이터는 브라우저의 내부 로케일 데이터베이스에서 가져와 메모리에 로드해야 합니다.

넷째, 브라우저는 포맷팅을 위한 내부 데이터 구조를 구축합니다. 포맷팅 패턴을 효율적인 표현으로 컴파일하고 값을 처리하기 위한 상태 머신을 설정합니다. 이러한 구조는 포맷터의 수명 동안 지속됩니다.

이 모든 작업은 포맷터를 생성할 때마다 발생합니다. 포맷터를 생성하고, 한 번 사용한 다음 폐기하면, 모든 설정 작업이 낭비됩니다.

성능 차이

포맷터를 재생성하는 성능 영향은 많은 값을 포맷팅할 때 눈에 띄게 됩니다.

포맷터를 재사용하지 않고 날짜 목록을 포맷팅하는 코드를 고려해보세요.

const dates = [
  new Date('2024-01-15'),
  new Date('2024-02-20'),
  new Date('2024-03-10')
];

// 각 날짜마다 새 포맷터를 생성합니다
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')
];

// 포맷터를 한 번만 생성
const formatter = new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
});

// 각 날짜에 대해 포맷터 재사용
dates.forEach(date => {
  const formatted = formatter.format(date);
  console.log(formatted);
});

이 코드는 하나의 포맷터를 생성하고 세 번 사용합니다. 천 개의 날짜에 대해서도 여전히 하나의 포맷터를 생성하고 천 번 사용합니다.

이러한 접근 방식 간의 시간 차이는 포맷팅하는 값의 수에 따라 증가합니다. 천 개의 포맷터를 생성하여 천 개의 날짜를 포맷팅하는 것은 하나의 재사용된 포맷터로 포맷팅하는 것보다 50배 이상 오래 걸릴 수 있습니다.

모듈 스코프에서 포맷터 재사용하기

포맷터를 재사용하는 가장 간단한 방법은 모듈 스코프에서 한 번 생성하고 모듈 전체에서 사용하는 것입니다.

// 모듈 스코프에서 포맷터 생성
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));
}

// 모든 함수가 동일한 포맷터 인스턴스를 공유합니다
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();

// 포매터는 createDateFormatter를 호출할 때 한 번만 생성됩니다
// formatDate를 호출할 때마다 동일한 포매터를 재사용합니다
console.log(formatDate(new Date('2024-01-15')));
console.log(formatDate(new Date('2024-02-20')));
console.log(formatDate(new Date('2024-03-10')));

이 패턴은 재사용되는 구성된 포매터를 생성하고 싶지만 포매터 자체를 노출하고 싶지 않을 때 유용합니다.

포매터 재사용이 가장 중요한 경우

포매터 재사용은 특정 시나리오에서 가장 큰 이점을 제공합니다.

첫 번째 시나리오는 반복문입니다. 반복문 내에서 값을 포맷팅할 때, 각 반복마다 새 포매터를 생성하면 반복 횟수만큼 비용이 증가합니다.

// 비효율적: N개의 포매터 생성
for (let i = 0; i < 1000; i++) {
  const formatted = new Intl.NumberFormat('en-US').format(i);
  processValue(formatted);
}

// 효율적: 1개의 포매터 생성
const formatter = new Intl.NumberFormat('en-US');
for (let i = 0; i < 1000; i++) {
  const formatted = formatter.format(i);
  processValue(formatted);
}

두 번째 시나리오는 자주 호출되는 함수입니다. 함수가 값을 포맷팅하고 여러 번 호출된다면, 포매터 재사용은 각 호출마다 포매터를 재생성하는 것을 방지합니다.

// 비효율적: 매 호출마다 포매터 생성
function formatCurrency(amount) {
  const formatter = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
  });
  return formatter.format(amount);
}

// 효율적: 포매터 한 번만 생성
const currencyFormatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});

function formatCurrency(amount) {
  return currencyFormatter.format(amount);
}

세 번째 시나리오는 대규모 데이터셋 처리입니다. 수백 또는 수천 개의 값을 포맷팅할 때, 포매터 생성의 설정 비용이 전체 시간의 상당 부분을 차지합니다.

// 대규모 데이터셋에 비효율적
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)
  }));
}

// 대규모 데이터셋에 효율적
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) {
  // 로케일과 옵션으로 캐시 키 생성
  const key = JSON.stringify({ locale, options });

  // 캐시된 포매터가 있으면 반환
  if (formatterCache.has(key)) {
    return formatterCache.get(key);
  }

  // 새 포매터 생성 및 캐싱
  const formatter = new Intl.NumberFormat(locale, options);
  formatterCache.set(key, formatter);
  return formatter;
}

// 첫 번째 호출은 포매터를 생성하고 캐싱함
const formatter1 = getNumberFormatter('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter1.format(42.50));

// 두 번째 호출은 캐시된 포매터를 재사용
const formatter2 = getNumberFormatter('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter2.format(99.99));

// 다른 옵션은 새 포매터를 생성하고 캐싱
const formatter3 = getNumberFormatter('en-US', { style: 'percent' });
console.log(formatter3.format(0.42));

이 패턴을 사용하면 코드의 다른 부분에서 다양한 포맷팅 구성이 필요한 경우에도 포매터 재사용의 이점을 얻을 수 있습니다.

최신 브라우저 최적화

최신 JavaScript 엔진은 Intl 포매터 생성을 최적화하여 성능 비용을 줄였습니다. 오늘날 포매터 생성은 이전 브라우저보다 더 빠릅니다.

그러나 포매터 재사용은 여전히 모범 사례입니다. 최적화가 있더라도 포매터를 생성하는 것은 기존 포매터에서 format() 메서드를 호출하는 것보다 여전히 더 비용이 많이 듭니다. 비용 차이는 예전보다 작아졌지만 여전히 존재합니다.

고성능 코드, 루프에서 실행되는 코드, 대규모 데이터셋을 처리하는 코드에서는 포매터 재사용이 계속해서 측정 가능한 이점을 제공합니다. 포매터 생성의 최적화가 포매터 재사용의 가치를 없애지는 않습니다.

핵심 요약

Intl 포맷터를 생성하는 것은 브라우저가 옵션을 파싱하고, 로케일 협상을 수행하며, 로케일 데이터를 로드하고, 내부 데이터 구조를 구축해야 하기 때문에 비용이 많이 듭니다. 이 작업은 포맷터를 생성할 때마다 발생합니다.

포맷터 인스턴스를 재사용하면 이러한 작업의 반복을 피할 수 있습니다. 포맷터를 한 번 생성하고 format() 메서드를 여러 번 호출하면 됩니다. 이렇게 하면 포맷팅 작업에 소요되는 시간이 줄어듭니다.

포맷터 재사용은 루프, 자주 호출되는 함수, 대용량 데이터셋을 처리하는 코드에서 가장 중요합니다. 이러한 시나리오에서는 포맷터 생성 비용이 전체 실행 시간의 상당 부분을 차지할 수 있습니다.

가장 간단한 재사용 패턴은 모듈 스코프에서 포맷터를 생성하는 것입니다. 더 복잡한 시나리오의 경우 클로저나 구성 옵션에 기반한 캐싱을 사용할 수 있습니다.