Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] date-fns 라이브러리 제거 #469

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from

Conversation

lydiacho
Copy link
Member

Related Issue : Closes #458


🧑‍🎤 Summary

date-fns 라이브러리에서 사용하던 기능을 유틸함수로 직접 구현하여
date-fns 라이브러리를 제거하였습니다.

아래 코멘트가 굉장히 긴데 내용은 정말 별거 없고 그냥 읽기 쉬우시라고 코드도 같이 정리해둔것이니 부담없이 봐주세요!

🧑‍🎤 Screenshot


image


image

미미하지만...
tokenInstance 번들 25KB
빌드시간 약 0.5s 감소

🧑‍🎤 Comment

기존에 사용하던 모듈들은 다음과 같았어요

isAfter
import { isAfter } from 'date-fns/isAfter';
const beforeRecruiting = isAfter(new Date(applicationStart || ''), new Date());
isBefore
import { isBefore } from 'date-fns/isBefore';
const afterRecruiting = isBefore(soptApplyAccessTokenExpiredTime, new Date());
differenceInSeconds
import { differenceInSeconds } from 'date-fns/differenceInSeconds';
const diffInSeconds = differenceInSeconds(ExpiryTime, now);
subMinutes, format, locale/ko
const formattedApplicationEnd = format(subMinutes(new Date(applicationEnd || ''), 1), 'M월 dd일 (E) aaa HH시 mm분', {
  locale: ko,
});

변환해보자!

일단 기본적으로 고려한건 기존에 라이브러리를 사용하던 코드의 형태를 최대한 수정하지 않도록
date-fns에서 정의하는 타입 형태를 거의 그대로 가져왔어요

0️⃣ Date 형변환
기존 코드들을 살펴보니
Date 받아야할 곳에

const afterRecruiting = isBefore(soptApplyAccessTokenExpiredTime, new Date());

서버에서 받은 UTC 문자열을 바로 넣어줄 때도 있고

const beforeRecruiting = isAfter(new Date(applicationStart || ''), new Date());

이렇게 문자열 / 빈문자열을 Date 타입으로 변환해서 넣어줄 때도 있더라고요

기존에 date-fns는 모든 date prop을 string | number | Date 로 받아서 Date가 아니어도 알아서 변환을 해줬죠
저도 그 부분을 추가했습니다.
number는 넣어주는 경우는 없으니까 제외하고, string이 들어왔을 땐 별도로 형변환해주었어요 .

참고로 객체타입 아니고 문자열이니 typeof로 검사해줬어요

if (typeof date === 'string') date = new Date(date);
if (typeof dateToCompare === 'string') dateToCompare = new Date(dateToCompare);

1️⃣ isAfter, isBefore

이 친구들은 간단했어요.

기능 : date이 dateToCompare보다 이후인지 / 이전인지
타입 (출처 : 공식문서) :

function isBefore(
  date: string | number | Date,  // number 생략
  dateToCompare: string | number | Date  // number 생략
): boolean

Date.getTime() 을 사용해서 밀리초로 변환 후 차이 값 활용

export const isBefore = (date: Date | string, dateToCompare: Date | string): boolean => {
  if (typeof date === 'string') date = new Date(date);
  if (typeof dateToCompare === 'string') dateToCompare = new Date(dateToCompare);
  return date.getTime() < dateToCompare.getTime();
};

2️⃣ differenceInSeconds

기능 : 두 일시 간격이 몇 초인지 반환
타입 :

function differenceInSeconds(
  laterDate: string | number | Date, // number 생략
  earlierDate: string | number | Date, // number 생략
  options?: DifferenceInSecondsOptions // option 안써서 생략
): number

Date.getTime()을 사용해서 밀리초 차이값을 구한 후, 1000 나눠서 초 단위로 변환

이때 소수점 없애야 하는데 반올림 아니고 버림인 것 확인하고 Math.floor 사용

export const differenceInSeconds = (laterDate: Date | string, earlierDate: Date | string): number => {
  if (typeof laterDate === 'string') laterDate = new Date(laterDate);
  if (typeof earlierDate === 'string') earlierDate = new Date(earlierDate);
  return Math.floor((laterDate.getTime() - earlierDate.getTime()) / 1000);
};

3️⃣ subMinutes

기능 : 입력된 날짜에서 특정 수 (분)만큼 빼기
타입 :

function subMinutes(
  date: DateArg<DateType>, // Date | string 으로 수정
  amount: number,
  options?: SubMinutesOptions<ResultDate> // option 안써서 생략
): ResultDate // Date로 수정

Date.getTime()으로 밀리초 변환 → 변환한 값 - amount * 60 * 1000 → 결괏값 setTime()으로 다시 Date 타입으로 변환

