본문 바로가기
제니의 개발일지/도움이 되었던 것 정리

[react-calendar] 프로젝트에 달력 적용하기 TypeScript, react-calendar webpack 문제 해결

by 제니운 2023. 11. 1.
728x90

 

 

 

안녕하세요. 제니입니다!

오늘은 react-calendar 를 프로젝트에 적용한 기록을 남기려고 합니다.

그동안 여러 프로젝트에 적용을 했지만, 할 때마다 미루게 됐었는데요 ..!

이번엔 잘 기록해 두려고 합니다 ㅎㅎ

 

 

 

1-1. react-calendar 라이브러리

// npm
npm install react-calendar

// yarn
yarn add react-calendar

 

저는 yarn을 사용하기 때문에 yarn add로 라이브러리를 설치해주었습니다.

 

1-2. 초기 적용

참고: https://github.com/wojtekmaj/react-calendar/blob/main/packages/react-calendar/README.md

 

react-calendar의 github을 참고하면 쉽게 적용할 수 있습니다.

 

import Calendar from 'react-calendar';

 

 

1-2-1. 설치한 Calendar을 import 해줍니다.

출처: react-calendar github 공식 문서

 

공식 문서에는 이렇게 코드가 되어 있는데요.

저는 화살표 함수를 적용했습니다.

 

 

1-2-2. Calendar 적용

import React from 'react';
import Calendar from 'react-calendar';

const CalendarSelect = () => {
  return (
    <div>
      <Calendar />
    </div>
  );
};

export default CalendarSelect;

 

1-2-3. Type 적용

type ValuePiece = Date | null;
type Value = ValuePiece | [ValuePiece, ValuePiece];

const CalendarSelect = () => {
	... 
}

export default CalendarSelect;

 

공식문서처럼 type을 추가했습니다.

이제 Calendar의 value가 되어줄 상태를 위해 useState구문을 추가합니다.

 

 

1-2-4. value state

  const [value, onChange] = useState<Value>(new Date());

 

useState라서, 버릇처럼 value, setValue로 구현하고 있었는데,

위에 1-2-3에서 정의한 Value Type을 적용하기 때문에, [value, onChange]로 작성하시면 됩니다.

저처럼 공식 문석 제대로 안 보고 value, setValue를 하고

onChange함수 별도로 구현했다가 value가 바뀌지 않는 모습을 보며 이유를 찾아가는 고생을 하지 않으시길 ㅠㅠ

구글링 하면서 나오는 많은 자료가 type이 없이 적용되고 있기 때문에, type을 잘 생각하시면 좋습니다!

 

 

1-2-5 value, onChange 적용

import React from 'react';
import Calendar from 'react-calendar';

type ValuePiece = Date | null;
type Value = ValuePiece | [ValuePiece, ValuePiece];

const CalendarSelect = () => {
  const [calendarValue, setCalendarValue] = useState<Value>(new Date());
  return (
    <div>
      <Calendar value={calendarValue}/>
    </div>
  );
};

export default CalendarSelect;

 

 

Calendar에 value를 추가해주었고 지금까지 적용된 코드를 보면 위와 같습니다.

이제 날짜가 바뀔 때마다 value 값이 변할 수 있도록 onChange 함수를 추가하면 됩니다.

 

import React from 'react';
import Calendar from 'react-calendar';

type ValuePiece = Date | null;
type Value = ValuePiece | [ValuePiece, ValuePiece];

const CalendarSelect = () => {
  const [calendarValue, setCalendarValue] = useState<Value>(new Date());
  
  const onChangeCalendar = useCallback(() => {
    setCalendarValue(calendarValue);
  }, [calendarValue]);

  return (
    <div>
      <Calendar onChange={onChangeCalendar} value={calendarValue}/>
    </div>
  );
};

export default CalendarSelect;

 

저는 onChageCalendar라는 함수명으로 추가했습니다.

 

 

2-1 커스텀 적용하실 경우

출처: react-calendar githuv 공식 문서

 

공식 문서에 보면 css를 import 하라고 되어 있습니다.

import 'react-calendar/dist/Calendar.css';

 

동일하게 import 해줍니다.

 

 

2-2 webpack 문제가 발생될 경우

* 해당 2-2는 webpack error가 발생될 경우만 참고하시고 발생되지 않는다면 2-3으로 바로 넘어가시면 됩니다!

 

이렇게 해서 대부분 적용이 되실거에요. 구글링해봐도 대부분 동일하게 적용을 하시는데,

제가 참여한 자사 서비스 프로젝트의 경우엔  webpack 구조가 구글링 해서 나오는 구조와 달랐습니다.

그래서

 

ERROR in ./node_modules/react-calendar/dist/Calendar.css 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

 

이런 에러가 계속 나왔고 react-calendar css 적용을 위해 webpack 을 변경할 수 없었기 때문에,

여러가지 시도를 해보았는데요.

 

webpack.config.dev.js

 

