[react-datepicker] datepicker 커스텀하기 (+ timepicker)

서론

회사 프로젝트에서 datepicker가 필요했다. 라이브러리를 찾다가 커스텀하기에 용이해 보였던 react datepicker를 사용했다. 혼자 해보고 싶다면 다음 링크의 예제를 참고해서 해도 충분히 할 수 있을 것이다.
https://reactdatepicker.com/

목표

  • 형태가 일그러지지 않을 것
  • 날짜 범위에서 앞의 날짜보다 뒤의 날짜가 앞서지 않을 것
  • '이전'의 범위를 할 경우엔 뒤의 날짜에서 오늘 이후의 날짜를 선택할 수 없을 것
  • '이후'의 범위를 할 경우엔 앞의 날짜에서 오늘 이전의 날짜를 선택할 수 없을 것
  • 월, 년도를 선택할 수 있어야 할 것

구현

기본 Datepicker의 모습은 다음과 같다.

완성된 모습은 다음과 같다. (혹시 검색을 할 때 나처럼 이미지를 보고 내가 구현하고자 하는 것과 유사한지 확인하는 사람도 있을 수 있으니 구현된 이미지도 남긴다.)

import React from 'react';
import DatePicker from 'react-datepicker';
import { ko } from 'date-fns/esm/locale';
import styled from '@emotion/styled';
/* Component */
import Icon from './Icon';
/* Style */
import { theme } from '@/styles/theme';
import { StDatePickerBox } from '@/styles/SearchFilter.style';

interface DatepickerProps {
  selectedDate: Date | null;
  handleDateChange: (date: Date | null) => void;
  isDateDisabled?: boolean;
  value?: string | undefined;
  time?: string;
  minDate?: Date | null;
  maxDate?: Date | null;
  disabledDates?: Date[];
}

const Datepicker: React.FC<DatepickerProps> = ({
  selectedDate,
  handleDateChange,
  isDateDisabled,
  value,
  time,
  minDate,
  maxDate,
  disabledDates,
}) => {
  return (
    <>
      <StDatePickerBox className="date-picker-box">
        {isDateDisabled ? (
          <span>연도-월-일</span>
        ) : time === '이전' ? (
          <StyledDatePicker
            locale={ko}
            dateFormat="yyyy-MM-dd"
            selected={selectedDate}
            closeOnScroll={true}
            onChange={handleDateChange}
            placeholderText="연도-월-일"
            value={value}
            minDate={minDate && minDate}
            maxDate={maxDate ? maxDate : new Date()}
            excludeDates={disabledDates ?? []}
            className="date-picker"
            onKeyDown={(e) => e.preventDefault()}
            showMonthDropdown
            showYearDropdown
            dropdownMode="select"
          />
        ) : time === '이후' ? (
          <StyledDatePicker
            locale={ko}
            dateFormat="yyyy-MM-dd"
            selected={selectedDate}
            closeOnScroll={true}
            onChange={handleDateChange}
            placeholderText="연도-월-일"
            value={value}
            minDate={minDate ? minDate : new Date()}
            maxDate={maxDate && maxDate}
            excludeDates={disabledDates ?? []}
            className="date-picker"
            onKeyDown={(e) => e.preventDefault()}
            showMonthDropdown
            showYearDropdown
            dropdownMode="select"
          />
        ) : (
          <StyledDatePicker
            locale={ko}
            dateFormat="yyyy-MM-dd"
            selected={selectedDate}
            closeOnScroll={true}
            onChange={handleDateChange}
            placeholderText="연도-월-일"
            value={value}
            minDate={minDate && minDate}
            maxDate={maxDate && maxDate}
            excludeDates={disabledDates ?? []}
            className="date-picker"
            onKeyDown={(e) => e.preventDefault()}
            showMonthDropdown
            showYearDropdown
            dropdownMode="select"
          />
        )}
        <Icon name="IconCalendar" width="1.8rem" height="1.8rem" />
      </StDatePickerBox>
    </>
  );
};

export default Datepicker;

