[WizSched] Calendar 컴포넌트 - 날짜 이동 기능
🍀 목차
목표
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)을 주면 이번 달의 날짜 배열이 리턴된다.
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 (한국 표준시)
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 = () => {
return eachDayOfInterval({
start: startOfWeek(firstDayOfMonth()),
end: subDays(firstDayOfMonth(), 1),
});
};
// currentDate 금월 첫 날 ~ 마지막 날 date들
const currMonthDates = () => {
return eachDayOfInterval({
start: firstDayOfMonth(),
end: lastDayOfMonth(),
});
};
// currentDate 금월 마지막 날의 마지막 주에 포함된 date들을 금월 마지막 날을 포함하지 않고 반환
const nextMonthDates = () => {
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));
};
이렇게 연결해 주었다. MonthNavigation
으로 월을 이동시키고, DateGrids
는 date들이 렌더링 되는 컴포넌트이다.
디자인 구현
달력은 주 7일의 고정된 column을 갖고 있기에 Grid 레이아웃보다 적합한 레이아웃은 없다고 생각했다. Grid를 사용해 본 적이 없어 사용해 보고 싶기도 했다.
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-svgr
을 Client Types로 제공한다.
/// <reference types="vite-plugin-svgr/client" />
그 외에도 isToday
, isSaturday
, isSunday
를 활용한 날짜 스타일링과 Flex 레이아웃을 통한 MonthNavigation
수정을 거쳤다. 또, 이번 달에 포함된 날짜만 선택 가능한 요소임을 보여주는 cursor-pointer
을 추가해 주는 것도 잊지 말자.
위 사진에서 보이는 구글 로그인 시에만 선택 가능한 CalendarSelector
은 PR에 포함시키지 않았다.
최종 완성 GIF
- 촬영 날짜 : 2024년 2월 27일
참고자료
https://date-fns.org/docs/Getting-Started