React Router v7에서 상대적 시간을 포맷하는 방법

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

문제

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

해결책

로케일을 인식하는 상대적 시간 형식을 사용하여 타임스탬프 차이를 문법적으로 올바른 표현으로 변환합니다. 주어진 타임스탬프와 현재 시점 사이의 시간 차이를 계산한 다음, 사용자의 로케일 규칙을 사용하여 해당 차이를 형식화합니다. 이렇게 하면 수동 문자열 조작 없이도 각 언어에 맞는 올바른 문법, 단어 순서 및 변형 패턴에 따라 과거 및 미래 시간 표현이 생성됩니다.

단계

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

'FormattedRelativeTime' 컴포넌트는 숫자 'value'와 'unit' prop이 필요합니다. 시간 차이를 계산하고 적절한 단위를 선택하는 헬퍼 함수를 만듭니다.

export function getRelativeTimeValue(date: Date | number) {
  const now = Date.now();
  const timestamp = typeof date === "number" ? date : date.getTime();
  const diffInSeconds = Math.round((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: 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. FormattedRelativeTime를 사용하여 상대적 시간 컴포넌트 생성하기

react-intl에서 제공하는 FormattedRelativeTime 컴포넌트를 사용하면 형식화된 상대적 시간을 렌더링하고 선택적으로 일정 간격으로 업데이트할 수 있습니다.

import { FormattedRelativeTime } from "react-intl";
import { getRelativeTimeValue } from "./getRelativeTimeValue";

interface RelativeTimeProps {
  date: Date | number;
  updateIntervalInSeconds?: number;
}

export function RelativeTime({
  date,
  updateIntervalInSeconds,
}: RelativeTimeProps) {
  const { value, unit } = getRelativeTimeValue(date);

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

numeric="auto" 옵션을 사용하면 포맷터가 적절한 경우 "1일 전" 대신 "어제"와 같은 표현을 사용할 수 있습니다. 선택적인 updateIntervalInSeconds 프롭은 상대적 시간을 최신 상태로 유지하기 위해 컴포넌트가 다시 렌더링되는 빈도를 제어합니다.

3. React Router 라우트에서 컴포넌트 사용하기

타임스탬프를 표시해야 하는 모든 라우트 컴포넌트에서 상대적 시간 컴포넌트를 렌더링합니다.

import { RelativeTime } from "./RelativeTime";

interface Post {
  id: string;
  title: string;
  content: string;
  createdAt: number;
}

export function PostDetail({ post }: { post: Post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <time dateTime={new Date(post.createdAt).toISOString()}>
        <RelativeTime date={post.createdAt} />
      </time>
      <p>{post.content}</p>
    </article>
  );
}

이 컴포넌트는 사용자의 로케일에 따라 타임스탬프를 자동으로 형식화하여 영어로는 "2 days ago", 프랑스어로는 "il y a 2 jours", 스페인어로는 "hace 2 días"와 같은 표현을 생성합니다.

4. useIntl을 사용하여 명령적으로 상대적 시간 형식화하기

형식화된 문자열이 직접 필요한 경우(예: 요소 속성 설정)에는 useIntl 훅에서 제공하는 formatRelativeTime 함수를 사용하세요.

import { useIntl } from "react-intl";
import { getRelativeTimeValue } from "./getRelativeTimeValue";

interface CommentProps {
  author: string;
  text: string;
  timestamp: number;
}

export function Comment({ author, text, timestamp }: CommentProps) {
  const intl = useIntl();
  const { value, unit } = getRelativeTimeValue(timestamp);
  const relativeTime = intl.formatRelativeTime(value, unit, {
    numeric: "auto",
  });

  return (
    <div aria-label={`Comment by ${author}, posted ${relativeTime}`}>
      <strong>{author}</strong>
      <p>{text}</p>
    </div>
  );
}

이 접근 방식은 속성에 사용하거나, 다른 텍스트와 연결하거나, 전체 로케일 지원을 유지하면서 비 React API에 전달할 수 있는 형식화된 문자열을 제공합니다.