react-datepicker 라이브러리의 달력 UI 커스텀 과정 기록입니다.



선택한 날짜 값과 관련된 로직은 제외하고 UI 컴포넌트 관련된 내용만 먼저 정리해보려고 한다.
대략적인 구조는 이렇게 구성했다.
1) BaseInput
- 기존 DatePicker에서 제공하는 기본 input을 숨기고(inline 속성) 해당 컴포넌트를 사용했다.
- 클릭하면 달력을 열어준다 (isOpen 상태를 토글)
- 선택할 날짜를 보여준다
2) DatePicker : react-datepicker 라이브러리에서 제공하는 input + 달력 객체
- isOpen 상태값이 true이면 달력 펼침
3) DatePickerHeader : 커스텀 달력 헤더
4) 닫힘 버튼
- 처음에 커스텀 헤더에 추가해줬는데 이벤트 버블링때문에 제대로 작동을 안하는 문제가 발생
- 상위 컴포넌트로 빼서 absolute로 포지셔닝하니 정상 동작 하였다.
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css"; // 기본 스타일 정의
import DatePickerHeader from "./DatePickerHeader";
const DatePickerInput = ()=>{
// ...
return (
<div className="relative">
<BaseInput value={displayValue || ""}/>
{isOpen && (
<>
<div
className="absolute z-20 mt-1 h-[388px] w-[327px] rounded-lg bg-white lg:h-[584px] lg:w-[640px]"
ref={pickerRef}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<DatePicker
inline
selectsRange
locale={ko}
startDate={startDate}
endDate={endDate}
onChange={handleChange}
minDate={startDate}
renderCustomHeader={(props) => <DatePickerHeader {...props} />}
/>
</div>
<button
type="button"
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
handleOpenDropdown();
}}
className="absolute left-[14px] top-[78px] z-30 cursor-pointer lg:top-[84px]"
>
<IoIosClose className="size-6 text-black-100 lg:size-9" />
</button>
</>
)}
</div>
)
</div>
}
//DatePickerHeader.tsx
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";
interface DatePickerHeaderProps {
date: Date;
decreaseMonth: () => void;
increaseMonth: () => void;
prevMonthButtonDisabled: boolean;
nextMonthButtonDisabled: boolean;
}
const DatePickerHeader = ({
date,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}: DatePickerHeaderProps) => {
const iconStyle = "size-6 lg:size-9 text-grayscale-200";
return (
<div className="flex flex-col gap-2">
<div className="lg:h-15 z-20 flex h-12 items-center justify-center text-sm font-semibold leading-6 lg:text-lg lg:font-normal lg:leading-[26px]">
기간 선택
</div>
<div className="lg:h-15 mb-2 flex h-12 items-center justify-between px-[14px] py-3">
<button type="button" onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>
<MdKeyboardArrowLeft className={iconStyle} />
</button>
<span className="text-base font-semibold leading-[26px] text-black-400 lg:text-[20px] lg:leading-8">
{`${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, "0")}`}
</span>
<button type="button" onClick={increaseMonth} disabled={nextMonthButtonDisabled}>
<MdKeyboardArrowRight className={iconStyle} />
</button>
</div>
</div>
);
};
export default DatePickerHeader;
먼저 데이터피커에서 지원하는 속성들을 설정해주었다.
- inline:
DatePicker의 기본 input 없이 달력만 렌더 - selectsRange:
날짜 범위를 선택할 수 있도록 설정하는 속성입니다. 이 속성이 활성화되면 시작 날짜와 종료 날짜를 선택할 수 있는 두 개의 날짜 입력 필드가 나타납니다. <-> 디폴트 : 날짜 한개만 선택 - locale={ko}:
ko는 한국어를 의미합니다. 이 속성은 날짜를 표시하는 방식, 예를 들어 날짜의 월, 일, 요일 등이 한국어로 표시되도록 설정합니다. locale에 설정된 값에 따라 로케일이 맞춰집니다. - startDate={startDate}:
시작 날짜를 설정하는 속성입니다. startDate는 선택 가능한 시작 날짜를 나타내며, 이 값은 상태나 props로 관리됩니다. - endDate={endDate}:
종료 날짜를 설정하는 속성입니다. endDate는 선택 가능한 종료 날짜를 나타내며, 이 값도 상태나 props로 관리됩니다. - onChange={handleChange}:
날짜가 변경될 때 호출되는 함수입니다. 날짜 범위를 선택하면 handleChange 함수가 호출되어 startDate와 endDate를 업데이트할 수 있습니다. 일반적으로 선택된 날짜를 부모 컴포넌트로 전달하거나 상태를 업데이트하는 데 사용됩니다. - minDate={startDate}:
minDate는 선택할 수 있는 최소 날짜를 설정하는 속성입니다. 이 속성에 startDate를 할당하면, 시작 날짜 이후로만 선택할 수 있도록 제한됩니다. - renderCustomHeader={(props) => <DatePickerHeader {...props} />}:
renderCustomHeader는 달력의 헤더를 커스터마이즈할 수 있게 해주는 속성입니다. 기본 헤더 대신 DatePickerHeader 컴포넌트를 렌더링하여 날짜 선택기의 상단을 원하는 방식으로 커스터마이징할 수 있습니다.
공식 문서에서 다양한 속성과 여러 용례를 알려주고 있다.
React Datepicker crafted by HackerOne
reactdatepicker.com
공식문서를 좀 보면서 했더라면 좋았겠다는 생각을 포스팅 하면서 뒤늦게 해본다 ㅎㅎ..
여기까지는 어찌 어찌 했는데 선택한 날짜나 날짜 범위의 색상, 호버할때 색상 등을 커스텀하는게 좀 힘들었던 기억이 난다.
검색을 해봐도 개발자도구로 한땀한땀 클래스 이름을 짚어가면서 global.css에 추가해주는것 말고는 별 다른 수가 없는 것 같았다.
이런식으로..

