Intl.RelativeTimeFormat API

완전한 국제화 지원으로 JavaScript에서 상대 시간 문자열 형식 지정

소개

"3시간 전" 또는 "2일 후"와 같은 상대 타임스탬프 표시는 웹 애플리케이션에서 일반적인 요구 사항입니다. 소셜 미디어 피드, 댓글 섹션, 알림 시스템 및 활동 로그는 모두 콘텐츠가 오래될수록 업데이트되는 사람이 읽을 수 있는 시간 표시가 필요합니다.

이 기능을 처음부터 구축하는 것은 어려움을 제시합니다. 시간 차이를 계산하고, 적절한 단위를 선택하고, 언어 전반에 걸쳐 복수형 규칙을 처리하고, 지원되는 모든 로케일에 대한 번역을 유지해야 합니다. 이러한 복잡성은 개발자가 전통적으로 Moment.js와 같은 라이브러리를 사용하여 간단한 형식 지정처럼 보이는 것에 상당한 번들 크기를 추가한 이유를 설명합니다.

Intl.RelativeTimeFormat API는 네이티브 솔루션을 제공합니다. 완전한 국제화 지원으로 상대 시간 문자열을 포맷하며, 복수형 규칙과 문화적 관습을 자동으로 처리합니다. 이 API는 전 세계적으로 95%의 커버리지를 가진 모든 주요 브라우저에서 작동하며, 외부 종속성 없이 수십 개 언어로 자연스러운 출력을 생성합니다.

기본 사용법

Intl.RelativeTimeFormat 생성자는 숫자 값과 시간 단위를 현지화된 문자열로 변환하는 포매터 인스턴스를 생성합니다.

const rtf = new Intl.RelativeTimeFormat('en');

console.log(rtf.format(-1, 'day'));
// "1 day ago"

console.log(rtf.format(2, 'hour'));
// "in 2 hours"

console.log(rtf.format(-3, 'month'));
// "3 months ago"

format() 메서드는 두 개의 매개변수를 받습니다:

  • value: 시간의 양을 나타내는 숫자
  • unit: 시간 단위를 지정하는 문자열

음수 값은 과거 시간을 나타내고, 양수 값은 미래 시간을 나타냅니다. API는 자동으로 복수형을 처리하여 값에 따라 "1일 전" 또는 "2일 전"을 생성합니다.

지원되는 시간 단위

API는 8개의 시간 단위를 지원하며, 각각 단수형과 복수형을 모두 허용합니다:

const rtf = new Intl.RelativeTimeFormat('en');

// These produce identical output
console.log(rtf.format(-5, 'second'));
// "5 seconds ago"

console.log(rtf.format(-5, 'seconds'));
// "5 seconds ago"

가장 작은 것부터 가장 큰 것까지 사용 가능한 단위:

  • second 또는 seconds
  • minute 또는 minutes
  • hour 또는 hours
  • day 또는 days
  • week 또는 weeks
  • month 또는 months
  • quarter 또는 quarters
  • year 또는 years

분기 단위는 회계 기간을 추적하는 비즈니스 애플리케이션에서 유용하며, 다른 단위들은 일반적인 상대 시간 형식 지정 요구사항을 충족합니다.

자연어 출력

numeric 옵션은 포매터가 숫자 값을 사용할지 자연어 대안을 사용할지 제어합니다.

const rtfNumeric = new Intl.RelativeTimeFormat('en', {
  numeric: 'always'
});

console.log(rtfNumeric.format(-1, 'day'));
// "1 day ago"

console.log(rtfNumeric.format(0, 'day'));
// "in 0 days"

console.log(rtfNumeric.format(1, 'day'));
// "in 1 day"

numericauto로 설정하면 일반적인 값에 대해 더 관용적인 표현을 생성합니다:

const rtfAuto = new Intl.RelativeTimeFormat('en', {
  numeric: 'auto'
});

console.log(rtfAuto.format(-1, 'day'));
// "yesterday"

console.log(rtfAuto.format(0, 'day'));
// "today"

console.log(rtfAuto.format(1, 'day'));
// "tomorrow"

이 자연어 출력은 더 대화적인 인터페이스를 만듭니다. auto 옵션은 모든 시간 단위에서 작동하지만, 일(day) 단위에서 효과가 가장 두드러집니다. 다른 언어들은 API가 자동으로 처리하는 고유한 관용적 대안을 가지고 있습니다.

형식 스타일

style 옵션은 다양한 인터페이스 컨텍스트에 맞게 출력 상세도를 조정합니다.

const rtfLong = new Intl.RelativeTimeFormat('en', {
  style: 'long'
});

console.log(rtfLong.format(-2, 'hour'));
// "2 hours ago"

