-
[WizSched] Calendar 컴포넌트 - 날짜 이동 기능Front-End/Project 2024. 2. 28. 14:40
🍀 목차
목표
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 = () => { // 주의 첫일과 월의 첫일이 같으면 이전 달 날짜 배열은 빈 배열 리턴 if (isSameDay(firstDayOfMonth(), startOfWeek(firstDayOfMonth()))) { return []; } return eachDayOfInterval({ start: startOfWeek(firstDayOfMonth()), end: subDays(firstDayOfMonth(), 1), }); }; // currentDate 금월 첫 날 ~ 마지막 날 date들 const currMonthDates = () => { return eachDayOfInterval({ start: firstDayOfMonth(), end: lastDayOfMonth(), }); }; // currentDate 금월 마지막 날의 마지막 주에 포함된 date들을 금월 마지막 날을 포함하지 않고 반환 const nextMonthDates = () => { // 주의 말일과 월의 말일이 같으면 이전 달 날짜 배열은 빈 배열 리턴 if (isSameDay(lastDayOfMonth(), endOfWeek(lastDayOfMonth()))) { return []; } 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