🌱 ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [WizSched] Calendar 컴포넌트 - 날짜 이동 기능
    Front-End/Project 2024. 2. 28. 14:40
    🍀 목차
    목표
    date-fns
    기능 구현
    디자인 구현
    최종 완성 GIF

     

     

    목표

    구글 캘린더 사진
    구글 캘린더

    • 현재 월에 대한 캘린더를 보여준다.
    • 월을 이동할 수 있다.
    • 그 외 추가 디자인(오늘임을 확인할 수 있는 color 표시, 전월과 익월 날짜 스타일링)등

     

     

    date-fns

     아무런 라이브러리를 사용하지 않고 구현하고 싶었지만, 목표 기간이 있기에 Date 객체를 다루며 마주칠 에러들을 모두 방지하며 완성하기엔 무리가 있어 보여 라이브러리의 도움을 받기로 했다.

     그중에서도 date-fns는 필요한 기능만 가져올 수 있고, TypeScript 지원, 날짜 변경 시 불변성을 지키며 새로운 객체를 반환해 주는 점, 필요로 했던 기능이 모두 있던 점 등을 매력적으로 느껴 선택하게 되었다.  

    // 설치
    yarn add date-fns

     

    사용한 메서드들은 아래와 같다.

     

     

    isToday

     주어진 날짜가 오늘인지 판별한다. 오늘이면 true, 아니면 false를 리턴한다. 오늘 날짜에 스타일링을 할 때 사용했다.

    // 오늘이 2024년 2월 10일이라면 new Date(2024, 1, 10, 14, 0)(2024.2.10 14:00:00)은 오늘인가?
    
    const result = isToday(new Date(2024, 1, 10, 14, 0));
    //=> true

     

     

    isSaturday, isSunday

     각각 주어진 날짜가 토요일인지, 일요일인지 판별한다. 토요일, 일요일 날짜에 스타일링을 할 때 사용했다.

    const result = isSaturday(new Date(2014, 8, 27))
    //=> true
    const result = isSunday(new Date(2014, 8, 21))
    //=> true

     

     

    eachDayOfInterval

     지정 시간 간격 내의 날짜 배열을 리턴한다. 금월(이번 달)에 포함된 date들을 담기 위해 사용했다.

    const result = eachDayOfInterval({
      start: new Date(2014, 9, 6),
      end: new Date(2014, 9, 10)
    });
    
    //=> [
    //   Mon Oct 06 2014 00:00:00,
    //   Tue Oct 07 2014 00:00:00,
    //   Wed Oct 08 2014 00:00:00,
    //   Thu Oct 09 2014 00:00:00,
    //   Fri Oct 10 2014 00:00:00
    // ]

     

     start에는 (Date.prototype.getFullYear(), getMonth(), 1)을 주고 end에는 (Date.prototype.getFullYear(), getMonth()+1, 0)을 주면 이번 달의 날짜 배열이 리턴된다. 

     

    2024년 2월을 조회해본 결과
    2024 2월 조회

     

     

    startOfMonth

     전달받은 date의 월 첫날을 반환한다. 예를 들어 2024년 2월 X일을 전달하면 2024년 2월 1일을 리턴한다.

    startOfMonth(new Date(2024, 1, 11));
    
    // Output : Thu Feb 01 2024 00:00:00 GMT+0900 (한국 표준시)

     

    endOfMonth

     전달받은 date의 말일을 반환한다. 2024년 2월 X일을 전달하면 2024년 2월 29일을 리턴한다.

    endOfMonth(new Date(2024, 1, 11));
    
    // Output : Thu Feb 29 2024 23:59:59 GMT+0900 (한국 표준시)

     

    startOfWeek

     전달받은 date의 한 주 첫날(일요일)을 반환한다. 예를 들어 2024년 2월 1일을 전달하면 2024년 1월 28일을 리턴하는 식이다.

    startOfWeek(new Date(2024, 1, 1))
    
    // Output : Sun Jan 28 2024 00:00:00 GMT+0900 (한국 표준시)

     

    endOfWeek

     전달받은 date의 한 주 말일을 반환한다. 2024년 2월 1일을 전달하면 2024년 2월 3일이 리턴된다.

    endOfWeek(new Date(2024, 1, 1));
    
    // Output : Sat Feb 03 2024 23:59:59 GMT+0900 (한국 표준시)

     

    2월 시작주와 마지막 주의 다른 월 정보들
    2월 시작주와 마지막 주의 다른 월 정보들

     

     startOfMonth, endOfMonth, startOfWeek, endOfWeek의 경우 위 캘린더 사진의 하늘색 블록처럼 타(他) 월과 현재 월의 구분이 필요할 때 유용하게 사용할 수 있다.

     

    addMonths

     전달받은 date에 전달받은 개월 수를 더하여 반환한다. (2024년 1월 31일, 1개월)을 전달하면 2024년 2월 29일이 나온다.

    addMonths(new Date(2024, 0, 31), 1);
    
    // Output : Thu Feb 29 2024 00:00:00 GMT+0900 (한국 표준시)

     

    subMonths

     전달받은 date에 전달받은 개월 수를 빼 반환한다. (2024년 2월 29일, 1개월)을 전달하면 2024년 1월 29일이 나온다. 

    subMonths(new Date(2024, 1, 29), 1);
    
    // Output : Mon Jan 29 2024 00:00:00 GMT+0900 (한국 표준시)

     

     

     

     

    아래부터는 Date.prototype에 정의된 메서드와 동일한 기능을 하는 메서드들이다. 선택적으로 프로젝트에 적용할 수 있다.

     

    getMonth

    Date.prototype.getMonth()와 동일한 기능을 한다. 월은 0부터 시작하기에 렌더시에는 +1을 해준다.

    // Date.getMonth()
    new Date().getMonth();
    // 2월이라면 output은 1
    
    // data-fns getMonth()
    getMonth(new Date());
    // 2월이라면 output은 1

     

     

    getDay

    Date.prototype.getDay()와 동일한 기능을 한다. 일요일(0) ~ 토요일(6)까지 정수를 반환한다.

    // Date.getDay()
    new Date().getDay();
    // 화요일이라면 output 2
    
    // date-fns getDay()
    getDay(new Date());
    // 화요일이라면 output 2

     

     요일의 경우 배열의 idx와 해당 정수를 매치시키면 보다 활용도가 높아진다.

    const DAYS = ['일', '월', '화', '수', '목', '금', '토'] as const;
    
    // output : 만약 오늘이 일요일이면, '일'
    DAYS[getDay(new Date())];

     

     

    기능 구현

     

     캘린더 컴포넌트 안에 해당 로직들을 다 관리하는 건 부담이 클 것 같아 커스텀 훅으로 뺐다. 

    포함된 상태와 함수들은 아래와 같다.

     

    const [currentDate, setCurrentDate] = useState(new Date());
      
    // currentDate의 금월 첫 날 반환
    const firstDayOfMonth = () => {
        return new Date(getYear(currentDate), getMonth(currentDate), 1);
     };
    
    // currentDate의 금월 마지막 날 반환
      const lastDayOfMonth = () => {
        return new Date(getYear(currentDate), getMonth(currentDate) + 1, 0);
      };
    
    // currentDate 금월 첫 날의 첫 주에 포함된 date들을 금월 첫 날을 포함하지 않고 반환
      const prevMonthDates = () => {
        // 주의 첫일과 월의 첫일이 같으면 이전 달 날짜 배열은 빈 배열 리턴
        if (isSameDay(firstDayOfMonth(), startOfWeek(firstDayOfMonth()))) {
          return [];
        }
    
        return eachDayOfInterval({
          start: startOfWeek(firstDayOfMonth()),
          end: subDays(firstDayOfMonth(), 1),
        });
      };
    
    // currentDate 금월 첫 날 ~ 마지막 날 date들
      const currMonthDates = () => {
        return eachDayOfInterval({
          start: firstDayOfMonth(),
          end: lastDayOfMonth(),
        });
      };
    
    // currentDate 금월 마지막 날의 마지막 주에 포함된 date들을 금월 마지막 날을 포함하지 않고 반환
      const nextMonthDates = () => {
        // 주의 말일과 월의 말일이 같으면 이전 달 날짜 배열은 빈 배열 리턴
        if (isSameDay(lastDayOfMonth(), endOfWeek(lastDayOfMonth()))) {
          return [];
        }
    
        return eachDayOfInterval({
          start: addDays(lastDayOfMonth(), 1),
          end: lastDayOfWeek(lastDayOfMonth()),
        });
      };
    
    // currentDate의 전월
      const prevMonth = () => {
        setCurrentDate(subMonths(new Date(currentDate), 1));
      };
    
    // currentDate의 익월
      const nextMonth = () => {
        setCurrentDate(addMonths(new Date(currentDate), 1));
      };

     

     

    useCalendar 커스텀 훅을 사용하는 Calendar 컴포넌트
    useCalendar 커스텀 훅을 사용하는 Calendar 컴포넌트

     

    이렇게 연결해 주었다. MonthNavigation으로 월을 이동시키고, DateGrids는 date들이 렌더링 되는 컴포넌트이다.

    잘 된다!

     

     

    디자인 구현

     

      달력은 주 7일의 고정된 column을 갖고 있기에 Grid 레이아웃보다 적합한 레이아웃은 없다고 생각했다. Grid를 사용해 본 적이 없어 사용해 보고 싶기도 했다.

     

    DateGrids에 스타일을 준 사진
    이렇게 주면...!

     

    Tailwind CSS 기준으로 grid grid-cols-7을 주면 열 7의 레이아웃이 완성된다!

     

    캘린더의 틀이 잡혀가는 모습
    캘린더의 틀이 잡혀간다.

     

    이제 이전, 다음 버튼을 왼쪽 화살표, 오른쪽 화살표 즉 IconButton으로 바꿔보자.

    아이콘은 heroicons에서 찾았다. 동적 스타일링을 용이하게 하기 위해 svg파일을 리액트 컴포넌트로 변환하는 과정을 거친다.

     

    바로 변환시 오류가 뜨는 모습
    바로 변환시 오류가 뜨는 모습

     

     svgr 라이브러리를 사용했다. vite의 경우 vite-plugin-svgr을 사용하며, 아래 키워드로 설치한다.

    yarn add -D vite-plugin-svgr

     

     설치 후, vite.config.ts 파일을 수정한다.

    // vite.config.ts
    import svgr from "vite-plugin-svgr";
    
    export default {
      // ...
      plugins: [svgr()],
    };

     

     이제 SVG 파일을 리액트 컴포넌트로 가져올 수 있다.

    import Logo from "./logo.svg?react";

     

     TypeScript를 사용한다면 vite-end.d.ts에 아래 줄을 추가해 준다. 트리플-슬래시 지시어는 한 줄 주석으로 types로 패키지의 의존성을 선언한다. d.ts 파일을 직접 작성할 때만 사용하며, 컴파일러가 참조 파일을 먼저 컴파일하고 그 결과를 현재 파일과 함께 사용하게 한다. 외부 라이브러리의 타입 정보를 가져올 때도 많이 사용한다. 

     여기서도 인터페이스를 보완하는 코드 조각(Shim)으로 vite-plugin-svgrClient Types로 제공한다.

    /// <reference types="vite-plugin-svgr/client" />

     

     

    화살표들(svg 파일)이 적용된 모습

     

    추가로 진행한 디자인에 관련된 사진
    추가로 진행한 디자인

     

     

     그 외에도 isToday, isSaturday, isSunday를 활용한 날짜 스타일링과 Flex 레이아웃을 통한 MonthNavigation 수정을 거쳤다. 또, 이번 달에 포함된 날짜만 선택 가능한 요소임을 보여주는 cursor-pointer을 추가해 주는 것도 잊지 말자.

     

    완성된 사진
    완성

     

     

    위 사진에서 보이는 구글 로그인 시에만 선택 가능한 CalendarSelector은 PR에 포함시키지 않았다.

    grid가 적용된 사진
    grid

     

    최종 완성 GIF

    • 촬영 날짜 : 2024년 2월 27일

     

    캘린더를 확인해보는 gif파일

     

     

    참고자료

    https://date-fns.org/docs/Getting-Started

    https://tailwindcss.com/docs/grid-template-columns

    https://heroicons.com/

    https://www.npmjs.com/package/vite-plugin-svgr

    댓글

🍀 Y0ungZ dev blog.
스크롤 버튼