export const StyledDatePicker = styled(DatePicker)`
  border: none;

  color: ${theme.color.baseMid};

  font-size: 1.7rem;
  font-style: normal;
  font-weight: 400;
  line-height: 2.2rem;
`;

/* Styled Component */
export const StDatePickerBox = styled.div`
  display: flex;
  height: 5.2rem;
  padding: 0.8rem 1.6rem;
  justify-content: space-between;
  align-items: center;
  gap: 0.8rem;
  align-self: stretch;

  border-radius: 0.4rem;
  border: 1px solid ${theme.color.baseLight};

  & input {
    width: 100%;
    color: ${theme.color.baseDark};

    font-size: 1.7rem;
    font-style: normal;
    font-weight: 400;
    line-height: 2.2rem;
  }

  & input::placeholder {
    color: #c6c6d6;

    font-size: 1.7rem;
    font-style: normal;
    font-weight: 400;
    line-height: 2.2rem;
  }

  & span {
    color: #c6c6d6;

    font-size: 1.7rem;
    font-style: normal;
    font-weight: 400;
    line-height: 2.2rem;
  }

  & .react-datepicker-wrapper {
    width: 100%;
  }

  & .react-datepicker {
    font-size: 1.5em;
    font-family: 'Pretendard';
  }
  & .react-datepicker__header {
    font-size: 1em;
  }
  & .react-datepicker__month {
    margin: 0.4em 1em;
  }
  & .react-datepicker__day-name,
  & .react-datepicker__day {
    width: 1.9em;
    line-height: 1.9em;
  }
  & .react-datepicker__current-month {
    font-size: 1em;
  }
  & .react-datepicker__navigation {
    top: 1em;
    line-height: 1.7em;
    border: 0.45em solid transparent;
  }
  & .react-datepicker__navigation--previous {
    top: 0rem;
  }
  & .react-datepicker__navigation--next {
    top: 0em;
  }

  // 연도
  & .react-datepicker__year-read-view--selected-year {
    font-size: 1.2em;
    color: ${theme.color.baseMidDark};
  }
  & .react-datepicker__year-read-view--down-arrow {
    top: 0.5rem;
    font-size: 1.5em;
    color: ${theme.color.baseMidDark};
  }

  & .react-datepicker__month-select,
  .react-datepicker__year-select {
    // select box
    border: none;
    padding: 0.3rem 0.7rem;
    margin: 0.5rem 0rem;
  }

  // TimePicker
  & .react-datepicker__time {
    font-size: 1.6rem;
  }
  & .react-datepicker-time__header {
    font-size: 1.5rem;
  }
`;

* react-datepicker의 크기는 font-size로 결정이 된다.

  • locale: 지역(언어)
  • dateFormat: 날짜 포맷
  • selected: 선택된 날짜(Date 타입)
  • closeOnScroll: 스크롤하면 닫힘
  • onChange: 변화 적용
  • placeholderText: placeholder
  • value: 값
  • minDate/maxDate: 최솟값, 최댓값
  • excludeDates: 제외할 일자
  • onKeyDown: 입력시 (현재는 입력 방지를 위해 막아둠)
  • showMonthDropdown/showYearDropdown/dropdownMode: 월, 년도 선택

사용하는부분은 아래와 같다.

  const [selectedStartDate, setSelectedStartDate] = useState<Date | null>(null);
  const [startDate, setStartDate] = useState<string | undefined>(
    undefined,
  );
  const [selectedEndDate, setSelectedEndDate] = useState<Date | null>(null);
  const [endDate, setEndDate] = useState<string | undefined>(
    undefined,
  );
  const [dateBtn, setDateBtn] = useState('전체');
  const [isDateDisabled, setIsDateDisabled] = useState(false);
  const handleStartDateChange = (date: Date | null) => {
    setDateBtn('');
    if (date) {
      setSelectedStartDate(date);
      setStartDate(String(dayjs(date).format('YYYY-MM-DD')));
    }
  };
  const handleEndDateChange = (date: Date | null) => {
    setDateBtn('');
    if (date) {
      setSelectedEndDate(date);
      setEndDate(String(dayjs(date).format('YYYY-MM-DD')));
    }
  };

