[React.js] 캘린더 제작하기(from scratch)

솔방울·2022년 11월 19일
2

React.js

목록 보기
4/6
post-custom-banner

1) react-calendar 라이브러리

팀원들과의 프로젝트 중 캘린더를 제작해야 할 일이 생겼다. 그냥 빨리 기능을 구현하기 급급했던 나는, React에서 react-calendar 라이브러리를 발견했고, 이를 적용하려고 했다. 하지만 단점은 다음과 같았다.

-1 디자이너가 '완전히' 원하는 캘린더로 customizing이 불가하다.

디자이너가 요구한 캘린더는 다음과 같다.

-1) 주차별 캘린더에 따른 데이터

-2) 월별 캘린더에 따른 데이터

다음은 react-calendar의 기본 css를 적용했을 때의 캘린더이다.

-1) react-calendar(월별)

-2) short-react-calendar(주차별)

보면 알 수 있듯이, 정말로 날짜만 제공한다. 내가 필요했던 추가적인 기능은 현재일을 기준으로 한 이번주, 이번달의 날마다 모임의 개수를 api call을 통해 받아오고 달력에 함께 보여주는 것이었다.

하지만 react-calendar는 바깥으로 보이는 css 적인 부분을 수정할 순 있지만 안에 어떤 데이터를 추가해서 불가능했다. 애초에 이 react-calendar를 만든 사람이 정리해놓은 npm 문서를 봐도, 주차의 시작이 월/일인지, 영어로 되어 있는 일별(Mon, tue,,,)를 어떻게 바꿀 것인지(일,월,화..)만 정의되어 있지 추가적인 데이터를 넣어서 받는 props는 정의되어 있지 않다.

import Calendar from "react-calendar";
import "react-calendar/dist/Calendar.css"; //기본 CSS, 적용 안하면 UI가 개구리다

...중략

<Calendar
  onChange={onChange}
  value={value}
  calendarType={"US"}
  minDetail={"month"}
  formatShortWeekday={(locale, date) =>
    ["S", "M", "T", "W", "T", "F", "S"][date.getDay()]
  }
/>

위의 formatShortWeekday에 있는 props는 위의 일별 영어(Mon,tue,Wed..) 를 한 글자로 바꾸는 것이다 (M,T,W...)
calendarType={"US"}는 주차의 시작을 일요일로 바꾸는 것이다.

그리고 css를 먹이는 작업도 힘들다. react-calendar를 깔아서 보면 모두 css 파일을 통해서 classname을 정의했다. 그러므로 다음과 같이 className에 따라서 css를 오버라이딩해서 바꿀 수 있다.

근데 진짜 골때리는 작업이다... 하나하나 모두 비교해가며 오버라이딩하는 과정이 너무 비효율적으로 느껴졌다.

하지만 장점도 있다. 진짜 그냥 순수 calendar 기능만 원하는 사람이면 설치해서 바로 적용하는 것을 추천한다.

2) 리액트에서 캘린더를 직접 구현하자

그런 저런 이유로, 나는 실력도 키울겸 캘린더를 내 마음에 맞게 쓸 수 있게 직접 만들어보기로 했다.

import React, { useState } from "react";

const [currentDay, setCurrentDay] = useState(new Date());

일단 현재일 기준이 필요하므로 useState의 기본값을 new Date()로 설정했다. new Date()는 현재일 기준의 날짜 객체 데이터를 생성하는 것이다.

자. 내가 만들어야 할 데이터는 총 2가지이다.

1) 현재일이 속해있는 주의 일~토의 데이터

예를 들어 오늘이 11월 19일이라면, 19일이 포함된 해당 주차의 일별 데이터가 필요하다. 다음과 같다.

[13,14,15,16,17,18,19] //13일이 해당 주차의 일요일, 19일이 마지막 토요일

이를 위해 필요한 기능은 다음과 같다.

const nowDay = currentDay.getDay();
const nowDate = currentDay.getDate();
const nowMonth = currentDay.getMonth();
const nowYear = currentDay.getFullYear();

Date객체에 있는 메소드에는 getDay()와 getDate()가 있는데,
getDay는 지금이 무슨 요일인지 숫자로 보여준다 (일요일=0, 토요일6)
getDate는 말 그대로 지금이 며칠인지 알려준다.
그러므로 현재일 getDate() - getDay()는 무조건 해당 주차의 시작점인 일요일의 날짜를 반환하게 된다.

ex) 현재일이 11월 18일(금)이면 nowDay는 5, nowDate는 18이다. 18-5 = 13(일)

그렇다면 nowDate-nowDay를 기준으로 for문을 6번 돌려서 1일씩 더해주면 된다.

1) 예외처리 (첫 번째 데이터가 전월인 경우)

하지만 이렇게만 되면 항상 좋겠지만 nowDate-nowDay가 0보다 작을 때도 고려해야 한다. 왜 이런 상황이 나오게 되냐면, nowDate는 1일(11월 1일)인데 nowDay(요일)은 2(목요일)이 된다면 기준일은 -1이 된다. -1일이라는 데이터는 없기 때문에, 현재일 기준 전월의 마지막날을 가져와 함께 더해줘야 한다.

