junyeokk
Blog
React Ecosystem·2025. 11. 15

react-day-picker

날짜 선택 UI는 간단해 보이지만, 직접 만들려고 하면 생각보다 복잡하다. 달력 그리드를 렌더링하는 것 자체는 어렵지 않지만, 월 탐색, 범위 선택, 다국어 지원, 접근성, 키보드 내비게이션까지 고려하면 자체 구현은 비용이 크다. 특히 날짜 관련 로직은 로케일마다 주 시작일이 다르고(일요일 vs 월요일), 달력 체계 자체가 다른 경우(페르시아력, 히브리력)도 있어서 범용적으로 만들기가 까다롭다.

react-day-picker는 이 문제를 해결하기 위해 만들어진 React 전용 날짜 선택 라이브러리다. 2015년부터 개발되어 온 오래된 라이브러리로, 내부적으로 date-fns를 사용해 날짜 계산과 로케일 처리를 수행한다. 핵심 설계 철학은 스타일과 동작의 분리다. 달력의 구조와 선택 로직은 라이브러리가 제공하고, 스타일링은 완전히 사용자에게 위임한다. CSS 변수 기반 기본 스타일을 제공하면서도, Tailwind CSS나 CSS Modules 등 어떤 스타일링 방식이든 쉽게 적용할 수 있다.


설치와 기본 사용법

bash
npm install react-day-picker

v9부터 date-fns가 peer dependency가 아닌 내부 dependency로 포함되었다. 별도로 date-fns를 설치할 필요가 없다. 프로젝트에서 date-fns를 직접 사용하지 않는다면 의존성에서 제거해도 된다.

가장 기본적인 날짜 선택 컴포넌트:

tsx
import { useState } from "react";
import { DayPicker } from "react-day-picker";
import "react-day-picker/style.css";

function MyDatePicker() {
  const [selected, setSelected] = useState<Date>();

  return (
    <DayPicker
      mode="single"
      selected={selected}
      onSelect={setSelected}
    />
  );
}

mode prop으로 선택 방식을 지정하고, selectedonSelect로 상태를 제어한다. 기본 스타일시트를 import하면 macOS 스타일의 깔끔한 달력이 렌더링된다.


선택 모드 (Selection Modes)

DayPicker는 네 가지 선택 모드를 제공한다. 각 모드에 따라 selected의 타입과 onSelect 콜백의 시그니처가 달라진다.

single — 단일 날짜 선택

가장 기본적인 모드. 하나의 날짜만 선택할 수 있다.

tsx
const [selected, setSelected] = useState<Date>();

<DayPicker
  mode="single"
  selected={selected}
  onSelect={setSelected}
/>

selectedDate | undefined 타입이다. 이미 선택된 날짜를 다시 클릭하면 선택이 해제된다. required prop을 추가하면 항상 하나의 날짜가 선택된 상태를 유지할 수 있다.

tsx
<DayPicker mode="single" required selected={selected} onSelect={setSelected} />

multiple — 다중 날짜 선택

여러 날짜를 개별적으로 선택할 수 있다.

tsx
const [selected, setSelected] = useState<Date[]>([]);

<DayPicker
  mode="multiple"
  selected={selected}
  onSelect={setSelected}
  min={1}   // 최소 선택 개수
  max={5}   // 최대 선택 개수
/>

selectedDate[] 타입이다. minmax로 선택 가능한 날짜 수를 제한할 수 있다. 휴일 선택, 출석 체크 같은 UI에 적합하다.

range — 날짜 범위 선택

시작일과 종료일을 선택해서 범위를 지정한다. 호텔 예약, 휴가 신청 같은 기간 선택 UI에 필수적인 모드다.

tsx
import { DateRange, DayPicker } from "react-day-picker";

const [range, setRange] = useState<DateRange>();

<DayPicker
  mode="range"
  selected={range}
  onSelect={setRange}
  min={1}   // 최소 일수
  max={30}  // 최대 일수
/>

DateRange{ from: Date; to: Date } 구조다. 첫 번째 클릭이 시작일, 두 번째 클릭이 종료일이 된다. 시작일 이전 날짜를 클릭하면 시작일이 재설정된다. 시작일과 종료일 사이의 날짜들은 자동으로 하이라이트된다.