그래서 그 결과가.. ▼ 넘넘 길어서 아래에..
/*------------------- date picker 커스텀 --------------------*/
/* 너비 채우기 */
.react-datepicker-wrapper {
@apply border-[0.5px] border-grayscale-100;
}
.react-datepicker,
.react-datepicker__month-container {
@apply mb-4 w-full lg:mb-6 !important;
}
.react-datepicker__month {
@apply m-0 !important;
}
/* week 스타일 */
.react-datepicker__week,
.react-datepicker__day-names {
@apply flex px-2 lg:px-4;
}
/* 모든 날짜 셀에 대한 기본 크기 설정 */
.react-datepicker__day {
@apply my-[7px] flex h-7 w-full items-center justify-center text-[13px] font-medium leading-[22px] text-black-400 lg:my-[14px] lg:h-10 lg:text-[20px] lg:leading-8 !important;
}
/* 요일 헤더 스타일 */
.react-datepicker__day-name {
@apply flex h-[42px] w-full items-center justify-center text-[13px] font-medium leading-[22px] text-grayscale-500 lg:h-16 lg:text-[20px] lg:leading-8 !important;
}
.react-datepicker__day-name,
.react-datepicker__day,
.react-datepicker__time-name {
@apply mx-0 !important;
}
/* 헤더 스타일 */
.react-datepicker .react-datepicker__header {
@apply bg-white;
}
.react-datepicker__header,
.react-datepicker__header--custom {
@apply border-transparent !important;
}
/* 현재 달이 아닌 날짜 스타일 */
.react-datepicker__day--outside-month {
@apply text-grayscale-300 !important;
}
/* date range */
.react-datepicker__day--in-selecting-range,
.react-datepicker__day--in-range,
.react-datepicker__month-text--in-selecting-range,
.react-datepicker__month-text--in-range,
.react-datepicker__quarter-text--in-selecting-range,
.react-datepicker__quarter-text--in-range,
.react-datepicker__year-text--in-selecting-range,
.react-datepicker__year-text--in-range,
.react-datepicker__day--in-selecting-range:not(.react-datepicker__day--outside-month) {
@apply bg-primary-orange-50 text-black-400 opacity-80 !important;
}
/* 범위의 첫 날짜에 대한 스타일 */
.react-datepicker__day--range-start.react-datepicker__day--in-range {
@apply bg-primary-orange-50 !important;
background: linear-gradient(to left, #fff7eb 50%, transparent 50%) !important;
}
/* 범위의 마지막 날짜에 대한 스타일 */
.react-datepicker__day--range-end.react-datepicker__day--in-range {
@apply bg-primary-orange-50 !important;
background: linear-gradient(to right, #fff7eb 50%, transparent 50%) !important;
}
/* Start, End 날짜 스타일 - 선택 순서와 관계없이 항상 적용 */
.react-datepicker__day--range-start,
.react-datepicker__day--selecting-range-start,
.react-datepicker__day--range-end,
.react-datepicker__day--selecting-range-end,
.react-datepicker__day--selected {
@apply relative text-grayscale-50 !important;
z-index: 1;
}
/* Start, End 날짜의 원형 배경 스타일 */
.react-datepicker__day--range-start::before,
.react-datepicker__day--selecting-range-start::before,
.react-datepicker__day--range-end::before,
.react-datepicker__day--selecting-range-end::before,
.react-datepicker__day--selected::before {
@apply absolute left-1/2 top-1/2 h-7 w-7 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-orange-300 content-[''] lg:h-10 lg:w-10 !important;
z-index: -1;
}
/* 날짜 호버 스타일 */
.react-datepicker__day:hover,
.react-datepicker__day--selecting-range-start:hover,
.react-datepicker__day--selecting-range-end:hover {
@apply bg-primary-orange-50 !important;
}
/* 오늘 날짜 기본 스타일 */
.react-datepicker__day--today {
@apply border-transparent bg-transparent font-bold !important;
color: theme("colors.black.400") !important;
}
/* 오늘 날짜가 범위 안에 있을 때의 스타일 */
.react-datepicker__day--today.react-datepicker__day--in-range {
@apply bg-primary-orange-50 !important;
}
/* 오늘 날짜가 시작일이나 종료일일 때의 스타일 */
.react-datepicker__day--today.react-datepicker__day--range-start::before,
.react-datepicker__day--today.react-datepicker__day--range-end::before {
@apply bg-primary-orange-300 !important;
}
/* 종료일 스타일 */
.react-datepicker__day--selecting-range-end,
.react-datepicker__day--selected {
@apply relative text-grayscale-50 !important;
background: linear-gradient(to right, #fff7eb 50%, transparent 50%) !important;
}
/* 범위 선택 중 종료일 스타일 유지 */
.react-datepicker__day--in-selecting-range.react-datepicker__day--selecting-range-end,
.react-datepicker__day--in-range.react-datepicker__day--range-end {
@apply text-grayscale-50 !important;
background: linear-gradient(to right, #fff7eb 50%, transparent 50%) !important;
}
/* 시작일 스타일 */
.react-datepicker__day--selecting-range-start,
.react-datepicker__day--selected {
@apply relative text-grayscale-50 !important;
background: linear-gradient(to right, #fff7eb 50%, transparent 50%) !important;
}
/* 범위 선택 중 시작일 스타일 유지 */
.react-datepicker__day--in-selecting-range.react-datepicker__day--selecting-range-start,
.react-datepicker__day--in-range.react-datepicker__day--range-start {
@apply text-grayscale-50 !important;
background: linear-gradient(to left, #fff7eb 50%, transparent 50%) !important;
}
/* 첫번째 날짜 선택 후 이전 날짜에 호버했을때 첫번째 날짜 스타일 */
.react-datepicker__day--keyboard-selected,
.react-datepicker__month-text--keyboard-selected,
.react-datepicker__quarter-text--keyboard-seleted,
.react-datepicker__year-text--keyboard-selected {
@apply bg-grayscale-50 !important;
}
.react-datepicker__day--disabled {
@apply bg-grayscale-200 bg-opacity-50;
}
.react-datepicker__day--disabled {
@apply rounded-sm bg-grayscale-100 bg-opacity-50;
}
/* --------------------- date picker커스텀 끝--------------------- */
global.css 를 작성하는데 이 분의 블로그를 참고했습니다!!
https://e-juhee.tistory.com/entry/react-datepicker
[react-datepicker] datepicker custom
react-datepicker로 커스텀 성공ㅎ_ㅎ README * : 필수 인자 // DatePicker : 데이트피커를 이용해 시작 날짜와 종료 날짜를 입력받습니다. 입력 인자 * startDate : 시작 날짜 (string) 초기값으로 현재 날짜를 stri
e-juhee.tistory.com
참고 ! global.css 에서 tailwind 유틸리티 속성을 적용하기 위해서는 @apply 를 앞에 붙여주어야 한다.

'개발개발 > WorkRoot_워크루트' 카테고리의 다른 글
| 리액트 훅 폼 (React Hook Form) 트러블 슈팅 2 - 여러 유형의 폼 데이터 제출하기 (1) | 2025.01.09 |
|---|---|
| 리액트 훅 폼 (React Hook Form) 트러블 슈팅 1 - 여러 탭에 걸친 폼 데이터 제출하기(watch, setValue 사용하기, getValues) (0) | 2025.01.03 |
| 스토리북 (storybook) 트러블 슈팅 - 2 (React-hook-form) (1) | 2025.01.03 |
| 스토리북 (storybook) 트러블 슈팅 - 1 (Tailwind, addon-styling, 컴포넌트 고민 ) (3) | 2025.01.03 |
| 스토리북 (storybook) 기본 사용 방법 (1) | 2025.01.03 |