const rtfShort = new Intl.RelativeTimeFormat('en', {
  style: 'short'
});

console.log(rtfShort.format(-2, 'hour'));
// "2 hr. ago"

const rtfNarrow = new Intl.RelativeTimeFormat('en', {
  style: 'narrow'
});

console.log(rtfNarrow.format(-2, 'hour'));
// "2h ago"

가독성이 가장 중요한 표준 인터페이스에는 long 스타일(기본값)을 사용하세요. 모바일 인터페이스나 데이터 테이블과 같이 공간이 제한된 레이아웃에는 short 스타일을 사용하세요. 모든 문자가 중요한 극도로 컴팩트한 디스플레이에는 narrow 스타일을 사용하세요.

시간 차이 계산

Intl.RelativeTimeFormat API는 값을 포맷하지만 계산하지는 않습니다. 시간 차이를 계산하고 적절한 단위를 직접 선택해야 합니다. 이러한 관심사의 분리를 통해 계산 로직을 제어하면서 포맷 복잡성은 API에 위임할 수 있습니다.

기본 시간 차이 계산

특정 시간 단위에 대해 두 날짜 간의 차이를 계산합니다.

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

function formatDaysAgo(date) {
  const now = new Date();
  const diffInMs = date - now;
  const diffInDays = Math.round(diffInMs / (1000 * 60 * 60 * 24));

  return rtf.format(diffInDays, 'day');
}

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

console.log(formatDaysAgo(yesterday));
// "yesterday"

const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);

console.log(formatDaysAgo(nextWeek));
// "in 7 days"

이 접근 방식은 사용 사례에 적합한 단위를 알고 있을 때 작동합니다. 댓글 타임스탬프는 항상 시간이나 일을 사용할 수 있고, 이벤트 일정은 일이나 주에 초점을 맞출 수 있습니다.

자동 단위 선택

범용 상대 시간 형식 지정의 경우, 시간 차이 크기에 따라 가장 적절한 단위를 선택합니다.

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

const units = {
  year: 24 * 60 * 60 * 1000 * 365,
  month: 24 * 60 * 60 * 1000 * 365 / 12,
  week: 24 * 60 * 60 * 1000 * 7,
  day: 24 * 60 * 60 * 1000,
  hour: 60 * 60 * 1000,
  minute: 60 * 1000,
  second: 1000
};

function formatRelativeTime(date) {
  const now = new Date();
  const diffInMs = date - now;
  const absDiff = Math.abs(diffInMs);

  for (const [unit, msValue] of Object.entries(units)) {
    if (absDiff >= msValue || unit === 'second') {
      const value = Math.round(diffInMs / msValue);
      return rtf.format(value, unit);
    }
  }
}

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatRelativeTime(fiveMinutesAgo));
// "5 minutes ago"

const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(threeDaysAgo));
// "3 days ago"

이 구현은 가장 큰 단위부터 가장 작은 단위까지 반복하여 시간 차이가 단위의 밀리초 값을 초과하는 첫 번째 단위를 선택합니다. 초 단위로의 폴백은 함수가 항상 결과를 반환하도록 보장합니다.

단위 임계값은 근사값을 사용합니다. 월은 가변적인 월 길이를 고려하지 않고 1년의 1/12로 계산됩니다. 이러한 근사치는 정밀도보다 가독성이 더 중요한 상대 시간 표시에 적합합니다.

국제화 지원

포매터는 상대 시간 표시에 대한 로케일별 규칙을 준수합니다. 언어마다 복수형 규칙, 어순, 관용적 표현이 다릅니다.

const rtfEnglish = new Intl.RelativeTimeFormat('en', {
  numeric: 'auto'
});

console.log(rtfEnglish.format(-1, 'day'));
// "yesterday"

const rtfSpanish = new Intl.RelativeTimeFormat('es', {
  numeric: 'auto'
});

console.log(rtfSpanish.format(-1, 'day'));
// "ayer"

const rtfJapanese = new Intl.RelativeTimeFormat('ja', {
  numeric: 'auto'
});

console.log(rtfJapanese.format(-1, 'day'));
// "昨日"

복수형 규칙은 언어마다 크게 다릅니다. 영어는 단수와 복수를 구분합니다(1 day vs 2 days). 아랍어는 개수에 따라 6가지 복수형을 사용합니다. 일본어는 수량에 관계없이 동일한 형태를 사용합니다. API는 이러한 복잡성을 자동으로 처리합니다.

const rtfArabic = new Intl.RelativeTimeFormat('ar');

console.log(rtfArabic.format(-1, 'day'));
// "قبل يوم واحد"

console.log(rtfArabic.format(-2, 'day'));
// "قبل يومين"