저희 webpack을 보면 path가 'public/assets'로 되어 있어서,

기존 calendar.css 를 보면

 

위치가 node_modules의 react-calendar 내 dist 폴더 내 css이기 때문에

적용이 되지 않는 것이었습니다.

calendar.css를 그대로 복사해서 webpack path로 되어 있는

public/assets내 css 폴더에 calendar.css를 만들어 Calendar.css의 내용을 붙여넣기 하였고

 

html내 css를 적용하는 곳에

위와 같이 코드를 추가해주었고 

css 적용된 calendar

바로 css 커스텀 적용이 되었습니다.

 

 

2-3 커스텀 적용

 

지금부턴 calendar.css 내에서 원하는 색과 size등 적용해주시면 됩니다.

저는 디자이너의 디자인으로 맞출 거라서 위 파란색 배경색이 싫다, 라고 하신다면

 

calendar.css 변경

 

이렇게 이미 정의되어 있는 곳에 색을 변경만 해주시면 됩니다.

만약 css 내에 정의하고 싶은 className이 없다면, 개발자도구 열어서 element의 className을 확인하시거나 또는

 

하위 요소 custom

이미 적용된 css 내 하위 요소를 만들어 적용할 수 있습니다.

제가 !important를 정말 좋아하지 않는데, 하위 요소에 고정적으로 css가 적용되어 있어서 이 부분은 어쩔 수 없이,,라이브러란 편리하면서도 귀찮은 부분이 많은 것 같습니다..

 

 

2-3-1 요일 커스텀

 

요일 표시

현재까지 커스텀 된 모습인데, 디자이너분이 '일'이 없이 숫자만 표시되게 디자인을 해 두셨기 때문에

 

출처: react-calendar github 공식 문서

 

공식 문서를 참고해서 formatDay를 적용하려고 합니다.

공식 문서가 상세한 것 같으면서도 상세하지 않네요 ㅠㅠ

저렇게만 적용했을 , date type 관련한 에러가 발생될 수 있어서

 

<Calendar
  onChange={onChange}
  value={value}
  formatDay={(locale, date) => date.toLocaleString('en', { day: 'numeric' })}
/>

 

toLocaleString으로 적용해주었습니다.

이렇게 코드를 작성하게 되면

 

요일 custom

숫자만 표시되는 것을 확인하실 수 있습니다.

위 코드처럼 작성했을 때 formatDay에 esLint 경고가 나올 수 있는데, 여러 방법을 찾아봐도 별도의 매개변수를 적용할 수 있는 방법이 없어서 저는 disabled 처리 했습니다.

다른 방법으로 적용하신 게 있으시다면 말씀해주세요!

 

 

2-4. 캘린더 날짜 선택 후 닫고 싶을 때

 

캘린더에 value를 바꾼 후, 캘린더를 닫고 싶었습니다.

다시 달력 아이콘을 클릭해서 끄는 건 ux에 좋지 않을 것 같았거든요.

 

2-4-1. useState

 

전 일단, calendar를 열기 위해서

 const [openCalendar, setOpenCalendar] = useState<boolean>(false);

 

useState로 관리를 해주고

  <S.CalenarSection>
    <S.DateSelect dateFlex="2">
      2023
      <S.CalendarIcon
        src={calendarImg}
        alt="store-calendar-icon"
      />
    </S.DateSelect>
    {openCalendar
    && (
      <S.CalenarContainer>
        <Calendar
          onChange={onChange}
          value={value}
          // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
          formatDay={(locale, date) => date.toLocaleString('en', { day: 'numeric' })}
        />
      </S.CalenarContainer>
    )}
  </S.CalenarSection>

 

CalendarIcon 이미지를 클릭해야 달력이 나오도록 구현했습니다.

아래 Calendar 코드는 위에 쭉 설명된 내용이 추가 되어 있습니다.

S로 스타일이 잡혀 있는 것은 제 이전 게시글 중 Styled-Component 코드를 정리하기 위해 사용했던 방법입니다.

 

 

2-4-2. 달력 아이콘 이미지 클릭 함수

  const onClickCalendarIcon = useCallback(() => {
    setOpenCalendar((prev) => !prev);
  }, []);

 

달력 아이콘 이미지를 클릭할 때, 열렸다 닫혔다를 반복할 수 있도록 함수를 구현했습니다.

  <S.CalendarIcon
    src={calendarImg}
    alt="store-calendar-icon"
    onClick={onClickCalendarIcon}
  />

 

그리고 위 코드에서 Icon에 onClick 함수를 추가해주었습니다.

 

2-4-3. 달력이 닫히도록 제어하기

위에서 달력의 value가 바뀌는 것을 onChange로 해서 쭉 내려왔습니다.

이걸 이용해서 onChange 함수를 한 개 더 만들려고 합니다.

  const handleCalendarChange = useCallback((newValue: Value) => {
    onChange(newValue);
    setOpenCalendar(false);
  }, []);

 