/* 아래처럼 사용하시면 됩니다 */
<Datepicker
  selectedDate={selectedStartDate}
  handleDateChange={handleStartDateChange}
  isDateDisabled={isDateDisabled}
  value={startDate && startDate}
  time="이전"
  maxDate={selectedEndDate}
/>
  <Icon name="IconDash" width="1rem" />

<Datepicker
  selectedDate={selectedEndDate}
  handleDateChange={handleEndDateChange}
  isDateDisabled={isDateDisabled}
  value={endDate && endDate}
  time="이전"
  minDate={selectedStartDate}
/>

Timepicker

기본 Timepicker와 완성된 모습이다.

Timepicker도 Datepicker와 유사하다.

import DatePicker from 'react-datepicker';
import styled from '@emotion/styled';
import { ko } from 'date-fns/esm/locale';
/* Component */
import Icon from './Icon';
/* Style */
import { theme } from '@/styles/theme';
import { StDatePickerBox } from '@/styles/SearchFilter.style';
import dayjs from 'dayjs';

interface DatepickerProps {
  selectedDate: Date | null;
  selectedTime: Date | null;
  handleDateChange: (date: Date | null) => void;
  value?: string | undefined;
}

export const TimePicker = ({
  selectedDate,
  selectedTime,
  handleDateChange,
  value,
}: DatepickerProps) => {
  const filterPassedTime = (time: any) => {
    const currentDate = new Date();
    currentDate.setMinutes(currentDate.getMinutes() + 30);

    const selectedDate = new Date(time);

    return currentDate.getTime() <= selectedDate.getTime();
  };

  return (
    <StDatePickerBox>
      <StyledTimePicker
        locale={ko}
        dateFormat="hh:mm:ss"
        selected={selectedTime}
        closeOnScroll={true}
        onChange={handleDateChange}
        value={value && value}
        filterTime={
          dayjs(selectedDate).format('YYYY-MM-DD') === dayjs(new Date()).format('YYYY-MM-DD')
            ? filterPassedTime
            : undefined
        }
        placeholderText="00:00"
        showTimeSelect
        showTimeSelectOnly
        timeIntervals={1}
        timeCaption="Time"
        onKeyDown={(e: any) => e.preventDefault()}
      />
      <Icon name="IconClock" width="1.8rem" height="1.8rem" />
    </StDatePickerBox>
  );
};

export const StyledTimePicker = styled(DatePicker)`
  border: none;

  color: ${theme.color.baseMid};

  font-size: 1.7rem;
  font-style: normal;
  font-weight: 400;
  line-height: 2.2rem;
`;
  • timeIntervals: 몇 분 단위로 옵션을 줄 것인지
  • filterTime: 현재는 datepicker에서 오늘을 선택했을 때(datepicker와 함께 쓰일 경우) 현재 시간 이전은 선택을 막아뒀습니다. (알림 기능을 위해 만들어진 컴포넌트)

사용하는 부분은 아래와 같다.

  const [selectedStartDate, setSelectedStartDate] = useState<Date | null>(null);
  const [startDate, setStartDate] = useState<string | undefined>();
  const [selectedStartTime, setSelectedStartTime] = useState<Date | null>(null);
  const [startTime, setStartTime] = useState<string | undefined>();
  const handleStartDateChange = (date: Date | null) => {
    if (date) {
      setSelectedStartDate(date);
      setStartDate(String(dayjs(date).format('YYYY-MM-DD')));
    }
  };
  const handleStartTimeChange = (date: Date | null) => {
    if (date) {
      setSelectedStartTime(date);
      setStartTime(String(dayjs(date).format('HH:mm')));
    }
  };

<TimePicker
  selectedDate={selectedStartDate}
  selectedTime={selectedStartTime}
  handleDateChange={handleStartTimeChange}
  value={startTime && startTime}
/>