console.log(rtfArabic.format(-3, 'day'));
// "قبل 3 أيام"

console.log(rtfArabic.format(-11, 'day'));
// "قبل 11 يومًا"

포매터는 또한 오른쪽에서 왼쪽으로 쓰는 언어의 텍스트 방향을 처리하고 문화적으로 적절한 형식 규칙을 적용합니다. 이러한 자동 현지화는 번역 파일을 유지 관리하거나 사용자 정의 복수형 로직을 구현할 필요를 없애줍니다.

formatToParts를 사용한 고급 형식 지정

formatToParts() 메서드는 포맷된 문자열을 객체 배열로 반환하여 개별 구성 요소의 사용자 정의 스타일링이나 조작을 가능하게 합니다.

const rtf = new Intl.RelativeTimeFormat('en');

const parts = rtf.formatToParts(-5, 'second');

console.log(parts);
// [
//   { type: 'integer', value: '5', unit: 'second' },
//   { type: 'literal', value: ' seconds ago' }
// ]

각 part 객체는 다음을 포함합니다:

  • type: 숫자 값의 경우 integer, 텍스트의 경우 literal
  • value: 이 부분의 문자열 콘텐츠
  • unit: 시간 단위(정수 부분에 존재)

이 구조는 숫자를 텍스트와 다르게 스타일 지정하거나 표시를 위해 특정 구성 요소를 추출하려는 경우 사용자 정의 렌더링을 가능하게 합니다:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

function formatWithStyledNumber(value, unit) {
  const parts = rtf.formatToParts(value, unit);

  return parts.map(part => {
    if (part.type === 'integer') {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  }).join('');
}

console.log(formatWithStyledNumber(-5, 'hour'));
// "<strong>5</strong> hours ago"

자연어 대안이 있는 값과 함께 numeric: 'auto'를 사용할 때, formatToParts()는 단일 리터럴 부분을 반환합니다:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

const parts = rtf.formatToParts(-1, 'day');

console.log(parts);
// [
//   { type: 'literal', value: 'yesterday' }
// ]

이 동작을 통해 자연어가 사용되는 경우와 숫자 형식이 사용되는 경우를 감지하여 출력 유형에 따라 다른 스타일이나 동작을 적용할 수 있습니다.

성능 최적화

Intl.RelativeTimeFormat 인스턴스를 생성하려면 로케일 데이터를 로드하고 포맷 규칙을 초기화해야 합니다. 이 작업은 불필요하게 반복하지 않을 만큼 비용이 많이 듭니다.

포매터 인스턴스 캐싱

포매터를 한 번 생성하고 재사용하세요:

const formatterCache = new Map();

function getFormatter(locale, options = {}) {
  const cacheKey = `${locale}-${JSON.stringify(options)}`;

  if (!formatterCache.has(cacheKey)) {
    formatterCache.set(
      cacheKey,
      new Intl.RelativeTimeFormat(locale, options)
    );
  }

  return formatterCache.get(cacheKey);
}

// Reuse cached formatters
const rtf = getFormatter('en', { numeric: 'auto' });
console.log(rtf.format(-1, 'day'));
// "yesterday"

이 캐싱 전략은 활동 피드나 댓글 스레드 렌더링과 같이 많은 타임스탬프를 포맷팅할 때 중요해집니다.

계산 오버헤드 최소화

상대 시간을 반복적으로 계산하는 대신 타임스탬프를 저장하세요:

// Store the creation date
const comment = {
  text: "Great article!",
  createdAt: new Date('2025-10-14T10:30:00Z')
};

// Calculate relative time only when rendering
function renderComment(comment, locale) {
  const rtf = getFormatter(locale, { numeric: 'auto' });
  const units = {
    day: 24 * 60 * 60 * 1000,
    hour: 60 * 60 * 1000,
    minute: 60 * 1000,
    second: 1000
  };

  const diffInMs = comment.createdAt - new Date();
  const absDiff = Math.abs(diffInMs);

  for (const [unit, msValue] of Object.entries(units)) {
    if (absDiff >= msValue || unit === 'second') {
      const value = Math.round(diffInMs / msValue);
      return rtf.format(value, unit);
    }
  }
}

이 접근 방식은 데이터 저장과 표현을 분리하여, 기본 데이터를 수정하지 않고도 사용자의 로케일이 변경되거나 렌더링이 업데이트될 때 상대 시간을 다시 계산할 수 있게 합니다.

실제 구현

계산 로직과 포맷팅을 결합하면 프로덕션 애플리케이션에 적합한 재사용 가능한 유틸리티 함수가 생성됩니다:

class RelativeTimeFormatter {
  constructor(locale = 'en', options = { numeric: 'auto' }) {
    this.formatter = new Intl.RelativeTimeFormat(locale, options);

    this.units = [
      { name: 'year', ms: 24 * 60 * 60 * 1000 * 365 },
      { name: 'month', ms: 24 * 60 * 60 * 1000 * 365 / 12 },
      { name: 'week', ms: 24 * 60 * 60 * 1000 * 7 },
      { name: 'day', ms: 24 * 60 * 60 * 1000 },
      { name: 'hour', ms: 60 * 60 * 1000 },
      { name: 'minute', ms: 60 * 1000 },
      { name: 'second', ms: 1000 }
    ];
  }

  format(date) {
    const now = new Date();
    const diffInMs = date - now;
    const absDiff = Math.abs(diffInMs);

    for (const unit of this.units) {
      if (absDiff >= unit.ms || unit.name === 'second') {
        const value = Math.round(diffInMs / unit.ms);
        return this.formatter.format(value, unit.name);
      }
    }
  }
}

// Usage
const formatter = new RelativeTimeFormatter('en');

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatter.format(fiveMinutesAgo));
// "5 minutes ago"

