page-cover
개발
자바스크립트에서 n시간 전, n일 전 구현 방법
sooros5132-avatarsooros513211/8/2023Next.js React JavaScript
아래 방법은 다국어를 지원하고 Next.js에서의 구현 방법이다. 리액트도 사용 가능하다.

GitHub - sooros5132/notion-blog-kit: A statically generated blog using Next.js, Notion Api.

A statically generated blog using Next.js, Notion Api. - sooros5132/notion-blog-kit

GitHub - sooros5132/notion-blog-kit: A statically generated blog using Next.js, Notion Api.-faviconhttps://github.com/sooros5132/notion-blog-kit

위 저장소에 적용했던 것을 공유하고자 작성.
최신 자바스크립트에선 Intl.RelativeTimeFormat를 이용해 쉽게 구현할 수 있다.

구현

typescript
const defaultLanguage = 'en-US';

const timeFormats = [
  [60, 'seconds', 1], // 60
  [3600, 'minutes', 60], // 60*60, 60
  [86400, 'hours', 3600], // 60*60*24, 60*60
  [604800, 'days', 86400], // 60*60*24*7, 60*60*24
  [2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
  [29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
  [Infinity, 'years', 29030400] // Infinity, 60*60*24*7*4*12 -> 336
] as const;

function isValidDate(d: Date) {
  return d instanceof Date && !isNaN(d as any);
}

function relativeTimeFormat(
  date: Date,
  referenceDate: Date
): { time: number; unit: Intl.RelativeTimeFormatUnit } {
  const _seconds = (date.getTime() - referenceDate.getTime()) / 1000;
  const seconds = Math.floor(Math.abs(_seconds));
  const sign = Math.sign(_seconds);

  let i = 0,
    format;
  while ((format = timeFormats[i++])) {
    if (seconds < format[0]) {
      let distance;

      if (format[1] === 'years') {
        // 윤년 처리
        distance = date.getFullYear() - referenceDate.getFullYear();
      } else {
        distance =
          sign === 1
            ? Math.floor((seconds / format[2]) * sign)
            : Math.ceil((seconds / format[2]) * sign);
      }

      // { time: 1, unit: 'days' } 처럼 나오게 된다.
      return { time: distance, unit: format[1] };
    }
  }
  return { time: 0, unit: 'second' };
}

export type RelativeTimeFormatProps = {
	date: Date,
	numeric: Intl.RelativeTimeFormatNumeric
}

export function RelativeTimeFormat({ date, numeric }: RelativeTimeFormatProps) {
  const [isBrowser, setIsBrowser] = useState(false);
  const language = isBrowser ? navigator?.language || defaultLanguage : defaultLanguage;

  useEffect(() => setIsBrowser(true), []);

  // Date 형식이 아닌 값 예외처리
  if (!isValidDate(date)) {
    return <span suppressHydrationWarning>Invalid Date</span>;
  }

	// 브라우저가 아닌 경우
  if (!isBrowser) {
    return (
      <span suppressHydrationWarning>{new Intl.DateTimeFormat(language).format(date)}</span>
    );
  }

  const { time, unit } = relativeTimeFormat(date, new Date());

  return (
    <span suppressHydrationWarning>
      {new Intl.RelativeTimeFormat(language, {
        numeric: numeric || 'auto'
      }).format(time, unit)}
    </span>
  );
}
서버에서 빌드중인 경우엔 언어를 모르다 보니 Intl.DateTimeFormat을 이용해 MM/dd/yyyy같은 형식의 날짜를 보여주고
hydrate가 되었을 때 브라우저의 language 값을 가져와서 Intl.RelativeTimeFormat으로 1일 전, 1주일 전으로 바꿔준다. 브라우저의 navigator값을 읽다 보니 자동으로 다국어 지원이 된다.
span에 suppressHydrationWarning 이건 hydrate 중에 경고를 표시하기 때문이다.

공통 컴포넌트 (예시: <div>) – React

The library for web and native user interfaces

공통 컴포넌트 (예시: <div>) – React-faviconhttps://ko.react.dev/reference/react-dom/components/common#common-props

suppressHydrationWarning: 불리언 타입입니다. 서버 렌더링을 사용할 때, 일반적으로 서버와 클라이언트가 서로 다른 콘텐츠를 렌더링하면 경고가 표시됩니다. 일부 드문 사례(예시: 타임스탬프)에서는 정확한 일치를 보장하기가 매우 어렵거나 불가능합니다. suppressHydrationWarningtrue로 설정하면, React는 해당 엘리먼트의 어트리뷰트와 콘텐츠가 일치하지 않아도 경고를 표시하지 않습니다. 이는 한 단계의 깊이에서만 작동하며, 탈출구로 사용하기 위한 것입니다. 과도하게 사용하지 마세요. suppressing hydration 오류에 대해서 읽어보세요.

테스트 결과

2023-11-08 22:52:00 기준으로 계산되었다.
numeric 부분이 always인 경우는 아래처럼 출력된다.
plain text
0000-01-01    2,023년 전
2022-01-01    1년 전
2023-01-01    11개월 전
2023-11-07    1일 전
2023-11-08 22:22:00    30분 전
2023-11-08 22:51:59    1초 전
2023-11-08 22:52:00    0초 후
2023-11-08 22:52:01    1초 후
2023-11-08 23:59:00    1시간 후
2023-12-01    3주 후
2024-01-01    1개월 후
2024-02-01    3개월 후
2024-03-01    4개월 후
2024-04-01    5개월 후
2024-05-01    6개월 후
2024-06-01    7개월 후
2024-07-01    8개월 후
2024-08-01    9개월 후
2024-09-01    10개월 후
2024-10-01    11개월 후
2024-11-01    1년 후
2024-12-01    1년 후
2033-11-08    10년 후
3023-11-08    1,000년 후
4023-11-08    2,000년 후
5023-11-08    3,000년 후
6023-11-08    4,000년 후
7023-11-08    5,000년 후
8023-11-08    6,000년 후
9023-11-08    7,000년 후
9923-11-08    7,900년 후
9999-12-31    7,976년 후
NaN    Invalid Date
null    53년 전
undefined    Invalid Date
0000-00-00    Invalid Date
numeric 부분이 auto인 경우는 아래처럼 출력된다.
plain text
0000-01-01    2,023년 전
2022-01-01    작년
2023-01-01    11개월 전
2023-11-07    어제
2023-11-08 22:22:00    30분 전
2023-11-08 22:51:59    1초 전
2023-11-08 22:52:00    지금
2023-11-08 22:52:01    1초 후
2023-11-08 23:59:00    1시간 후
2023-12-01    3주 후
2024-01-01    다음 달
2024-02-01    3개월 후
2024-03-01    4개월 후
2024-04-01    5개월 후
2024-05-01    6개월 후
2024-06-01    7개월 후
2024-07-01    8개월 후
2024-08-01    9개월 후
2024-09-01    10개월 후
2024-10-01    11개월 후
2024-11-01    내년
2024-12-01    내년
2033-11-08    10년 후
3023-11-08    1,000년 후
4023-11-08    2,000년 후
5023-11-08    3,000년 후
6023-11-08    4,000년 후
7023-11-08    5,000년 후
8023-11-08    6,000년 후
9023-11-08    7,000년 후
9923-11-08    7,900년 후
9999-12-31    7,976년 후
NaN    Invalid Date
null    53년 전
undefined    Invalid Date
0000-00-00    Invalid Date
다국어가 잘 되는지 위치를 도쿄로 바꿔보자
모두 다 잘 된다.
null이 53년 전으로 나오는 이유는 new Date(null)을 해보면 Thu Jan 01 1970 09:00:00 GMT+0900 (한국 표준시)으로 나오게 돼서 그렇다.
실제로 사용할 땐 hover때나 자세히 보기 같은 곳에선 n일전이 아니라 MM/dd/yyyy형식의 날짜를 보여주면 UX적으로 좋으니 디테일을 챙기자.
아래는 예시로 유튜브의 영상 정보 부분을 가져왔다.