🚒 캘린더를 왜 구현해요 라이브러리 쓰세요
라고 생각하기 쉽습니다만
의외로 캘린더는 쓸만한 라이브러리가 많이 없습니다.
대부분 css 커스터마이징에 제약사항이 존재하기 때문입니다.
이는 개발자가 그냥 대충 프로젝트에 갖다 쓰는 용도로는 충분히 유용하지만
디자이너가 있는 팀에서는 사용하기 어렵다는 말로도 바꿀 수 있습니다.
디자이너가 디자인에 있어 타협을 하든 개발자가 피눈물을 흘리든 둘중의 하나는 해야하기 때문입니다.
제가 속한 팀에서도 달력이 필요한 부분이 있었는데요
기존 코드의 경우 라이브러리를 갖다 쓰면서 라이브러리의 css를 모두 오버라이딩하는 식으로
커스터마이징을 수행해두었습니다.
이는 향후 달력 디자인이 바뀌었을 때 대응을 매우 어렵게한다는 문제가 있었는데요
다행히 캘린더는 구현하는게 그리 어렵지 않았습니다.
다만.. 쓸만한 코드를 구글에서 찾는 일이 쉽지 않더라구요
쓸만한 코드를 찾아 헤매느니 그냥 두뇌풀가동해서 직접 짜는게 낫겠다 싶어서
간단히 구현해봤습니다.
date-fns
캘린더 라이브러리를 안쓰겠다고했지
날짜 라이브러리를 안쓰겠다고는 하지 않았습니다.
물론 외부종속성을 혐오하시는 분이라면 직접 구현해서 쓰셔도 상관은 없지만
자바스크립트의 Date 객체는 워낙.. 기묘하고 동작을 쉽게 예상할 수 없다보니
검증된 라이브러리를 사용하는 것을 저는 선호하는 편입니다.
date-fns의 특징은 자바스크립트 빌트인 Date객체를 적극 활용하며
트리쉐이킹 / 타입스크립트를 모두 지원하고 순수함수들로 구성된 라이브러리라는 것인데요
Date 객체 조작의 문법적 불편함을 해소해준다는 점에서 저는 사용하지만
마음에 들지 않으시는 분들은 직접 구현해서 쓰셔도 상관은 없겠습니다.
크게 중요하지는 않아요
캘린더를 구현하려면 어떻게 해야할까
사실 굉장히 간단하게 구현이 되어서 그냥 코드만 봐도 충분할 듯 합니다.
아래 코드는 제가 직접 구현한 useCalendar 훅인데요
date-fns와 react 만 있으면 어디서든 재사용할 수 있습니다.
세부구현도 어렵지않으니 얼마든지 커스터마이징하셔도 좋을 것 같습니다.
원한다면 세부적인 설정을 커스텀훅의 인수로 전달해주어
더 유연하게 사용할 수 있겠지만 그건.. 여러분들이 알아서...ㅎㅎ;;
import { getDaysInMonth, subMonths } from 'date-fns';
import React from 'react';
const DATE_MONTH_FIXER = 1;
const CALENDER_LENGTH = 35;
const DEFAULT_TRASH_VALUE = 0;
const DAY_OF_WEEK = 7;
const DAY_LIST = ['일', '월', '화', '수', '목', '금', '토'];
const useCalendar = () => {
const [currentDate, setCurrentDate] = React.useState(new Date());
const totalMonthDays = getDaysInMonth(currentDate);
const prevDayList = Array.from({
length: Math.max(0, currentDate.getDay() - 1),
}).map(() => DEFAULT_TRASH_VALUE);
const currentDayList = Array.from({ length: totalMonthDays }).map(
(_, i) => i + 1,
);
const nextDayList = Array.from({
length: CALENDER_LENGTH - currentDayList.length - prevDayList.length,
}).map(() => DEFAULT_TRASH_VALUE);
const currentCalendarList = prevDayList.concat(currentDayList, nextDayList);
const weekCalendarList = currentCalendarList.reduce(
(acc: number[][], cur, idx) => {
const chunkIndex = Math.floor(idx / DAY_OF_WEEK);
if (!acc[chunkIndex]) {
acc[chunkIndex] = [];
}
acc[chunkIndex].push(cur);
return acc;
},
[],
);
return {
weekCalendarList: weekCalendarList,
currentDate: currentDate,
setCurrentDate: setCurrentDate,
};
};
export default useCalendar;
총 길이 30여줄의 간단한 캘린더 훅입니다.
타입스크립트로 작성되었지만 딱히 타입이 적극적으로 사용되진 않았으니
자바스크립트에서도 쉽게 사용하실 수 있겠습니다.
이 커스텀훅의 핵심은 달력의 시작과 끝에 적절한 트래쉬값을 붙여주고
이를 reduce를 이용하여 달력으로 렌더링하기 편한 2중배열 형태로 변환해주는 작업이라고 할 수 있습니다.
커스텀훅으로 쓰는 대신 데이트 객체를 달력으로 쓰기편한 2중배열형태로 뽑아내는 함수만 발라내어
유틸함수로 사용하시는 것도 자유일 것 같습니다.
이 캘린더훅의 반환값은 간단합니다.
useState의 반환값과 현재 위치한 날짜의 달력정보를 담고있는 2차원 배열
weekCalendarList가 반환됩니다.
이 weekCalendarList 훅의 생김새는 다음과 같은데요
number를 요소로 가지며 길이 7인 배열 5개를 튜플로 가지는 2차원 배열이 담겨있습니다.
위 반환값의 경우에는 2023년 10월에 대한 달력 결과물입니다.
차례대로 일월화수목금토 입니다.
31일 이후부터는 11월이기때문에 5주차 수요일부터는 트래쉬값 0이 들어가있는 모습입니다.
만약 달이 일요일부터 시작되지 않는 경우에도 똑같이 적용되는데요
수요일부터 달이 시작되는 2023년 11월의 달력을 예시로 보겠습니다.
이렇게 앞부분 역시 0으로 값이 채워져있는것을 볼 수 있습니다.
이를 활용하여 간단히 달력을 구현할 수 있는데요
만약 0 , 0, 0과 같은 트래쉬값 대신 이전달 / 이후달의 날짜를 가지고 오고싶다면
해당부분에 대한 구현을 약간 수정해주시면 되겠습니다.
달 넘어가기
import { subMonths } from 'date-fns';
import React from 'react';
const Cal = ({}: calProps) => {
const calendar = useCalendar();
return (
<div>
<button
onClick={() => {
calendar.setCurrentDate(subMonths(calendar.currentDate, -1));
}}
>
다음달로 이동하기
</button>
<button
onClick={() => {
calendar.setCurrentDate(subMonths(calendar.currentDate, 1));
}}
>
이전달로 이동하기
</button>
</div>
);
};
date-fns에서 제공하는 subMonths 함수를 이용하면
달력 넘기기도 쉽게 구현할 수 있는데요
subMonth의 첫번쨰 인수에 Date 객체를 넣고 두번째 인수에 1을 넣으면 이전달로
-1을 넣으면 다음달로 이동하게됩니다. 값을 얼마나 넣느냐에 따라 이동도 다르게되겠죠?
이제 이 커스텀훅을 이용하여 너무 쉽게 커스터마이징할 수 있는 달력 스타일을 만들어보겠습니다.
완성된 화면들입니다.
앞서 소개한 weekCalendarList 배열에 2중 map을 돌려주기만하면 쉽게 구현할 수 있는 ui인데요
const [select, setSelect] = React.useState<number[]>([]);
{CALENDER_RESULT_LIST.map((item) => (
<div className=" flex px-20 w-full" key={Math.random()}>
{item.map((day) => (
<button
onClick={() => {
const findItem = select.find((item) => item === day);
if (findItem) {
setSelect((state) =>
state.filter((item) => item !== day),
);
} else {
setSelect((state) => [...state, day]);
}
}}
className={`flex justify-between min-w-[calc(100%/7)] active:bg-blue-500 hover:bg-blue-400 items-center text-center ${
day === 0 ? ' invisible' : ''
}
${select.find((item) => item === day) ? ' bg-blue-600' : ''}
`}
key={Math.random()}
>
<div className=" mx-auto">{day}</div>
</button>
))}
</div>
))}
css 스타일링은 tailwindcss를 활용하였습니다.
select라는 상태를 하나 더 만들어서 포커스된 날짜는 css를 다르게 주는 로직도 추가해봤습니다.
이해를 돕기 위해 일부러 추상화를 하나도 입히지 않았고
실제로 사용하면 안되는 로직들이 들어있기 때문에
이렇게 작성하면 돌아가겠구나. 정도로만 봐주시면 되겠습니다.
마치며
엄청나게 어려운 캘린더 기능이 필요한게 아니라면
캘린더는 그냥 구현하는게 더 정신건강에 이로울 수 있습니다.
하하;;
'react' 카테고리의 다른 글
리액트 쿼리 v5 에서 바뀐 점들 체크해봐요 (2) | 2023.11.15 |
---|---|
실습과 함께 배우는 리액트 쿼리로 낙관적 업데이트 하는 법 (1) | 2023.10.15 |
리액트 타이핑효과 커스텀 훅 만들기 (0) | 2023.08.15 |
what is react server components 이..이거 뭐냐? (2) | 2023.08.07 |
리액트를 사용하는 이유는 무엇일까? (2) | 2023.08.04 |