const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
console.log(formatter.format(tomorrow));
// "tomorrow"

이 클래스는 포매터와 단위 선택 로직을 모두 캡슐화하여 Date 객체를 받아 포맷된 문자열을 반환하는 깔끔한 인터페이스를 제공합니다.

프레임워크와의 통합

React 애플리케이션에서는 포매터를 한 번 생성하고 컨텍스트나 props를 통해 전달하세요:

import { createContext, useContext } from 'react';

const RelativeTimeContext = createContext(null);

export function RelativeTimeProvider({ locale, children }) {
  const formatter = new RelativeTimeFormatter(locale);

  return (
    <RelativeTimeContext.Provider value={formatter}>
      {children}
    </RelativeTimeContext.Provider>
  );
}

export function useRelativeTime() {
  const formatter = useContext(RelativeTimeContext);
  if (!formatter) {
    throw new Error('useRelativeTime must be used within RelativeTimeProvider');
  }
  return formatter;
}

// Component usage
function CommentTimestamp({ date }) {
  const formatter = useRelativeTime();
  return <time>{formatter.format(date)}</time>;
}

이 패턴은 포매터가 로케일당 한 번 생성되고 상대 시간 포맷팅이 필요한 모든 컴포넌트에서 공유되도록 보장합니다.

브라우저 지원

Intl.RelativeTimeFormat는 전 세계적으로 95%의 커버리지를 가진 모든 최신 브라우저에서 작동합니다:

  • Chrome 71+
  • Firefox 65+
  • Safari 14+
  • Edge 79+

Internet Explorer는 이 API를 지원하지 않습니다. IE 지원이 필요한 애플리케이션의 경우 폴리필을 사용할 수 있지만, 네이티브 구현이 더 나은 성능과 더 작은 번들 크기를 제공합니다.

이 API를 사용해야 하는 경우

Intl.RelativeTimeFormat는 다음과 같은 경우에 가장 적합합니다:

  • 피드 및 타임라인에서 콘텐츠 경과 시간 표시
  • 댓글 또는 게시물 타임스탬프 표시
  • 현재 시간을 기준으로 이벤트 일정 포맷팅
  • 상대 타임스탬프를 사용하는 알림 시스템 구축
  • 사람이 읽을 수 있는 시간으로 활동 로그 생성

API는 다음 용도에 적합하지 않습니다:

  • 절대 날짜 및 시간 포맷(Intl.DateTimeFormat 사용)
  • 정확한 밀리초 정확도가 필요한 정밀 시간 추적
  • 매초 업데이트되는 카운트다운 타이머
  • 날짜 산술 또는 달력 계산

상대 시간과 절대 시간 표시가 모두 필요한 애플리케이션의 경우 Intl.RelativeTimeFormatIntl.DateTimeFormat를 결합하세요. 최근 콘텐츠에는 상대 시간을 표시하고 오래된 콘텐츠에는 절대 날짜로 전환합니다:

function formatTimestamp(date, locale = 'en') {
  const now = new Date();
  const diffInMs = Math.abs(date - now);
  const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;

  if (diffInMs < sevenDaysInMs) {
    const rtf = new RelativeTimeFormatter(locale);
    return rtf.format(date);
  } else {
    const dtf = new Intl.DateTimeFormat(locale, {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    });
    return dtf.format(date);
  }
}

const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
console.log(formatTimestamp(yesterday));
// "yesterday"

const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
console.log(formatTimestamp(lastMonth));
// "Sep 14, 2025"

이 하이브리드 접근 방식은 최근 콘텐츠에 대해 상대 시간의 자연어 이점을 제공하면서 오래된 타임스탬프에 대해서는 명확성을 유지합니다.