export const subMinutes = (date: Date | string, amount: number): Date => {
  if (typeof date === 'string') date = new Date(date);
  const newDate = new Date();
  newDate.setTime(date.getTime() - amount * 60 * 1000);
  return newDate;
};

4️⃣ format, locale/ko
타입 :

function format(
  date: string | number | Date,  // number 생략
  formatStr: string,
  options?: FormatOptions // options 삭제 
): string

요 format 함수 변환 작업을 하면서
언석님이 추천해주셨언 Intl.DateTimeFormat을 활용해보고자 했어요.
그런데 결론부터 말하자면 최선의 선택은 아닌 것 같아서 그냥 직접 파싱함수 구현해주었습니다.

Intl.DateTimeFormat 시도 과정

일단 코드 내에서 쓰고있는 format은 아래와 같았어요

  • M월 dd일 (E) aaa HH시 mm분
  • M월 dd일 (E)
  • M월 dd일 EEEE
  • M월 dd일

date-fns의 format을 사용하면 LDML Date Field Symbol Table 기준에 맞춰 아래와 같이 format 문자열을 넣어주면 잘 변환이 되어요.

const formattedApplicationStart = format(..., 'M월 dd일 (E) aaa HH시 mm분');

그런데 Intl.DateTimeFormat은 LDML을 기반으로 하지 않기 때문에 완전히 다른 형태로 format을 설정해줘야 해요.
거두절미하고 Intl.DateTimeFormat을 활용한 유틸함수도 일단 만들어보았는데요

interface formatIntlOptions {
  year: 'numeric' | '2-digit';
  month: 'numeric' | 'long';
  day: 'numeric' | '2-digit';
  weekday: 'long' | 'short' | 'narrow';
  // 오전 오후 표시할 수 있는 건 없네...
  hour: 'numeric' | '2-digit';
  minute: 'numeric' | '2-digit';
}

export const _formatIntl = (date: Date, options: Partial<formatIntlOptions>): string => {
  return new Intl.DateTimeFormat('ko-KR', { ...options, timeZone: 'Asia/Seoul' }).format(date);
};

유틸함수 이렇게 구현하고 아래와 같이 테스트용 코드를 작성해서 output을 출력해봤어요 (format라브는 date-fns 쓴거, format유틸은 제가만든 유틸함수 쓴거, formatIntl이 DateTimeFormat 쓴거!)

const format라브 = format(new Date('2024-06-20T15:27:45.000Z'), 'M월 dd일 (E) aaa HH시 mm분', { locale: ko });
const format유틸 = _format(new Date('2024-06-20T15:27:45.000Z'), 'M월 dd일 (E) aaa HH시 mm분');
const formatIntl = _formatIntl(new Date('2024-06-20T15:27:45.000Z'), {
  month: 'long',
  day: '2-digit',
  weekday: 'short',
  hour: '2-digit',
  minute: '2-digit',
});
console.log('🚀format by lib: ', format라브);
console.log('🚀format by util: ', format유틸);
console.log('🚀format by Intl: ', formatIntl);

image

결과는 이랬어요

직접 만들어서 써보고 느낀 Intl 메소드의 한계점

  • 우리가 사용하는 format이 곳곳마다 조금씩 다른데, 형태가 한가지가 아닌 경우엔 사용하는 곳에서 (위에처럼) 매번 options prop을 직접 넣어줘야함.
  • 그나마 편하게 선택할 수 있게 타입추론 가능하도록 interface를 만들었지만, options prop 자체에도 한계점이 있음
    • 오전 오후 표시 기능 없어서 따로 추가해야 함
    • 시 / 분의 단위도 따로 붙여줘야 함

따라서 굳이 이걸 복잡하게 활용하기보다
기존에 저희가 쓰던 date-fns의 LDML 형태의 format 사용을 유지하되, 저희가 사용할만한 옵션만 따로 빼서 간단히 파서를 만들어주는게 훨씬 쉽겠다 싶었어요!

그래서 저희가 쓰고있는/쓸만한 기호만 추출해서
숫자로 변환해주고 + 기호 외의 글자는 그대로 유지되도록 정규식 replace 메소드 써서 함수 만들어줬어요

format함수
export const format = (date: Date | string, formatStr: string): string => {
  if (typeof date === 'string') date = new Date(date);
  const days = ['월', '화', '수', '목', '금', '토', '일'];
  const formatter: { [key: string]: string } = {
    M: (date.getMonth() + 1).toString(),
    dd: date.getDate().toString().padStart(2, '0'),
    E: days[date.getDay() - 1],
    EEEE: days[date.getDay() - 1] + '요일',
    aaa: date.getHours() < 12 ? '오전' : '오후',
    HH: date.getHours().toString().padStart(2, '0'),
    hh: (date.getHours() % 12 || 12).toString().padStart(2, '0'),
    mm: date.getMinutes().toString().padStart(2, '0'),
  };

  return formatStr.replace(/M|dd|E|EEE|aaa|HH|hh|mm/g, (substr) => formatter[substr]);
};