default — 커스텀 선택 로직

내장 선택 로직을 사용하지 않고 직접 구현할 때 사용한다.

tsx
const [selectedDays, setSelectedDays] = useState<Date[]>([]);

<DayPicker
  mode="default"
  modifiers={{ selected: selectedDays }}
  onDayClick={(day) => {
    // 커스텀 선택 로직
    setSelectedDays((prev) =>
      prev.some((d) => isSameDay(d, day))
        ? prev.filter((d) => !isSameDay(d, day))
        : [...prev, day]
    );
  }}
/>

mode="default"에서는 selected/onSelect 대신 modifiersonDayClick을 조합한다. 특정 요일만 선택 가능하게 하거나, 짝수일만 선택 가능하게 하는 등 내장 모드로 처리할 수 없는 복잡한 선택 규칙을 구현할 때 유용하다.


날짜 비활성화 (Disabling Dates)

특정 날짜의 선택을 막으려면 disabled prop을 사용한다. 다양한 형태의 값을 받을 수 있다.

tsx
// 1. 특정 날짜 비활성화
<DayPicker disabled={new Date(2026, 0, 1)} />

// 2. 여러 날짜 비활성화
<DayPicker disabled={[new Date(2026, 0, 1), new Date(2026, 11, 25)]} />

// 3. 날짜 범위 비활성화
<DayPicker disabled={{ from: new Date(2026, 0, 1), to: new Date(2026, 0, 15) }} />

// 4. 요일 비활성화 (0: 일, 6: 토)
<DayPicker disabled={{ dayOfWeek: [0, 6] }} />

// 5. 함수로 비활성화 조건 지정
<DayPicker disabled={(date) => date < new Date()} />

함수 형태가 가장 유연하다. 과거 날짜를 모두 비활성화하거나, 특정 API에서 받아온 날짜 목록만 활성화하는 등의 동적 조건을 구현할 수 있다.

disabled와 별개로 hidden prop도 있다. disabled는 날짜를 보여주되 선택할 수 없게 만들고, hidden은 아예 달력에서 보이지 않게 만든다.


Modifiers — 날짜에 의미 부여하기

Modifier는 DayPicker의 핵심 개념 중 하나다. 특정 날짜에 의미를 부여하고, 그에 따른 스타일이나 동작을 적용하는 메커니즘이다.

내장 modifier로 selected, disabled, hidden, today가 있고, 커스텀 modifier를 정의할 수 있다.

tsx
<DayPicker
  modifiers={{
    holiday: [new Date(2026, 0, 1), new Date(2026, 2, 1)],
    weekend: { dayOfWeek: [0, 6] },
    payday: (date) => date.getDate() === 25,
  }}
  modifiersClassNames={{
    holiday: "bg-red-100 text-red-800",
    weekend: "text-gray-400",
    payday: "font-bold underline",
  }}
/>

modifiers prop에 이름-조건 쌍을 정의하면, 해당 조건에 맞는 날짜에 자동으로 data attribute가 추가된다. modifiersClassNamesmodifiersStyles로 각 modifier에 대한 스타일을 지정할 수 있다. onDayClick 콜백에서도 modifiers 객체를 통해 클릭한 날짜가 어떤 modifier에 해당하는지 확인할 수 있다.

tsx
<DayPicker
  modifiers={{ booked: bookedDates }}
  onDayClick={(day, modifiers) => {
    if (modifiers.booked) {
      alert("이 날짜는 이미 예약되었습니다.");
      return;
    }
    // 선택 처리
  }}
/>

월 탐색과 표시 범위

기본 탐색

기본적으로 이전/다음 월 화살표로 탐색한다. startMonthendMonth로 탐색 가능 범위를 제한할 수 있다.

tsx
<DayPicker
  startMonth={new Date(2025, 0)}  // 2025년 1월부터
  endMonth={new Date(2027, 11)}   // 2027년 12월까지
/>

드롭다운 탐색

captionLayout prop으로 월/년 선택 방식을 변경할 수 있다.

tsx
<DayPicker
  captionLayout="dropdown"        // 월+연도 드롭다운
  startMonth={new Date(2020, 0)}
  endMonth={new Date(2030, 11)}