newValue의 타입으로 되어 있는 Value는 1-2-3 Type 적용에서 만들어두었던 Type이고

onChange는 Calendar에서 useState로 만들어 두었던 것을 사용했습니다.

그리고 setOpenCalendar는 달력을 열기 위해 사용하는 state입니다.

<Calendar
  // onChange={onChange}
  onChange={handleCalendarChange}
  value={value}
  // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
  formatDay={(locale, date) => date.toLocaleString('en', { day: 'numeric' })}
/>

 

그럼 마지막으로, 사용했던 onChange를 없애고 새로 만든 handleCalendarChange 함수를 적용해주면,

value가 선택될 때 달력이 닫히는 것을 확인하실 수 있습니다.

 

 

2-5. 달력의 바깥 영역을 클릭했을 때 닫히도록 구현하기

 

욕심은 끝이 없고,, 좋은 ui ux를 개발하기 위한 끝없는 여정!!

전 바깥 영역 클릭 시 닫히는 경험을 굉장히 좋아해서 이것까지 추가하려고 합니다.

 

 

2-5-1. 달력 영역

  const calendarRef = useRef<HTMLDivElement>(null);

 

calendarRef를 useRef를 사용해서 영역을 잡아주려고 합니다.

  <S.CalenarContainer ref={calendarRef}>
    <Calendar
      onChange={handleCalendarChange}
      value={value}
      // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
      formatDay={(locale, date) => date.toLocaleString('en', { day: 'numeric' })}
    />
  </S.CalenarContainer>

 

그리고 Calendar를 감싸고 있는 CalendarContainer에 ref를 지정해주었습니다.

 

 

2-5-2. 달력 바깥을 클릭하는 함수 만들기

 

전체 document 클릭 시 함수를 만들 거라서 useEffect 를 사용했습니다.

  useEffect(() => {
    const handleClickOutside = (event: MouseEvent | TouchEvent) => {
      if (!calendarRef.current?.contains(event.target as Node)) {
        setOpenCalendar(false);
      }
    };
  }, []);

 

웹에서도 모바일에서도 작동하도록 MouseEvent, TouchEvent를 적용했습니다.

코드를 해석하면, 내가 클릭 또는 터치한 곳이 calendarRef의 영역을 포함하지 않았을 때,

달력을 닫게 하는 코드입니다.

 

calendarRef.current가 null일 수 있으므로

  if (calendarRef.current && calendarRef.current.contains(event.target as Node)) {
    setOpenCalendar(false);
  }

 

if 문 안에 calendarRef.current && 를 추가해주었습니다.

  useEffect(() => {
    const handleClickOutside = (event: MouseEvent | TouchEvent) => {
      if (calendarRef.current && !calendarRef.current.contains(event.target as Node)) {
        setOpenCalendar(false);
      }
    };

    document.addEventListener('click', handleClickOutside);

    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, []);

 

document click의 이벤트 리스터까지 추가해주면, 

달력 바깥 영역 클릭 시 달력이 닫히는 기능이 추가됩니다.

 

 

2-5-3. 달력 아이콘 클릭 영역 제외하기

 

2-5-2까지 했을 때, 달력 아이콘을 클릭했더니 달력이 열리지 않는 현상이 생기셨을거에요.

그건 useEffect내에서 document click 이벤트리스너를 추가했기 때문에, 

달력 아이콘도 해당 영역에 들어왔기 때문입니다.

  const calendarIconRef = useRef<HTMLImageElement>(null);

 

달력 아이콘 이미지의 ref를 calendarIconRef로 만들어주고

  <S.CalendarIcon
    src={calendarImg}
    alt="store-calendar-icon"
    onClick={onClickCalendarIcon}
    ref={calendarIconRef}
  />

 

ref를 지정해줍니다.

  useEffect(() => {
    const handleClickOutside = (event: MouseEvent | TouchEvent) => {
      if (calendarRef.current && !calendarRef.current.contains(event.target as Node)
      && calendarIconRef.current && !calendarIconRef.current.contains(event.target as Node)) {
        setOpenCalendar(false);
      }
    };
    document.addEventListener('click', handleClickOutside);

    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, []);

 

위에서 작성한 useEffect내 handleClickOutside 함수에서,

calendarRef.current ~ 부분이 추가 되었습니다.

calendar와 동일하게 영역에서 캘린더 아이콘 이미지를 제외하는 코드입니다.

 

 

 

이렇게 하면 정말 모두 끝이 났습니다!

현재 정리된 내용은,

 

 

1. react-calendar를 활용한 Calendar 적용

2. Calendar의 custom

 2-1) webpack 오류 발생할 경우

3. 캘린더 닫히는 기능

 3-1) value 선택 시

 3-2) 바깥 영역 선택 시

 

 

위와 같습니다.

좋은 ui/ux를 주기 위한 경험은 꾸준히 할 수 있도록 오늘도 즐거운 코딩하세요!

 

 

 

728x90