안녕하세요. 제니입니다!
오늘은 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 해줍니다.
공식 문서에는 이렇게 코드가 되어 있는데요.
저는 화살표 함수를 적용했습니다.
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 커스텀 적용하실 경우
공식 문서에 보면 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을 보면 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 커스텀 적용이 되었습니다.
2-3 커스텀 적용
지금부턴 calendar.css 내에서 원하는 색과 size등 적용해주시면 됩니다.
저는 디자이너의 디자인으로 맞출 거라서 위 파란색 배경색이 싫다, 라고 하신다면
이렇게 이미 정의되어 있는 곳에 색을 변경만 해주시면 됩니다.
만약 css 내에 정의하고 싶은 className이 없다면, 개발자도구 열어서 element의 className을 확인하시거나 또는
이미 적용된 css 내 하위 요소를 만들어 적용할 수 있습니다.
제가 !important를 정말 좋아하지 않는데, 하위 요소에 고정적으로 css가 적용되어 있어서 이 부분은 어쩔 수 없이,,라이브러란 편리하면서도 귀찮은 부분이 많은 것 같습니다..
2-3-1 요일 커스텀
현재까지 커스텀 된 모습인데, 디자이너분이 '일'이 없이 숫자만 표시되게 디자인을 해 두셨기 때문에
공식 문서를 참고해서 formatDay를 적용하려고 합니다.
공식 문서가 상세한 것 같으면서도 상세하지 않네요 ㅠㅠ
저렇게만 적용했을 , date type 관련한 에러가 발생될 수 있어서
<Calendar
onChange={onChange}
value={value}
formatDay={(locale, date) => date.toLocaleString('en', { day: 'numeric' })}
/>
toLocaleString으로 적용해주었습니다.
이렇게 코드를 작성하게 되면
숫자만 표시되는 것을 확인하실 수 있습니다.
위 코드처럼 작성했을 때 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를 주기 위한 경험은 꾸준히 할 수 있도록 오늘도 즐거운 코딩하세요!
'제니의 개발일지 > 도움이 되었던 것 정리' 카테고리의 다른 글
[React] TypeScript로 표 만드는 라이브러리 추천 (0) | 2023.12.01 |
---|---|
JavaScript 절댓값 변환 (0) | 2023.11.23 |
[React] styled-component 복잡한 스타일 코드 개선 (0) | 2023.09.08 |
[React] 날짜, 시간을 type int 형일 때 unix 타임 스탬프 변환하여 서버에 보내주는 방법 (0) | 2023.06.22 |
[React] store 사용하지 않고 페이지 이동할 때 정보 저장하는 법(graphQL 사용, useNavigate, useLocation 활용) (1) | 2023.06.21 |