/>

드롭다운 모드에서는 startMonthendMonth가 필수다. 드롭다운 옵션의 범위를 결정하기 때문이다.

다중 월 표시

numberOfMonths로 한 번에 여러 달을 표시할 수 있다.

tsx
<DayPicker
  numberOfMonths={2}    // 2개월 동시 표시
  pagedNavigation       // 2개월씩 이동 (없으면 1개월씩)
/>

range 모드와 함께 사용하면 체크인/체크아웃 날짜 선택 UI를 깔끔하게 구현할 수 있다.


로케일 설정

DayPicker는 date-fns의 로케일 시스템을 그대로 활용한다. v9부터는 react-day-picker/locale에서 로케일을 직접 import할 수 있다.

tsx
import { DayPicker } from "react-day-picker";
import { ko } from "react-day-picker/locale";

function KoreanCalendar() {
  return <DayPicker locale={ko} />;
}

이것만으로 월 이름, 요일 이름, 날짜 포맷이 한국어로 변경된다.

주 시작일 변경

로케일에 따라 주 시작일이 자동으로 설정되지만, weekStartsOn으로 직접 지정할 수도 있다.

tsx
<DayPicker
  locale={ko}
  weekStartsOn={1}  // 0: 일요일, 1: 월요일
/>

한국에서는 보통 일요일(0)이 주 시작일이지만, 유럽 스타일로 월요일 시작을 선호하는 경우가 있다. 이 옵션으로 로케일의 기본값을 오버라이드할 수 있다.

RTL 지원

아랍어, 히브리어 같은 RTL(Right-to-Left) 언어도 dir prop으로 지원한다.

tsx
import { arSA } from "react-day-picker/locale";

<DayPicker locale={arSA} dir="rtl" />

숫자 체계

v9에서 추가된 numerals prop으로 아라비아 숫자, 태국 숫자 등 다양한 숫자 체계를 적용할 수 있다.

tsx
import { hi } from "react-day-picker/locale";

<DayPicker locale={hi} numerals="deva" />  // 데바나가리 숫자

스타일링

DayPicker의 스타일링 시스템은 세 가지 레이어로 구성된다.

1. CSS 변수 — 기본 테마 커스터마이징

기본 스타일시트를 import한 후, CSS 변수를 오버라이드하는 방식이다. 가장 간단하게 룩앤필을 변경할 수 있다.

css
/* 기본 스타일 import */
@import "react-day-picker/style.css";

.rdp-root {
  --rdp-accent-color: #3b82f6;              /* 선택된 날짜 색상 */
  --rdp-accent-background-color: #eff6ff;   /* 선택된 날짜 배경 */
  --rdp-day-height: 40px;                   /* 날짜 셀 높이 */
  --rdp-day-width: 40px;                    /* 날짜 셀 너비 */
  --rdp-today-color: #ef4444;               /* 오늘 날짜 색상 */
}

다크모드는 CSS 변수를 조건부로 오버라이드하면 된다.

css
[data-theme="dark"] .rdp-root {
  --rdp-accent-color: #60a5fa;
  --rdp-accent-background-color: #1e3a5f;
}

주요 CSS 변수 정리:

변수설명기본값
--rdp-accent-color선택 날짜, UI 요소의 액센트 색상
--rdp-accent-background-color선택 날짜의 배경색
--rdp-day-height / --rdp-day-width날짜 셀 크기44px
--rdp-today-color오늘 날짜 텍스트 색상
--rdp-outside-opacity이전/다음 달 날짜의 투명도
--rdp-disabled-opacity비활성 날짜의 투명도
--rdp-range_middle-background-color범위 중간 날짜 배경색
--rdp-range_middle-color범위 중간 날짜 텍스트색
--rdp-months-gap다중 월 표시 시 월 사이 간격

2. classNames — 클래스명 오버라이드

모든 UI 요소의 클래스명을 직접 지정할 수 있다.

tsx
<DayPicker
  classNames={{
    root: "my-calendar",
    day: "my-day",
    selected: "my-selected",
    today: "my-today",
    disabled: "my-disabled",
  }}
/>

3. Tailwind CSS 통합

Tailwind CSS를 사용한다면 getDefaultClassNamesclassNames를 조합해서 기본 클래스를 확장할 수 있다.