ex) nowDate-nowDay = -1일 때, 10월의 마지막 날은 31일이므로, 추가해야 할 date = nowDate-nowDay + lastLastDayIndex 로 새롭게 정의된다. 즉 첫 번째 데이터인 일요일은 10월 30일이 되는 것이다.

2) 예외처리 (마지막 데이터가 다음월인 경우)

nowDate-nowDay를 1씩 더하다 보면, 해당 주차의 마지막쯤에 해당되는 데이터가 현재 월의 마지막 날보다 넘어가는 경우가 있다.

ex) 11월의 마지막은 30일 수요일, 그렇다면 마지막 데이터인 토요일은 12월 3일이여야 한다.

이럴 경우엔 현재 월 기준의 마지막 날을 빼주어야 한다.

구현한 것

//주차 데이터를 담을 공간
const justArr = []; 

// 현재일 기준의 주차 중 첫번째 날(일요일)
const firstDate = nowDate - nowDay; 

// 현재 월 기준 마지막 날의 날짜
const currentlastDayIndex = new Date(nowYear, nowMonth + 1, 0).getDate(); 

//주의해야 하는 점은, new Date()의 3번째 인자인 날짜에 0을 넣게 되면
//2번째 인자에 있는 월의 전월의 마지막 날짜를 가져오게 된다. 
//그러므로 현재 월 기준의 마지막 날짜를 가져오고 싶으면 현재 월 기준으로
//1을 더해주어야 한다.

// 전월 기준 마지막 날의 날짜
const lastLastDayIndex = new Date(nowYear, nowMonth, 0).getDate(); 

// 일요일 기준으로 1씩 6번 for문을 돌려줌
for (let i = 0; i < 7; i++) {
  let date = firstDate + i; 
  //마지막 데이터가 다음월인 경우
  if (date > currentlastDayIndex) { 
    date = date - currentlastDayIndex;
    //처음 데이터가 전월인 경우
  } else if (date <= 0) { 
    date = lastLastDayIndex + date;
  }
  justArr.push(date);
}

돌려보면 어떤 경우의 수에서도 잘 나옴을 볼 수 있다.
하지만 해당 데이터는 '일' 데이터만 받을 수 있다는 단점이 있다.

예외처리를 하지 않고 월 데이터까지 받고 싶으면 그냥 new Date 객체를 활용하면 된다.

예외처리를 고려하지 않아도 되는 방법

const justArr = [];

// 현재일 기준 주차의 첫 번째 날 (일요일)
const firstDayOfWeek = new Date(nowYear, nowMonth, nowDate - nowDay);

// 첫 번째 날의 날짜를 가져옴
const firstDate = firstDayOfWeek.getDate();
// 첫 번째 날의 월을 가져옴
const firstMonth = firstDayOfWeek.getMonth();

for (let i = 0; i < 7; i++) {
  // 1일씩 더한 새로운 날짜 객체를 만듦 
  let date = new Date(nowYear, firstMonth, firstDate + i);
  // MM-DD 의 형식으로 저장
  justArr.push(`${date.getMonth() + 1}-${date.getDate()}`);
}

//date.getMonth()에서 1을 더하는 이유는, 1월이 0부터 시작하기 때문에
//보여질 때는 제대로 1을 더해주어야 현재 월로 반영이 된다.

new Date에서는 음수 양수를 고려하여 month를 자동으로 바꿔주어서, 전월에 있는 마지막 날의 날짜가 무엇인지, 현재 월의 마지막 날짜가 무엇인지 고려하지 않아도 된다. date가 음수면은 자동으로 month가 한 달 내려가고, 해당 월 기준 마지막 날짜를 넘기면 month가 한 달 올라가기 때문이다.

즉, 결론은 연도와 월은 기준일을 사용하면 되고, 날짜만 +- 해주면 된다.

그러나 저렇게 다 바깥에 내놓으면 new Date가 바뀌는 state 변경 외에 부모에서 이벤트가 일어나거나 하면 필요도 없는 연산을 계속하게 될 것이다. 이럴 땐 useMemo와 useCallback을 이용해 연산 결과와 함수를 메모이제이션 할 수 있다.

  const [currentDay, setCurrentDay] = useState(new Date());
  
  
  // currentDay이 바뀔 때에만 작동함.
  const calendarVabs = useMemo(() => {
    const nowDay = currentDay.getDay();
    const nowDate = currentDay.getDate();
    const nowMonth = currentDay.getMonth();
    const nowYear = currentDay.getFullYear();
    return { nowDay, nowDate, nowMonth, nowYear };
  }, [currentDay]);
  
  const { nowDay, nowDate, nowMonth, nowYear } =
    calendarVabs;

    // ""
  const orgnizeWeekArr = useCallback(() => {
    const justArr = [];
    const firstDate1 = new Date(nowYear, nowMonth, nowDate - nowDay);
    const firstDateofDate = firstDate1.getDate();
    const month = firstDate1.getMonth();
    for (let i = 0; i < 7; i++) {
      let date = new Date(nowYear, month, firstDateofDate + i);
      justArr.push(`${date.getMonth() + 1}-${date.getDate()}`);
    }
    setNowArr(justArr);
  }, [currentDay]);
  

이제 날짜 데이터는 얻었으니 여러분 입맛에 변형시키면 된다...!

profile
당신이 본 큰 소나무도 원래 작은 솔방울에 불과했다.
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 11월 27일

멋있어 😊

답글 달기