React에는 아주 많은 달력 라이브러리가 있지만 가장 기본적이면서도 심플한 react-datepicker 라이브러리를 활용해봤다 !
사용 방법이 간단하고 제공하는 기능이 꽤나 많다는 점이 좋았다.
라이브러리 설치 및 사용
Install
$ npm i react-datepicker
$ npm i --save-dev @types/react-datepicker
Import
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
커스텀 참고
Date Picker
Default
<DatePicker
selected={ }
onChange={ }
/>
selected는 필수는 아니지만 선택한 값을 표시하기 위해 사용했다.
onChange는 DatePicker의 Props 중 유일한 필수 값이다.
index.d.ts
import { Middleware, Placement, UseFloatingOptions } from "@floating-ui/react";
import { Locale } from "date-fns";
import * as React from "react";
type PopperProps = Partial<Omit<UseFloatingOptions, "open" | "whileElementsMounted" | "placement" | "middleware">>;
export interface CalendarContainerProps {
className?: string | undefined;
children?: React.ReactNode | React.ReactNode[] | undefined;
}
export function registerLocale(localeName: string, localeData: Locale): void;
export function setDefaultLocale(localeName: string): void;
export function getDefaultLocale(): string;
export function CalendarContainer(props: CalendarContainerProps): React.ReactElement;
interface HighlightDates {
[className: string]: Date[];
}
interface Holiday {
date: string;
holidayName: string;
}
export interface ReactDatePickerCustomHeaderProps {
monthDate: Date;
date: Date;
changeYear(year: number): void;
changeMonth(month: number): void;
customHeaderCount: number;
decreaseMonth(): void;
increaseMonth(): void;
prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean;
decreaseYear(): void;
increaseYear(): void;
prevYearButtonDisabled: boolean;
nextYearButtonDisabled: boolean;
}
export interface ReactDatePickerProps<
WithRange extends boolean | undefined = undefined,
WithMultiple extends boolean | undefined = undefined,
> {
adjustDateOnChange?: boolean | undefined;
allowSameDay?: boolean | undefined;
ariaDescribedBy?: string | undefined;
ariaInvalid?: string | undefined;
ariaLabelClose?: string | undefined;
ariaLabelledBy?: string | undefined;
ariaRequired?: string | undefined;
autoComplete?: string | undefined;
autoFocus?: boolean | undefined;
calendarClassName?: string | undefined;
calendarContainer?(props: CalendarContainerProps): React.ReactNode;
calendarIconClassname?: string | undefined;
calendarStartDay?: number | undefined;
children?: React.ReactNode | undefined;
chooseDayAriaLabelPrefix?: string | undefined;
className?: string | undefined;
clearButtonClassName?: string | undefined;
clearButtonTitle?: string | undefined;
closeOnScroll?: boolean | ((e: Event) => boolean) | undefined;
customInput?: React.ReactNode | undefined;
customInputRef?: string | undefined;
customTimeInput?: React.ReactNode | undefined;
dateFormat?: string | string[] | undefined;
dateFormatCalendar?: string | undefined;
dayClassName?(date: Date): string | null;
weekDayClassName?(date: Date): string | null;
monthClassName?(date: Date): string | null;
timeClassName?(date: Date): string | null;
disabledDayAriaLabelPrefix?: string | undefined;
disabled?: boolean | undefined;
disabledKeyboardNavigation?: boolean | undefined;
dropdownMode?: "scroll" | "select" | undefined;
endDate?: Date | null | undefined;
excludeDates?: Date[] | Array<{ date: Date; message: string }> | undefined;
excludeDateIntervals?: Array<{ start: Date; end: Date }> | undefined;
excludeTimes?: Date[] | undefined;
filterDate?(date: Date): boolean;
filterTime?(date: Date): boolean;
fixedHeight?: boolean | undefined;
forceShowMonthNavigation?: boolean | undefined;
formatWeekDay?(day: string): React.ReactNode;
formatWeekNumber?(date: Date): string | number;
highlightDates?: Array<HighlightDates | Date> | undefined;
holidays?: Holiday[] | undefined;
icon?: string | React.ReactElement;
id?: string | undefined;
includeDates?: Date[] | undefined;
includeDateIntervals?: Array<{ start: Date; end: Date }> | undefined;
includeTimes?: Date[] | undefined;
injectTimes?: Date[] | undefined;
inline?: boolean | undefined;
focusSelectedMonth?: boolean | undefined;
isClearable?: boolean | undefined;
locale?: string | Locale | undefined;
maxDate?: Date | null | undefined;
maxTime?: Date | undefined;
minDate?: Date | null | undefined;
minTime?: Date | undefined;
monthsShown?: number | undefined;
name?: string | undefined;
nextMonthAriaLabel?: string | undefined;
nextMonthButtonLabel?: string | React.ReactNode | undefined;
nextYearAriaLabel?: string | undefined;
nextYearButtonLabel?: string | React.ReactNode | undefined;
onBlur?(event: React.FocusEvent<HTMLInputElement>): void;
onCalendarClose?(): void;
onCalendarOpen?(): void;
onChange(
date: WithRange extends false | undefined
? (WithMultiple extends false | undefined ? Date | null : Date[] | null)
: [Date | null, Date | null],
event: React.SyntheticEvent<any> | undefined,
): void;
onChangeRaw?(event: React.FocusEvent<HTMLInputElement>): void;
onClickOutside?(event: React.MouseEvent<HTMLDivElement>): void;
onDayMouseEnter?: ((date: Date) => void) | undefined;
onFocus?(event: React.FocusEvent<HTMLInputElement>): void;
onInputClick?(): void;
onInputError?(err: { code: number; msg: string }): void;
onKeyDown?(event: React.KeyboardEvent<HTMLDivElement>): void;
onMonthChange?(date: Date): void;
onMonthMouseLeave?: (() => void) | undefined;
onSelect?(date: Date, event: React.SyntheticEvent<any> | undefined): void;
onWeekSelect?(
firstDayOfWeek: Date,
weekNumber: string | number,
event: React.SyntheticEvent<any> | undefined,
): void;
onYearChange?(date: Date): void;
open?: boolean | undefined;
openToDate?: Date | undefined;
peekNextMonth?: boolean | undefined;
placeholderText?: string | undefined;
popperClassName?: string | undefined;
popperContainer?(props: { children: React.ReactNode[] }): React.ReactNode;
popperModifiers?: readonly Middleware[] | undefined;
popperPlacement?: Placement | undefined;
popperProps?: PopperProps | undefined;
preventOpenOnFocus?: boolean | undefined;
previousMonthAriaLabel?: string | undefined;
previousMonthButtonLabel?: string | React.ReactNode | undefined;
previousYearAriaLabel?: string | undefined;
previousYearButtonLabel?: string | React.ReactNode | undefined;
readOnly?: boolean | undefined;
renderCustomHeader?(params: ReactDatePickerCustomHeaderProps): React.ReactNode;
renderDayContents?(dayOfMonth: number, date?: Date): React.ReactNode;
renderQuarterContent?(quarter: number, shortQuarterText: string): React.ReactNode;
renderMonthContent?(monthIndex: number, shortMonthText: string, fullMonthText: string): React.ReactNode;
renderYearContent?(year: number): React.ReactNode;
required?: boolean | undefined;
scrollableMonthYearDropdown?: boolean | undefined;
scrollableYearDropdown?: boolean | undefined;
selected?: Date | null | undefined;
selectsEnd?: boolean | undefined;
selectsStart?: boolean | undefined;
selectsRange?: WithRange;
selectsMultiple?: WithMultiple;
selectedDates?: Date[];
shouldCloseOnSelect?: boolean | undefined;
showDisabledMonthNavigation?: boolean | undefined;
showFullMonthYearPicker?: boolean | undefined;
showMonthDropdown?: boolean | undefined;
showMonthYearDropdown?: boolean | undefined;
showMonthYearPicker?: boolean | undefined;
showPopperArrow?: boolean | undefined;
showPreviousMonths?: boolean | undefined;
showQuarterYearPicker?: boolean | undefined;
showTimeInput?: boolean | undefined;
showTimeSelect?: boolean | undefined;
showTimeSelectOnly?: boolean | undefined;
showTwoColumnMonthYearPicker?: boolean | undefined;
showFourColumnMonthYearPicker?: boolean | undefined;
showWeekNumbers?: boolean | undefined;
showWeekPicker?: boolean | undefined;
showYearDropdown?: boolean | undefined;
showYearPicker?: boolean | undefined;
showIcon?: boolean | undefined;
startDate?: Date | null | undefined;
startOpen?: boolean | undefined;
strictParsing?: boolean | undefined;
tabIndex?: number | undefined;
timeCaption?: string | undefined;
timeFormat?: string | undefined;
timeInputLabel?: string | undefined;
timeIntervals?: number | undefined;
title?: string | undefined;
todayButton?: React.ReactNode | undefined;
toggleCalendarOnIconClick?: boolean | undefined;
useShortMonthInDropdown?: boolean | undefined;
useWeekdaysShort?: boolean | undefined;
weekAriaLabelPrefix?: string | undefined;
monthAriaLabelPrefix?: string | undefined;
value?: string | undefined;
weekLabel?: string | undefined;
withPortal?: boolean | undefined;
portalId?: string | undefined;
portalHost?: ShadowRoot | undefined;
wrapperClassName?: string | undefined;
yearDropdownItemNumber?: number | undefined;
excludeScrollbar?: boolean | undefined;
enableTabLoop?: boolean | undefined;
yearItemNumber?: number | undefined;
}
declare class ReactDatePicker<
WithRange extends boolean | undefined = undefined,
WithMultiple extends boolean | undefined = undefined,
> extends React.Component<
ReactDatePickerProps<WithRange, WithMultiple>
> {
readonly setBlur: () => void;
readonly setFocus: () => void;
readonly setOpen: (open: boolean, skipSetBlur?: boolean) => void;
readonly isCalendarOpen: () => boolean;
}
export default ReactDatePicker;
minDate, maxDate와 startDate, endDate의 차이
🍀 min, maxDate
- 사용자가 선택할 수 있는 날짜의 최소(대)값 지정
- 특정 날짜 이전(후)의 날짜를 선택하지 못하도록 제한
🍀 start, endDate
- 선택된 날짜 범위의 시작(끝) 날짜 지정, 주로 date range picker에서 사용
- 날짜 범위를 선택할 때, 시작(끝) 날짜를 설정하여 끝(시작) 날짜를 선택하는 범위 제한
즉, 단일 날짜 선택 → min, max / 날짜 범위 선택 → start, end
언어 지정
import { ko } from "date-fns/locale/ko";
registerLocale("ko", ko);
export default function DateSelector() {
return (
<DatePicker
...
locale={ko}
/>
);
}
Icon
react-icons와 함께 사용
import { MdOutlineCalendarToday } from "react-icons/md";
export default function DateSelector() {
return (
<DatePicker
...
showIcon
icon={<MdOutlineCalendarToday />}
toggleCalendarOnIconClick
/>
);
}
showIcon을 반드시 같이 써줘야 한다.
toggleCalendarOnIconClick → Icon을 눌렀을 때도 달력 팝업이 나오게 하려고 사용
Placeholder
export default function DateSelector() {
return (
<DatePicker
...
placeholderText="YYYY-MM-DD"
/>
);
}
Header Custom
😥 헤더를 굳이 커스텀한 이유
→ prev, next 버튼이 아이콘(svg)이 아니기 때문에 크기 조절이 상당히 어려웠다.
renderCustomHeader 속성 사용
DateCalendarHeader.tsx
// DateCalendarHeader.tsx
import {
CalendarButton,
CalendarButtonWrapper,
PickerCalendarHeader,
} from "./DateCalendarHeader.style";
import { MdNavigateBefore, MdOutlineNavigateNext } from "react-icons/md";
interface Props {
date: Date;
decreaseMonth(): void;
increaseMonth(): void;
prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean;
}
export default function DateCalendarHeader({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}: Props) {
return (
<PickerCalendarHeader>
<div>
<span>{date.getFullYear()}년 </span>
<span>{date.getMonth() + 1}월</span>
</div>
<CalendarButtonWrapper>
<CalendarButton
onClick={decreaseMonth}
disabled={prevMonthButtonDisabled}
>
<MdNavigateBefore />
</CalendarButton>
<CalendarButton
onClick={increaseMonth}
disabled={nextMonthButtonDisabled}
>
<MdOutlineNavigateNext />
</CalendarButton>
</CalendarButtonWrapper>
</PickerCalendarHeader>
);
}
ReactDatePickerCustomHeaderProps에 정의된 변수들은 전부 필수 값으로 돼있어서 컴포넌트에서 전달 받을 props에 대한 인터페이스를 따로 만들어줬다.
getMonth는 특이하게도 0부터 반환한다. 그렇기 때문에 반환 값에 +1을 해줘야 한다.
DateSelector.tsx
export default function DateSelector() {
return (
<DatePicker
...
renderCustomHeader={({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<DateCalendarHeader
date={date}
decreaseMonth={decreaseMonth}
increaseMonth={increaseMonth}
prevMonthButtonDisabled={prevMonthButtonDisabled}
nextMonthButtonDisabled={nextMonthButtonDisabled}
/>
)}
/>
);
}
Date Range Picker
Range
기본 버전
export default function DateRangeSelector() {
return (
<DatePicker
...
selectsRange
startDate={ }
endDate={ }
/>
);
}
BUT,
1️⃣ 가운데 -를 ~로 바꾸고 싶음
2️⃣ 두 날짜 사이 간격을 띄우고 싶음
DatePicker 두개 사용
위의 두 가지 조건을 충족하기 위해 DatePicker를 두 개로 나누고 스타일을 조절했다.
export default function DateRangeSelector() {
return (
<>
<DatePicker
selected={startDate}
onChange={ }
startDate={ }
endDate={ }
selectsStart
/>
~
<DatePicker
selected={endDate}
onChange={ }
startDate={ }
endDate={ }
selectsEnd
/>
</>
);
}
Problem
오늘 날짜와 동일한 일자가 모든 달마다 하이라이트
오늘 날짜 : 5월 30일
BUT 6월 30일에도 동일하게 하이라이팅이 들어가 있다. (다른 달도 동일)
< 참고 >
disabledKeyboardNavigation 속성 추가
달력 Range 색상 이상
‘월’이 범위에 속하지 않아도 ‘일’만으로도 날짜에 스타일이 적용되고 있다.
❌ 방법 1
highlightDates 속성에 선택한 범위의 날짜 배열을 넣어주기
import { eachDayOfInterval } from "date-fns";
export default function DateRangeSelector() {
return (
<>
...
<DatePicker
selected={endDate}
...
highlightDates={eachDayOfInterval({ start: startDate, end: endDate })}
/>
</>
);
}
→ 달라지는 것이 없다…
⭕ 방법 2
CSS 설정을 조절하기
7월 4일 (선택되지 않았는데 색상 O)
<div
class="react-datepicker__day react-datepicker__day--004 react-datepicker__day--in-selecting-range"
tabindex="-1"
aria-label="Choose 2024년 7월 4일 목요일"
role="option"
title=""
aria-disabled="false"
aria-selected="false"
>4</div>
7월 6일 (일반)
<div
class="react-datepicker__day react-datepicker__day--006 react-datepicker__day--weekend"
tabindex="-1"
aria-label="Choose 2024년 7월 6일 토요일"
role="option"
title=""
aria-disabled="false"
aria-selected="false"
>6</div>
6월 3일 (선택 범위 내)
<div
class="react-datepicker__day react-datepicker__day--003 react-datepicker__day--in-range react-datepicker__day--in-selecting-range"
tabindex="-1"
aria-label="Choose 2024년 6월 3일 월요일"
role="option"
title=""
aria-disabled="false"
aria-selected="true"
>3</div>
선택 범위 내의 날짜(6/3)와 선택되지 않았으나 색상이 있는 날짜(7/4)의 클래스 중 react-datepicker__day—in-selecting-range 가 동일하다.
(대체 후자는 왜 저 클래스를 주는건데…)
react-datepicker__day—in-selecting-range 클래스가 하이라이팅을 해주는 컬러 css를 가지고 있으므로 적용된 것이다.
두 날짜의 다른 차이점인 aria-selected 속성을 통해 범위에 속하지 않은 날짜에 대한 하이라이팅을 제거해줬다.
.react-datepicker__day--in-selecting-range[aria-selected="false"] {
color: #000;
background-color: white;
}
'React' 카테고리의 다른 글
react-router-dom의 outlet으로 layout 구성하기 (0) | 2024.06.13 |
---|---|
[ React ] 캘린더 컴포넌트 직접 만들기 (feat. date-fns) (0) | 2024.06.12 |
@react-icons/all-files로 모듈 크기 개선 (0) | 2024.05.21 |
react-icons 라이브러리 활용 (Figma, 프로젝트) (1) | 2024.05.21 |
React-query 개념 및 장점 (useQuery, useMutation, useInfiniteQuery) (0) | 2024.04.08 |