Next.js(Pages Router) v16에서 상대 시간을 포맷하는 방법

타임스탬프를 '2일 전'과 같은 문구로 포맷

문제

타임스탬프를 "2일 전" 또는 "3시간 후"와 같은 상대 시간 문구로 표시하면 콘텐츠가 즉각적이고 맥락적으로 느껴집니다. 그러나 이러한 문구는 언어마다 크게 다른 복잡한 문법 패턴을 따릅니다. 영어는 과거 수량 뒤에 "ago"를 배치하고 미래 수량 앞에 "in"을 배치하지만, 다른 언어는 다른 어순을 사용하거나, 수량에 따라 시간 단위를 변화시키거나, 완전히 다른 문법 구조를 사용할 수 있습니다. 문자열 연결로 이러한 문구를 수동으로 구성하면 작성된 언어를 제외한 모든 언어에서 잘못된 출력이 생성되어 국제 사용자의 사용자 경험이 손상됩니다.

솔루션

react-intl의 상대 시간 포맷팅을 사용하여 숫자 시간 차이를 사용자의 로케일에 맞는 문법적으로 올바른 문구로 변환합니다. 타임스탬프와 현재 시간 사이의 시간 차이를 계산하고, 적절한 단위(초, 분, 시간, 일)를 결정한 다음, 두 값을 react-intl의 포맷팅 API에 전달합니다. 기본 Intl.RelativeTimeFormat API는 로케일별 문법, 어순 및 변화 규칙을 자동으로 처리합니다.

단계

1. 상대 시간 값을 계산하는 헬퍼 생성

시간 차이는 숫자 값과 적절한 단위로 변환되어야 합니다. 타임스탬프를 현재 시간과 비교하고 최적의 단위를 선택하는 함수를 작성합니다.

export function getRelativeTimeValue(date: Date | number) {
  const now = Date.now();
  const timestamp = typeof date === "number" ? date : date.getTime();
  const diffInSeconds = (timestamp - now) / 1000;

  const minute = 60;
  const hour = minute * 60;
  const day = hour * 24;
  const week = day * 7;
  const month = day * 30;
  const year = day * 365;

  const absDiff = Math.abs(diffInSeconds);

  if (absDiff < minute) {
    return { value: Math.round(diffInSeconds), unit: "second" as const };
  } else if (absDiff < hour) {
    return {
      value: Math.round(diffInSeconds / minute),
      unit: "minute" as const,
    };
  } else if (absDiff < day) {
    return { value: Math.round(diffInSeconds / hour), unit: "hour" as const };
  } else if (absDiff < week) {
    return { value: Math.round(diffInSeconds / day), unit: "day" as const };
  } else if (absDiff < month) {
    return { value: Math.round(diffInSeconds / week), unit: "week" as const };
  } else if (absDiff < year) {
    return { value: Math.round(diffInSeconds / month), unit: "month" as const };
  } else {
    return { value: Math.round(diffInSeconds / year), unit: "year" as const };
  }
}

이 함수는 부호가 있는 숫자 값(과거는 음수, 미래는 양수)과 차이의 크기에 따라 가장 적절한 단위를 반환합니다.

2. useIntl 훅으로 상대 시간 포맷

formatRelativeTime 함수는 포맷된 상대 시간 문자열을 반환하며 숫자 값, 단위 및 Intl.RelativeTimeFormatOptions를 준수하는 옵션을 필요로 합니다. 1단계의 헬퍼를 사용하여 값과 단위를 가져온 다음 포맷합니다.

import { useIntl } from "react-intl";
import { getRelativeTimeValue } from "@/lib/relative-time";

export default function CommentTimestamp({ date }: { date: Date }) {
  const intl = useIntl();
  const { value, unit } = getRelativeTimeValue(date);

  return (
    <time dateTime={date.toISOString()}>
      {intl.formatRelativeTime(value, unit)}
    </time>
  );
}

formatRelativeTime 메서드는 영어로 "2 days ago" 또는 프랑스어로 "il y a 2 jours"와 같이 로케일에 적합한 표현을 생성합니다.

3. 선언적 형식화를 위해 FormattedRelativeTime 사용

FormattedRelativeTime은 시간 경과에 따른 자동 업데이트와 같은 고급 기능을 구현하며, value, unit 및 상대 형식화 옵션에 대한 props와 함께 formatRelativeTime API를 사용합니다. 자동 업데이트가 필요한 컴포넌트의 경우 컴포넌트 형식을 사용하세요.

import { FormattedRelativeTime } from "react-intl";
import { getRelativeTimeValue } from "@/lib/relative-time";

export default function LiveTimestamp({ date }: { date: Date }) {
  const { value, unit } = getRelativeTimeValue(date);

  return (
    <time dateTime={date.toISOString()}>
      <FormattedRelativeTime
        value={value}
        unit={unit}
        numeric="auto"
        updateIntervalInSeconds={10}
      />
    </time>
  );
}

updateIntervalInSeconds prop은 컴포넌트가 다시 렌더링되는 빈도를 제어하여 시간이 지남에 따라 표시되는 표현을 최신 상태로 유지합니다.

4. 형식화 스타일 사용자 지정

컴포넌트는 numeric 및 style 속성을 포함한 RelativeTimeFormatOptions를 허용합니다. 이러한 옵션을 조정하여 출력 형식을 제어하세요.

import { FormattedRelativeTime } from "react-intl";
import { getRelativeTimeValue } from "@/lib/relative-time";

export default function CompactTimestamp({ date }: { date: Date }) {
  const { value, unit } = getRelativeTimeValue(date);

  return (
    <FormattedRelativeTime
      value={value}
      unit={unit}
      numeric="auto"
      style="narrow"
    />
  );
}

numeric="auto"를 설정하면 적절한 경우 "1 day ago" 대신 "yesterday"와 같은 표현이 생성되며, style="narrow"는 "2 months ago" 대신 "2 mo. ago"와 같이 더 짧은 형태를 생성합니다.