DatePicker 디자인
위 사진은 현재 진행중인 사이드 프로젝트의 디자인시스템 입니다.
위 사진은 디자인 시스템에 있는 datepicker 가 사용되고있는 디자인 시안입니다. 해당 디자인은 기존에 나와있는 캘린더 라이브로리로 구현하기에 어려움이 있었습니다. 따라서 직접 Datepicer 캘린더를 구현하기로 했습니다.
Datepicker 날짜 구하기
먼저 각 날짜와 요일 계산등을 쉽게하도록 도와주는 라이브러리인 date-fns 를 설치해야합니다.
yarn add date-fns
1. 캘린더에 그려줄 날짜 계산 커스텀훅 작성하기
커스텀훅으로 작성하는 이유는 첫번째로 해당 데이터의 재사용성 증가,두번째로는 화면을그리는 UI와 데이터를 분리하여 유지보수를 쉽게 할 수 있습니다.
import {
addDays,
endOfMonth,
endOfWeek,
startOfMonth,
startOfWeek,
eachDayOfInterval,
format,
addMonths,
startOfYear,
} from 'date-fns';
const useCalender = (selectedDate: Date) => {
// 일~토 요일을 모아두는 배열
const weekDays = [];
const weekStartDate = startOfWeek(new Date());
for (let day = 0; day < 7; day += 1) {
weekDays.push(format(addDays(weekStartDate, day), 'EEEEE'));
}
// 현재 선택된 년도에서 1월~12월까지 Date 객체로 들어있는 배열
const allMonth = [];
const startMonth = startOfYear(selectedDate);
for (let month = 0; month < 12; month += 1) {
allMonth.push(addMonths(startMonth, month));
}
const 현재달의시작날짜 = startOfMonth(selectedDate);
const 현재달의마지막날짜 = endOfMonth(selectedDate);
const 현재달의첫주의시작날짜 = startOfWeek(현재달의시작날짜);
const 현재달마지막주의끝날짜 = endOfWeek(현재달의마지막날짜);
const currentMonthAllDates = eachDayOfInterval({
start: 현재달의첫주의시작날짜,
end: 현재달마지막주의끝날짜,
});
return { weekDays, currentMonthAllDates, allMonth };
};
export default useCalender;
위의 코드에서 사용되어진 메서드들은 https://date-fns.org/docs/Getting-Started 에서 확인하실수 있습니다.
공식문서에 매개변수,반환값 등등을 잘 알려주고있으니 참고하시면 될것같습니다.
weekDays : 일요일~토요일 값이 들어있는 배열
currentMonthAllDates : 달력에 표시될 현재 월 의 총날짜가 들어있는 Date 객체 배열
allMonth : 현재 년도의 1월~12월 까지의 Date 객체 배열
2. 데이터를 사용하여 캘린더를 그려보자
해당 캘린더는 위의 weekDays,currentMonthAllDates 두개의 데이터를 사용하여 그려지게 됩니다.
export default function Date({
selectedDate,
setSelectedDate,
setPickerType,
}: DatePickerProps) {
const { currentMonthAllDates, weekDays } = useCalender(selectedDate);
//다음 달로 이동
const nextMonth = () => {
setSelectedDate(addMonths(selectedDate, 1));
};
// 이전 달로 이동
const prevMonth = () => {
setSelectedDate(subMonths(selectedDate, 1));
};
// 선택한날짜로 Date 변경 및 캘린더 닫기
const onChangeDate = (date: Date) => {
setSelectedDate(date);
setPickerType('');
};
return (
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between px-3">
<div className="flex gap-1 caption1">
<span>{format(selectedDate, 'MMM yyyy')}</span>
<button type="button" onClick={() => setPickerType('month')}>
<CaretDownIcon />
</button>
</div>
<div className="flex gap-2">
<button type="button" onClick={prevMonth}>
<CaretLeftIcon className="w-4 h-4 fill-grey-400" />
</button>
<button type="button" onClick={nextMonth}>
<CaretRightIcon className="w-4 h-4 fill-grey-400" />
</button>
</div>
</div>
<div className="grid grid-cols-7 place-items-center">
{weekDays.map((days, index) => (
<div key={index}>{days}</div>
))}
</div>
<div className="grid grid-cols-7 ">
{currentMonthAllDates.map((date, index) => (
<button
key={index}
className={`p-2 rounded-full
${isSameMonth(selectedDate, date) ? '' : 'text-grey-200'}
${
isSameDay(selectedDate, date)
? 'bg-primary-200 text-[#FFFFFF]'
: 'hover:bg-primary-50 hover:text-primary-200'
}`}
type="button"
onClick={() => onChangeDate(date)}
>
{date.getDate()}
</button>
))}
</div>
</div>
);
}
해당 캘린더는 allMonth 를 사용하여 그려지게 됩니다.
import { format, subYears, addYears, isSameMonth } from 'date-fns';
import CaretLeftIcon from '@/public/CaretLeft.svg';
import CaretRightIcon from '@/public/CaretRight.svg';
import CaretDownIcon from '@/public/CaretDown.svg';
import useCalender from '@/hooks/common/useCalender';
import { DatePickerProps } from '.';
export default function Month({
setPickerType,
selectedDate,
setSelectedDate,
}: DatePickerProps) {
const { allMonth } = useCalender(selectedDate);
//다음 년도로 이동
const onNextYear = () => {
setSelectedDate(addYears(selectedDate, 1));
};
//이전 년도로 이동
const onPrevYear = () => {
setSelectedDate(subYears(selectedDate, 1));
};
// 월 변경시 캘린더 닫기 및 월 Date 변경
const onChangeMonth = (month: Date) => {
setPickerType('');
setSelectedDate(month);
};
return (
<div className="flex flex-col">
<div className="flex items-center justify-between w-full p-2">
<span className="flex gap-1 caption1">
{format(selectedDate, 'yyyy')}
<button type="button" onClick={() => setPickerType('date')}>
<CaretDownIcon />
</button>
</span>
<div>
<button type="button" onClick={onPrevYear}>
<CaretLeftIcon className="w-4 h-4" />
</button>
<button type="button" onClick={onNextYear}>
<CaretRightIcon className="w-4 h-4" />
</button>
</div>
</div>
<div className="grid grid-cols-3">
{allMonth.map((month, index) => (
<button
type="button"
key={index}
className={` rounded-full p-2 caption2
${
isSameMonth(selectedDate, month)
? 'bg-primary-200 text-[#FFFFFF]'
: 'hover:bg-primary-50 hover:text-primary-200'
}`}
onClick={() => onChangeMonth(month)}
>
{format(month, 'MMM')}
</button>
))}
</div>
</div>
);
}
3. 마지막으로 input 태그에 연결
캘린더에서 선택한 날짜를 input 태그를 사용하여 연결해야 유저에게 직접적으로 날짜를 보여줄수있습니다.
해당컴포넌트를 사용하려면 부모컴포넌트 즉 선택한 날짜 값을 사용해야되는 컴포넌트에서 현재날짜를 기본값으로 하는 상태와 해당상태변경함수를 prop으로 내려주어야합니다.
'use client';
import { useState, Dispatch, SetStateAction } from 'react';
import { format } from 'date-fns';
import useOutSideClick from '@/hooks/common/useOutSideClick';
import Date from './Date';
import Month from './Month';
export type PickerType = 'date' | 'month' | 'year' | '';
export interface DatePickerProps {
selectedDate: Date;
setSelectedDate: Dispatch<SetStateAction<Date>>;
setPickerType: Dispatch<SetStateAction<PickerType>>;
}
export default function DatePicker({
selectedDate,
setSelectedDate,
}: Exclude<DatePickerProps, 'setPickerType'>) {
const [pickerType, setPickerType] = useState<PickerType>('');
const toggleDatePicker = () => {
if (pickerType !== '') {
setPickerType('');
} else {
setPickerType('date');
}
};
const renderPickerByType = (type: PickerType) => {
switch (type) {
case 'date':
return (
<Date
selectedDate={selectedDate}
setSelectedDate={setSelectedDate}
setPickerType={setPickerType}
/>
);
case 'month':
return (
<Month
setPickerType={setPickerType}
selectedDate={selectedDate}
setSelectedDate={setSelectedDate}
/>
);
default:
return;
}
};
return (
<div className="relative bg-white" ref={ref}>
<input
type="text"
value={format(selectedDate, 'yyyy년 MM월 dd일')}
className="p-4 border rounded-[10px] focus-primary body1 text-center cursor-pointer"
readOnly
onClick={toggleDatePicker}
/>
{pickerType !== '' && (
<div className="absolute rounded-[10px] p-3 z-50 mt-2 w-72 bg-white shadow-chatCard caption2 animate-dropdown">
{renderPickerByType(pickerType)}
</div>
)}
</div>
);
}
해당코드에서 switch 문을 사용한이유는 년도를 선택할수있는 캘린더도 추가될 가능성이 있기에 확장성을 고려하여 switch 문을 사용하였습니다.
4.동작화면
이상 캘린더 라이브러리 없이 Datepicker를 구현해보았습니다.
사실 커스텀훅에 데이터만 있으면 어떤 캘린더든 구현을 할수있기때문에 커스텀훅을 보시고 아이디어를 얻으셨으면 좋겠습니다.
해당코드 깃허브 :
https://github.com/puppypawpaw/pawpaw-client/tree/dev/components/ui/DatePicker
PS. 위와같이 구현하였는데 맥북의 경우 화면이 작아 DatePicker 하단이 짤리는 현상이 발생하였습니다. 해당 트러블 슈팅에 관한 내용은 다음 글에 작성하였습니다.
https://taejinii.tistory.com/66
'React' 카테고리의 다른 글
React-Query - onClick 이벤트로 useQuery data fetching (0) | 2023.10.09 |
---|---|
React- 재사용성과 확장성을 높이는 리액트 컴포넌트 설계 하기 (1) | 2023.09.25 |
React - 리액트 해시태그 구현 (0) | 2023.09.23 |
React - useCallback, useMemo 에 대하여 (0) | 2023.07.04 |
React - React-hook-form 회원가입시 닉네임 중복검사 하는법 (0) | 2023.06.05 |