[React date picker] 리액트 Date picker 커스텀하기 (with TypeScript)
안녕하세요 이번에는 React-date-picker을 타입스크립트 + SCSS로 완전히 새로운 UI를 구현해본 경험을 소개하고자 합니다.
거의 전체를 SCSS와 조합하여 스타일링을 새로 한 것으로 모든 영역에서 커스텀하는 것이 필요한 분들께 도움이 많이 될 것이라고 확신합니다.
** 구체적인 스타일링은 삭제하였습니다. 궁금하신게 있다면 댓글 달아주세요 :)
좌측은 React date picker에서 제공하는 기본 UI이고 우측은 제공하는 기능들을 통해 직접 커스텀한 date picker입니다.
라이브러리에서 제공하는 Demo 사이트를 통해서 필요한 옵션들을 끌어와 사용하였습니다.
React-date-picker 라이브러리에서는 커스텀을 위한 여러 props를 제공합니다. 제공해주는 Props에 맞게 컴포넌트를 크게 세 개로 나누었습니다.
전체 컴포넌트가 적용된 코드입니다. 구조를 파악하시는데 도움이 되실 것 같습니다.
import React from 'react';
import DatePicker, {
CalendarContainer,
ReactDatePickerCustomHeaderProps,
ReactDatePickerProps,
registerLocale,
} from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { CustomContainerProps } from './_fragments/CustomContainer';
import CustomContainer from './_fragments/CustomContainer';
import CustomDateInput from './_fragments/CustomDateInput';
import CustomHeader from './_fragments/CustomHeader';
import { ko } from 'date-fns/locale';
export interface DatePickerBoxProps
extends Omit<ReactDatePickerProps, 'onChange'> {
dateRange: [Date | null, Date | null];
setDateRange: (e: [Date | null, Date | null]) => void;
onClick?(): void;
onChange?(): void;
value?: string;
}
registerLocale('ko', ko);
const DatePickerBox = ({
value,
onClick,
dateRange = [null, null],
setDateRange,
...rest
}: DatePickerBoxProps) => {
const [startDate, endDate] = dateRange;
const RenderCustomHeader = ({
date,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
decreaseMonth,
increaseMonth,
}: ReactDatePickerCustomHeaderProps) => (
<CustomHeader
{...{
date,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
decreaseMonth,
increaseMonth,
startDate,
endDate,
}}
/>
);
const CalendarContainer = ({ children, className }: CustomContainerProps) => (
<CustomContainer
{...{
startDate,
endDate,
children,
className,
}}
/>
);
return (
<DatePicker
locale={'ko'} // 한국어 적용 (import 필요)
dateFormat={'yyyy/MM/dd'} // 데이트 포맷 형태
selectsRange={true} // 범위를 설정할 수 있는 옵션
startDate={startDate} // 범위와 같이 사용되는 시작일자.
endDate={endDate} // 범위와 같이 사용되는 끝나는 일자
showPopperArrow={false} // 화살표 변경 여부
onChange={setDateRange}
isClearable={false} // x 버튼
shouldCloseOnSelect={false} // 날짜선택시 모달 닫힘 여부
renderCustomHeader={RenderCustomHeader}
customInput={<CustomDateInput onClick={onClick} {...rest} />}
calendarContainer={CalendarContainer}
/>
);
};
export default DatePickerBox;
1. customInput : <CustomDateInput />
해당 컴포넌트를 나타내는 Input에 해당하는 부분입니다. input은 forwardRef를 사용하여 ref를 전달해줍니다.
import React from 'react';
import { ReactDatePickerProps } from 'react-datepicker';
import { CalendarIcon } from '@myIcons';
import { Flex, Text } from '@chakra-ui/react';
export interface DatePickerBoxProps
extends Omit<ReactDatePickerProps, 'onChange'> {
onClick?(): void;
onChange?(): void;
}
export type Ref = HTMLDivElement;
const CustomDateInput = React.forwardRef<HTMLDivElement, DatePickerBoxProps>(
({ onClick }, ref) => {
return (
<Flex ref={ref} onClick={onClick} cursor="pointer" userSelect="none">
<Text>입찰일자</Text>
<CalendarIcon />
</Flex>
);
},
);
CustomDateInput.displayName = 'CustomDateInput';
export default CustomDateInput;
2. renderCustomHeader : <RenderCustomHeader />
renderCustomHeader 에서 제공하는 옵션들 중 제가 사용한 props는 아래와 같습니다.
date : 오늘 기준 (Date)
prevMonthButtonDisabled : 이전 달 버튼 disabled (boolean)
nextMonthButtonDisabled : 다음 달 버튼 disabled (boolean)
decreaseMonth : 이전 달 이동 함수 (() => void)
increaseMonth : 다음 달 이동 함수 (() => void)
import { ReactDatePickerProps } from 'react-datepicker';
import { getMonth, getYear } from 'date-fns';
import { Button, HStack } from '@chakra-ui/react';
interface CustomHeaderProps extends Omit<ReactDatePickerProps, 'onChange'> {
date: Date;
decreaseMonth(): void;
increaseMonth(): void;
prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean;
}
const CustomHeader = ({ ...basisProps }: CustomHeaderProps) => {
const {
date,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
decreaseMonth,
increaseMonth,
} = basisProps;
return (
<>
<HStack
w="100%"
className={'react-datepicker__header'}
>
<Button
disabled={prevMonthButtonDisabled} // 이전 버튼에 disabled속성을 넣어줍니다.
_hover={{ boxShadow: 'none' }}
>
<ArrowLeftIcon
cursor={'pointer'}
fontSize={'16px'}
onClick={decreaseMonth} // 이전 버튼 함수 적용
/>
</Button>
<div className="month-day">
{getYear(date)}년 {[getMonth(date) + 1]}월 // date-fns를 활용하여 현재 년, 월로 포맷 시킵니다.
</div>
<Button
disabled={nextMonthButtonDisabled} // 다음 버튼에 disabled속성을 넣어줍니다.
_hover={{ boxShadow: 'none' }}
>
<ArrowRightIcon
fontSize={'16px'}
cursor={'pointer'}
onClick={increaseMonth} // 다음 버튼 함수 적용
/>
</Button>
</HStack>
</>
);
};
export default CustomHeader;
3. calendarContainer : <CustomContainer />
해당 컴포넌트는 캘린더 전체 영역으로 children을 꼭 넘겨줘야 합니다. 여기서 children은 날짜가 새겨진 달력 부분입니다.
실제로 children을 넘기지 않아 보시면 해당 부분이 렌더링 되지 않는 것을 확인하실 수 있을 것입니다.
children에 해당하는 부분을 커스텀 하고 싶다면 className까지 넘겨줘야 합니다.
const CalendarContainer = ({ children, className }: CustomContainerProps) => (
<CustomContainer
{...{
startDate,
endDate,
children,
className,
}}
/>
);
해당 부분을 구현하기 위해서는 CalendarContainer을 import 해야 합니다. 해당 영역은 children 윗부분이 됩니다.
import { CalendarContainer, CalendarContainerProps } from 'react-datepicker';
import { format } from 'date-fns';
import { Box, HStack, Text } from '@chakra-ui/react';
export interface CustomContainerProps
extends Omit<CalendarContainerProps, 'onChange'> {
children: JSX.Element;
startDate: Date | null;
endDate: Date | null;
className: string | undefined;
}
const CustomContainer = ({
children,
className,
startDate,
endDate,
}: CustomContainerProps) => {
return (
<Box
p="20px"
userSelect="none"
>
<CalendarContainer className={className}>
<Box >
<Text>
입찰일자
</Text>
<HStack>
<Text>
{startDate ? format(startDate, 'yyyy-MM-dd') : 'yyyy-mm-dd'}
</Text>
<Text>
{endDate ? format(endDate, 'yyyy-MM-dd') : 'yyyy-mm-dd'}
</Text>
</HStack>
</Box>
<Box position={'relative'}>{children}</Box>
</CalendarContainer>
</Box>
);
};
export default CustomContainer;
4. SCSS Styling
구현하는 것은 어렵지 않았지만 스타일링에서 애를 많이 먹었습니다.
className을 잡아오고 디자인대로 적용을 시키는 과정에서 곤욕을 많이 겪었는데, 이런 부분은 공유가 되면 좋을 것 같습니다.
scss를 활용해서 색상을 변수명으로 잡았으며, 공통된 스타일링, 조건부 스타일링을 함수로 만들었습니다.
4-1. 색상 변수 설정
보안상 색상 코드를 지운점 양해 바랍니다.
$black: #251b1b;
$gray: rgba(37, 27, 27, 0.1);
$gray800: #;
$gray300: #;
$main: #ff0e9e;
$lightPink: #;
$lightGreen: #;
4-2. 공통 스타일링 함수화
자주 쓰이는 css 속성들을 함수화시켰습니다.
@mixin dayStartEndCircle {
background-color: $black;
color: white;
border-radius: 50%;
padding: 0.166rem;
}
@mixin bgWhiteWithNoneBorder {
border: none;
background-color: white;
}
@mixin removeMargin {
margin: 0;
padding: 0.166rem;
box-sizing: content-box;
}
4-3. 조건부 스타일링
시작 날짜와 끝 날짜가 활성화됐을 때에 스타일링을 조건부 함수로 설정하였습니다.
@mixin beforeAction($startOrEnd) {
@if $startOrEnd== 'start' {
left: 50%;
right: 0;
} @else {
left: 0;
right: 50%;
}
content: ' ';
top: 0;
bottom: 0;
position: absolute;
background-color: $gray;
}
4-4. 스타일링 적용
하단에 전체 className을 활용하여 적용한 코드가 있습니다. 참고하시며 보시면 도움이 많이 되실 것이라 생각됩니다.
컨테이너와 헤더 컨테이너 영역
해당 영역에 배경 색과 보더가 잡혀있습니다.
깔끔한 컨테이너 스타일링을 위해서 해당 영역에 bgWhiteWithNoneBorder()를 include 시켰습니다.
div.react-datepicker
. react-datepicker__header
요일에 해당하는 영역
. react-datepicker__day-names
일요일에 해당하는 날짜
. react-datepicker__day--weekend
토요일에 해당하는 날짜
해당 영역은 정확한 className이 없어서 div 리스트의 last-child를 픽하였습니다.
last-child에 해당하는 부분은 토요일에 해당하는 모든 부분이었기 때문에 '토' (. react-datepicker__day-name)와 범위가 선택됐을 때 (react-datepicker__day--in-range)의 영역을 빼주었습니다.
div div div:last-child:not(. react-datepicker__day-name,. react-datepicker__day--in-range)
시작일과 끝 일
시작일과 끝 일에서 범위까지 스타일링을 자연스럽게 하려면 before시에 스타일링을 해주는 것을 빼먹지 말아야 합니다.
저는 removeMargin()과 조건부 스타일링 함수인 beforeAction()을 이곳에 적용시켰습니다.
. react-datepicker__day--range-start
. react-datepicker__day--range-end
시작일과 끝일을 제외한 범위
datepicker에서 범위에 해당하는 부분에 시작 날짜와 끝 날짜가 포함되어있었습니다.
시작 날짜와 끝 날짜가 아니면서 범위 안에 드는 className을 한 번에 잡기 위해 :not이라는 조건을 활용했습니다.
. react-datepicker__day--in-range:not(. react-datepicker__day--range-start,. react-datepicker__day--range-end)
키보드로 선택됐을 때
. react-datepicker__day--keyboard-selected
오늘 날짜
. react-datepicker__day--today
해당 월에서 이전 월에 해당하는 날짜
해당 월에서 보이는 이전 월의 날짜나 다음 월의 날짜에 해당하는 영역입니다. 해당 부분의 날짜의 컬러 채도를 낮출 수 있습니다.
. react-datepicker__day--outside-month
적용한 스타일링
div.react-datepicker__input-container {
button.react-datepicker__close-icon {
display: 'none';
&::after {
background-color: transparent;
}
}
}
div.react-datepicker {
@include bgWhiteWithNoneBorder();
.react-datepicker__header {
@include bgWhiteWithNoneBorder();
}
.react-datepicker__day {
color: $gray800;
&:hover {
@include removeMargin();
border-radius: 50%;
}
}
.react-datepicker__day-names {
border: none;
}
.react-datepicker__day--weekend {
color: $lightPink;
}
div
div
div:last-child:not(.react-datepicker__day-names, .react-datepicker__day-name, .react-datepicker__day--in-range) {
color: $lightGreen;
}
.react-datepicker__day--in-range:not(.react-datepicker__day--range-start, .react-datepicker__day--range-end) {
@include removeMargin();
&:hover {
background-color: $gray;
@include removeMargin();
}
font-weight: 500;
font-size: 12px;
background-color: $gray;
border-radius: 0;
color: $gray800;
}
.react-datepicker__day--in-selecting-range:not(.react-datepicker__day--range-start, .react-datepicker__day--range-end) {
@include removeMargin();
background-color: $gray;
}
.react-datepicker__day--keyboard-selected {
@include removeMargin();
&:hover {
@include removeMargin();
border-radius: 50%;
}
background-color: transparent;
border-radius: 50%;
}
.react-datepicker__day--today {
color: $main;
font-weight: 800 !important;
border-radius: 50%;
@include removeMargin();
&:focus-visible {
outline: transparent;
}
&:hover {
border: 'none';
outline: 'none';
}
}
.react-datepicker__day--range-start {
position: relative;
@include removeMargin();
@include dayStartEndCircle();
&:hover {
background-color: $black;
padding: 0.166rem;
}
&::before {
@include removeMargin();
@include beforeAction('start');
}
}
처음에는 데이트 피커를 직접 구현하려고 했는데, 직접 구현을 했다면 시간적으로 효율성이 떨어졌을 것 같습니다.
이번 기회를 통해 라이브러리를 적재적소에 잘 사용하며, 니즈에 맞게 제공하는 옵션들이 있는지 잘 파악하고 적용하는 연습을 잘한 것 같습니다.
또 Scss는 데이트 피커를 구현하면서 두 번째로 사용해 본 것인데, 지난번에는 컬러만 변수명으로 활용했다면 이번에는 함수까지 작성하여 다양하게 활용해 보았습니다.
* 혹시 Scss를 써보지 않으신 분들은 해당 유튜브 영상을 보시고 오시면 이해가 더 잘 되실 것 같습니다. 쉽고 빠르게 설명을 잘해주어서 쉽게 적용시킬 수 있었습니다.
Reference