tsx
import { DayPicker, getDefaultClassNames } from "react-day-picker";

function TailwindCalendar() {
  const defaultClassNames = getDefaultClassNames();

  return (
    <DayPicker
      mode="single"
      classNames={{
        today: "border-amber-500",
        selected: "bg-amber-500 border-amber-500 text-white",
        root: `${defaultClassNames.root} shadow-lg p-5`,
        chevron: `${defaultClassNames.chevron} fill-amber-500`,
      }}
    />
  );
}

getDefaultClassNames()으로 기본 클래스명을 가져와서, 필요한 부분만 Tailwind 클래스로 확장하는 패턴이다. 기본 레이아웃은 유지하면서 색상이나 그림자 같은 시각적 요소만 Tailwind로 처리할 수 있다.

CSS Modules 사용

번들러가 CSS Modules를 지원한다면 react-day-picker/style.module.css를 import해서 사용할 수 있다.

tsx
import { DayPicker } from "react-day-picker";
import classNames from "react-day-picker/style.module.css";

<DayPicker classNames={classNames} />

포맷 커스터마이징

formatters prop으로 달력에 표시되는 텍스트의 포맷을 세밀하게 제어할 수 있다.

tsx
import { format } from "date-fns";

<DayPicker
  formatters={{
    formatCaption: (date, options) => format(date, "yyyy년 MMMM", options),
    formatWeekdayName: (date, options) => format(date, "EEE", options),
  }}
/>

기본 포맷이 마음에 들지 않거나, 특정 서비스의 디자인 가이드에 맞춰야 할 때 유용하다. formatCaption은 달력 상단의 "2026년 2월" 같은 캡션, formatWeekdayName은 "월", "화" 같은 요일 헤더를 제어한다.


Input 필드 연동

DayPicker 자체는 달력 그리드만 렌더링한다. input 필드와 연동하려면 직접 조합해야 한다. 팝오버 형태의 날짜 입력 UI를 만드는 전형적인 패턴:

tsx
import { useState, useRef } from "react";
import { DayPicker } from "react-day-picker";
import { format, parse, isValid } from "date-fns";

function DateInput() {
  const [selected, setSelected] = useState<Date>();
  const [inputValue, setInputValue] = useState("");
  const [isOpen, setIsOpen] = useState(false);

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value);
    const parsed = parse(e.target.value, "yyyy-MM-dd", new Date());
    if (isValid(parsed)) {
      setSelected(parsed);
    }
  };

  const handleDaySelect = (day: Date | undefined) => {
    if (day) {
      setSelected(day);
      setInputValue(format(day, "yyyy-MM-dd"));
    }
    setIsOpen(false);
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={handleInputChange}
        onFocus={() => setIsOpen(true)}
        placeholder="YYYY-MM-DD"
      />
      {isOpen && (
        <DayPicker
          mode="single"
          selected={selected}
          onSelect={handleDaySelect}
          month={selected}  // 선택된 날짜의 월로 이동
        />
      )}
    </div>
  );
}

핵심은 양방향 동기화다. input에 직접 타이핑하면 달력의 선택 상태가 업데이트되고, 달력에서 날짜를 클릭하면 input의 텍스트가 업데이트된다. month prop에 selected를 전달하면 선택된 날짜가 있는 월로 달력이 자동 이동한다.

실제 프로덕션에서는 Popover 컴포넌트(Radix UI의 @radix-ui/react-popover 등)를 사용해서 포커스 트래핑과 외부 클릭 닫기를 구현하는 것이 좋다.


접근성 (Accessibility)

DayPicker는 WAI-ARIA 패턴을 따르며, 키보드 내비게이션을 기본 지원한다.

  • 화살표 키: 날짜 간 이동
  • Enter / Space: 날짜 선택
  • Page Up / Page Down: 이전/다음 월 이동
  • Shift + Page Up / Page Down: 이전/다음 연도 이동
  • Home / End: 주의 시작/끝으로 이동

각 날짜 셀에는 자동으로 aria-label이 생성된다. labels prop으로 이 레이블을 커스터마이징할 수도 있다. 스크린 리더 사용자에게 더 친절한 날짜 정보를 제공할 수 있다.