date-fns 라브 코드도 열어봤는데 훨씬 복잡하지만 대충 이런 맥락으로 구현되어있더라구요

간단한 테스트 완료

저는 로컬에서 서버를 띄우는걸 포기했어요
그래서 이 유틸함수를 어떻게 간단하게 테스트해보면 좋을까... 하다가
postman에서 저희가 받는 UTC 문자열 하나 따와서 sampleDate로 넣어놓고,
date-fns 메소드를 먹였을 때의 Output과 제 유틸함수를 적용시킨 output을 일치 연산자로 비교해서 체크해줬어요

아무래도 테스트케이스가 부족하겠지만... 추후 서버 복구되면 QA 한번 돌려보면 될 것 같아요

테스트용 임시 함수
const dateTest = () => {
  const sampleDate = '2024-06-20T15:27:45.000Z';

  const isBefore라브 = isBefore(sampleDate, new Date(1987, 1, 11));
  const isBefore유틸 = _isBefore(sampleDate, new Date(1987, 1, 11));
  console.log('🚀isBefore Test:', isBefore라브 === isBefore유틸);

  const isAfter라브 = isAfter(sampleDate, new Date(1987, 1, 11));
  const isAfter유틸 = _isAfter(sampleDate, new Date(1987, 1, 11));
  console.log('🚀isAfter Test:', isAfter라브 === isAfter유틸);

  const dIS라브 = differenceInSeconds(sampleDate, new Date(2014, 6, 2, 12, 30, 7, 999));
  const dIS유틸 = _differenceInSeconds(sampleDate, new Date(2014, 6, 2, 12, 30, 7, 999));
  console.log('🚀differenceInSeconds Test:', dIS라브 === dIS유틸);

  const subMinutes라브 = subMinutes(new Date(sampleDate), 30);
  const subMinutes유틸 = _subMinutes(new Date(sampleDate), 30);
  console.log('🚀subMinutes Test:', subMinutes라브.getTime() === subMinutes유틸.getTime());

  const format라브 = format(sampleDate, 'M월 dd일 (E) aaa HH시 mm분', { locale: ko });
  const format유틸 = _format(sampleDate, 'M월 dd일 (E) aaa HH시 mm분');
  console.log('🚀format Test:', format유틸 === format라브);
};

image

첨언

UTC든 뭐든 Date 객체로 감사면 시스템에 맞춰 한국 표준 시간대로 자동으로 변환이 되더라고요. 저희는 이제 문자로 prop 받아도 무조건 다 Date로 변환시켜주는 방식을 사용하니까 locale 관련되어 따로 처리는 안해줬어요. 그래도 방법은 알아두었으니 혹시 모를 케이스를 대비해 추가하면 좋을 것 같다 싶다면 말씀해주세요! 추가하겠습니다.

@lydiacho lydiacho self-assigned this Oct 16, 2024
Copy link

height bot commented Oct 16, 2024

Link Height tasks by mentioning a task ID in the pull request title or commit messages, or description and comments with the keyword link (e.g. "Link T-123").

💡Tip: You can also use "Close T-X" to automatically close a task when the pull request is merged.

Copy link
Member

@eonseok-jeon eonseok-jeon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다 :)

src/common/utils/dateFormatter.ts Show resolved Hide resolved
Comment on lines +2 to +3
if (typeof date === 'string') date = new Date(date);
if (typeof dateToCompare === 'string') dateToCompare = new Date(dateToCompare);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

계속 반복되니

const toDate = (date: Date | string): Date => {
  return typeof date === 'string' ? new Date(date) : date;
};

따로 분리해줘도 좋을 거 같아요

Copy link
Member

@eonseok-jeon eonseok-jeon Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가로 유효하지 않은 date 값이 들어왔을 때 예외 처리도 toDate에서 같이 해주면 좋을 거 같아요~

Copy link
Member Author

@lydiacho lydiacho Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자가 확인해야할 내용은 아니니 페이지 터뜨리지 않게 throw Error말고 console error로 처리했어요.
어떠신가요!

const toDate = (date: Date | string): Date => {
  const newDate = typeof date === 'string' ? new Date(date) : date;
  if (isNaN(newDate.getTime())) {
    console.error(`${date} is invalid date.`);
  }
  return newDate;
};

src/common/utils/dateFormatter.ts Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Refactor] date-fns 제거
2 participants