TanStack Start v1에서 시간대를 처리하는 방법

사용자의 로컬 시간대로 시간 표시하기

문제

애플리케이션이 시간을 표시할 때, 이는 보는 사람의 위치에 따라 다르게 나타나는 단일 순간을 나타냅니다. 서버가 자체 시간대나 UTC로 "오후 8:00"을 렌더링하면, 다른 시간대에 있는 사용자는 잘못된 로컬 시간을 보게 되어 이벤트가 실제로 언제 발생하는지에 대한 혼란을 초래합니다. 사용자는 시간이 자동으로 로컬 시간대를 반영하기를 기대하지만, 적절한 처리 없이는 서버가 렌더링 중에 사용한 시간대로 시간을 보게 됩니다. 이러한 불일치로 인해 사용자는 약속을 놓치고, 일정을 잘못 이해하며, 애플리케이션의 신뢰성에 대한 신뢰를 잃게 됩니다.

해결 방법

날짜가 해석되고 표시되는 방식을 제어하기 위해 날짜 형식 지정 메서드에 timeZone 옵션을 전달합니다. formatDate 함수는 시간대 구성을 포함하는 Intl.DateTimeFormatOptions를 허용합니다. ISO 8601 문자열이나 Unix 타임스탬프와 같은 범용 형식으로 타임스탬프를 저장한 다음, 브라우저의 시간대가 자동으로 사용 가능한 클라이언트에서 형식을 지정합니다. Intl.DateTimeFormat API는 timeZone 옵션이 지정되지 않은 경우 사용자의 기본 시간대를 사용하여 수동 시간대 감지 없이 각 사용자의 위치에 맞게 시간이 올바르게 표시되도록 합니다.

단계

1. 시간대 인식 날짜 형식 지정 컴포넌트 생성

useIntl 훅을 사용하여 intl 객체와 해당 formatDate 메서드에 액세스합니다. 이 컴포넌트는 ISO 타임스탬프를 허용하고 사용자의 로컬 시간대에 맞게 형식을 지정합니다.

import { useIntl } from "react-intl";

interface LocalTimeProps {
  timestamp: string;
  showDate?: boolean;
  showTime?: boolean;
}

export function LocalTime({
  timestamp,
  showDate = true,
  showTime = true,
}: LocalTimeProps) {
  const intl = useIntl();
  const date = new Date(timestamp);

  const formatted = intl.formatDate(date, {
    ...(showDate && {
      year: "numeric",
      month: "short",
      day: "numeric",
    }),
    ...(showTime && {
      hour: "numeric",
      minute: "2-digit",
      timeZoneName: "short",
    }),
  });

  return <time dateTime={timestamp}>{formatted}</time>;
}

FormattedDate 컴포넌트와 formatDate 메서드는 DateTimeFormatOptions와 함께 Intl.DateTimeFormat API를 사용합니다. 명시적인 timeZone이 제공되지 않으면 브라우저가 자동으로 사용자의 시간대를 적용하여 UTC 타임스탬프를 로컬 시간으로 변환합니다.

2. 컴포넌트를 사용하여 이벤트 시간 표시하기

타임스탬프를 표시해야 하는 곳에 LocalTime 컴포넌트를 가져와서 사용하세요. 컴포넌트는 각 사용자의 브라우저 설정에 따라 시간대 변환을 자동으로 처리합니다.

import { createFileRoute } from "@tanstack/react-router";
import { LocalTime } from "~/components/LocalTime";

export const Route = createFileRoute("/events/$eventId")({
  component: EventDetail,
});

function EventDetail() {
  const event = Route.useLoaderData();

  return (
    <div>
      <h1>{event.title}</h1>
      <p>
        Starts: <LocalTime timestamp={event.startTime} />
      </p>
      <p>
        Ends: <LocalTime timestamp={event.endTime} />
      </p>
    </div>
  );
}

각 사용자는 자신의 로컬 시간대로 변환된 이벤트 시간을 확인할 수 있습니다. 서버의 ISO 타임스탬프는 사용자의 로케일 및 시간대 기본 설정에 따라 파싱되고 포맷됩니다.

3. 명시적인 시간대 표시로 시간 포맷하기

사용자가 시간이 어느 시간대를 나타내는지 알아야 할 때는 포맷 옵션에 시간대 이름을 포함하세요.

import { useIntl } from "react-intl";

interface ExplicitTimezoneProps {
  timestamp: string;
  timezone?: string;
}

export function ExplicitTimezone({
  timestamp,
  timezone,
}: ExplicitTimezoneProps) {
  const intl = useIntl();
  const date = new Date(timestamp);

  const formatted = intl.formatDate(date, {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
    timeZone: timezone,
    timeZoneName: "long",
  });

  return <time dateTime={timestamp}>{formatted}</time>;
}

timeZone 옵션을 지정하면 포매터가 해당 특정 시간대로 시간을 표시하도록 강제합니다. timezone이 undefined일 때 브라우저는 사용자의 로컬 시간대를 사용합니다. 이는 사용자의 위치와 관계없이 특정 시간대로 시간을 표시할 때 유용합니다.

4. 상대 시간 표시를 위한 헬퍼 생성하기

최근 이벤트의 경우 "2시간 전"과 같은 상대 시간을 표시하면서 툴팁에 절대 시간을 보존하세요.

import { useIntl } from "react-intl";

interface RelativeTimeProps {
  timestamp: string;
}

export function RelativeTime({ timestamp }: RelativeTimeProps) {
  const intl = useIntl();
  const date = new Date(timestamp);
  const now = Date.now();
  const diffInSeconds = Math.floor((now - date.getTime()) / 1000);

  const absoluteTime = intl.formatDate(date, {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "2-digit",
  });

  let value: number;
  let unit: Intl.RelativeTimeFormatUnit;

  if (diffInSeconds < 60) {
    value = -diffInSeconds;
    unit = "second";
  } else if (diffInSeconds < 3600) {
    value = -Math.floor(diffInSeconds / 60);
    unit = "minute";
  } else if (diffInSeconds < 86400) {
    value = -Math.floor(diffInSeconds / 3600);
    unit = "hour";
  } else {
    value = -Math.floor(diffInSeconds / 86400);
    unit = "day";
  }

  const relativeTime = intl.formatRelativeTime(value, unit);

  return (
    <time dateTime={timestamp} title={absoluteTime}>
      {relativeTime}
    </time>
  );
}

상대 시간은 사람이 이해하기 쉬운 기간을 표시하도록 업데이트되며, title 속성은 사용자가 마우스를 올렸을 때 정확한 로컬 시간을 보존합니다. 두 포맷 모두 자동으로 사용자의 시간대와 로케일을 반영합니다.