커스텀 컴포넌트

DayPicker의 모든 내부 요소를 커스텀 컴포넌트로 교체할 수 있다. components prop을 사용한다.

tsx
import { DayPicker, type DayProps } from "react-day-picker";

function CustomDay(props: DayProps) {
  const { day, ...rest } = props;
  const isToday = day.date.toDateString() === new Date().toDateString();

  return (
    <td {...rest}>
      {day.date.getDate()}
      {isToday && <span className="dot" />}
    </td>
  );
}

<DayPicker components={{ Day: CustomDay }} />

캡션, 네비게이션 버튼, 요일 헤더 등 거의 모든 부분을 교체할 수 있어서, DayPicker를 "달력 로직 엔진"으로만 사용하고 렌더링은 완전히 커스텀하는 것도 가능하다.


애니메이션

v9에서 animate prop이 추가되었다. 월 전환 시 슬라이드 애니메이션이 적용된다.

tsx
<DayPicker animate />

CSS 변수로 애니메이션 속도와 타이밍을 조절할 수 있다.

css
.rdp-root {
  --rdp-animation_duration: 0.3s;
  --rdp-animation_timing: cubic-bezier(0.4, 0, 0.2, 1);
}

다른 날짜 선택 라이브러리와 비교

특성react-day-pickerreact-datepicker@mui/x-date-pickers
번들 크기~25KB (gzip)~40KB (gzip)큼 (MUI 의존)
스타일링CSS 변수, classNames, 완전 커스텀CSS 오버라이드MUI 테마 시스템
날짜 라이브러리date-fns (내장)date-fns (peer)dayjs/luxon/date-fns 선택
선택 모드single/multiple/range/customsingle/rangesingle/range
접근성WAI-ARIA, 키보드 내장부분적완전 지원
Input 포함❌ (별도 구현)✅ (내장)✅ (내장)
다국어date-fns localedate-fns localeadapter별 다름
비달력 체계페르시아, 히브리, 불교력 등adapter별 제한적

react-day-picker의 강점은 "달력 그리드"에 집중한다는 점이다. input 필드가 내장되지 않은 대신, 커스터마이징 자유도가 높고 번들 크기가 작다. 디자인 시스템에 맞는 자체 input을 만들어야 하는 프로젝트에 적합하다.

react-datepicker는 input + 달력이 하나로 묶인 "완성형" 컴포넌트다. 빠르게 날짜 입력 UI를 구현해야 할 때 편리하지만, 커스터마이징 여지가 상대적으로 적다.

@mui/x-date-pickers는 MUI 프로젝트에서 쓰기 좋지만, MUI 없이 단독으로 쓰기엔 의존성이 무겁다.


shadcn/ui의 Calendar 컴포넌트

shadcn/ui의 Calendar 컴포넌트는 react-day-picker를 기반으로 만들어졌다. shadcn/ui를 사용 중이라면 DayPicker를 직접 쓰는 것보다 shadcn의 Calendar를 사용하는 것이 일관된 디자인을 유지하기 좋다.

bash
npx shadcn@latest add calendar

내부적으로 react-day-picker의 classNames prop에 Tailwind 클래스를 전달하는 방식이다. 커스터마이징이 필요하면 생성된 components/ui/calendar.tsx 파일을 직접 수정하면 된다.


정리

react-day-picker는 "달력 그리드 렌더링과 날짜 선택 로직"이라는 본질에 집중한 라이브러리다. input 필드를 내장하지 않는 대신, 어떤 UI 프레임워크와도 자연스럽게 조합할 수 있는 유연성을 제공한다. date-fns의 로케일 시스템을 그대로 활용하기 때문에 다국어 지원이 자연스럽고, CSS 변수 기반 스타일링으로 테마 커스터마이징이 간편하다. Headless UI 철학에 공감하면서도, 기본 스타일이 전혀 없는 것이 불편한 개발자에게 적절한 균형점을 제공하는 라이브러리다.

관련 문서

  • Radix UI - shadcn/ui의 기반이 되는 Headless UI 라이브러리
  • date-fns - react-day-picker 내부에서 사용하는 날짜 유틸리티
  • React i18next - 다국어 지원과 로케일 처리