React 달력 라이브러리
React 달력 라이브러리 완벽 가이드: 날짜 선택부터 일정 관리까지
서론
웹 애플리케이션에서 날짜 선택과 달력 기능은 예약 시스템, 일정 관리, 폼 입력 등 다양한 곳에서 필수적입니다. React 생태계에는 여러 달력 라이브러리가 있으며, 각각의 특징과 사용 사례가 다릅니다. 이 글에서는 주요 React 달력 라이브러리들을 비교하고, Next.js와 함께 사용하는 실전 방법을 다루겠습니다.
1. 주요 달력 라이브러리 비교
1.1 라이브러리 선택 기준
- 번들 크기: 애플리케이션 성능에 직접적인 영향
- 커스터마이징: 디자인 요구사항에 맞게 수정 가능한지
- 접근성: 키보드 네비게이션과 스크린 리더 지원
- 타입스크립트 지원: 타입 안정성
- 활발한 유지보수: 최신 React 버전과의 호환성
1.2 인기 라이브러리 비교
| 라이브러리 | 번들 크기 | 특징 | 사용 사례 |
|---|---|---|---|
| react-calendar | ~15KB | 가볍고 단순한 달력 | 기본 날짜 선택 |
| react-datepicker | ~50KB | 기능이 풍부한 날짜 선택기 | 복잡한 날짜 입력 |
| react-day-picker | ~20KB | 접근성 우수, 커스터마이징 용이 | 접근성 중요한 앱 |
| date-fns | ~70KB | 날짜 유틸리티 라이브러리 | 날짜 계산과 포맷팅 |
2. react-calendar: 가벼운 달력 컴포넌트
2.1 설치 및 기본 사용
npm install react-calendar
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function SimpleCalendar() {
const [date, setDate] = useState(new Date());
return (
<div className="flex flex-col items-center p-4">
<Calendar onChange={setDate} value={date} />
<p className="mt-4">선택한 날짜: {date.toLocaleDateString()}</p>
</div>
);
}
2.2 커스터마이징
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function CustomCalendar() {
const [date, setDate] = useState(new Date());
return (
<div className="flex flex-col items-center p-4">
<Calendar
onChange={setDate}
value={date}
className="rounded-lg border-2 border-gray-300 p-4"
tileClassName={({ date, view }) => {
// 오늘 날짜 스타일링
if (date.toDateString() === new Date().toDateString()) {
return 'bg-blue-500 text-white rounded-full';
}
return null;
}}
tileDisabled={({ date }) => {
// 과거 날짜 비활성화
return date < new Date();
}}
minDate={new Date()}
maxDate={new Date(new Date().setMonth(new Date().getMonth() + 3))}
/>
</div>
);
}
3. react-datepicker: 강력한 날짜 선택기
3.1 설치 및 기본 사용
npm install react-datepicker
npm install --save-dev @types/react-datepicker
'use client';
import { useState } from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
export default function DatePickerExample() {
const [startDate, setStartDate] = useState(new Date());
return (
<div className="p-4">
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
dateFormat="yyyy-MM-dd"
className="px-4 py-2 border rounded-lg"
/>
</div>
);
}
3.2 날짜 범위 선택
'use client';
import { useState } from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
export default function DateRangePicker() {
const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(null);
return (
<div className="p-4 space-y-4">
<div>
<label className="block mb-2 font-medium">시작일</label>
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
selectsStart
startDate={startDate}
endDate={endDate}
dateFormat="yyyy-MM-dd"
className="px-4 py-2 border rounded-lg w-full"
/>
</div>
<div>
<label className="block mb-2 font-medium">종료일</label>
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
selectsEnd
startDate={startDate}
endDate={endDate}
minDate={startDate}
dateFormat="yyyy-MM-dd"
className="px-4 py-2 border rounded-lg w-full"
/>
</div>
</div>
);
}
3.3 시간 선택 포함
'use client';
import { useState } from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
export default function DateTimePicker() {
const [startDate, setStartDate] = useState(new Date());
return (
<div className="p-4">
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
className="px-4 py-2 border rounded-lg"
/>
</div>
);
}
4. react-day-picker: 접근성 중심 달력
4.1 설치 및 기본 사용
npm install react-day-picker
'use client';
import { useState } from 'react';
import { DayPicker } from 'react-day-picker';
import 'react-day-picker/dist/style.css';
export default function DayPickerExample() {
const [selected, setSelected] = useState();
return (
<div className="flex flex-col items-center p-4">
<DayPicker
mode="single"
selected={selected}
onSelect={setSelected}
className="rounded-lg border p-4"
/>
{selected && (
<p className="mt-4">선택한 날짜: {selected.toLocaleDateString()}</p>
)}
</div>
);
}
4.2 다중 날짜 선택
'use client';
import { useState } from 'react';
import { DayPicker } from 'react-day-picker';
import 'react-day-picker/dist/style.css';
export default function MultiSelectCalendar() {
const [selectedDays, setSelectedDays] = useState([]);
return (
<div className="flex flex-col items-center p-4">
<DayPicker
mode="multiple"
selected={selectedDays}
onSelect={setSelectedDays}
className="rounded-lg border p-4"
/>
<div className="mt-4">
<p className="font-medium">선택한 날짜:</p>
<ul className="list-disc">
{selectedDays.map((day) => (
<li key={day.toISOString()}>
{day.toLocaleDateString()}
</li>
))}
</ul>
</div>
</div>
);
}
5. date-fns: 날짜 유틸리티 라이브러리
5.1 날짜 포맷팅과 계산
npm install date-fns
'use client';
import { useState } from 'react';
import { format, addDays, differenceInDays, isBefore, isAfter } from 'date-fns';
import { ko } from 'date-fns/locale';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function DateUtilsExample() {
const [selectedDate, setSelectedDate] = useState(new Date());
const formattedDate = format(selectedDate, 'yyyy년 MM월 dd일', { locale: ko });
const nextWeek = addDays(selectedDate, 7);
const daysFromToday = differenceInDays(selectedDate, new Date());
return (
<div className="p-4 space-y-4">
<Calendar onChange={setSelectedDate} value={selectedDate} />
<div className="space-y-2">
<p>선택한 날짜: {formattedDate}</p>
<p>7일 후: {format(nextWeek, 'yyyy-MM-dd')}</p>
<p>오늘로부터 {daysFromToday}일 후</p>
<p>과거 날짜인가? {isBefore(selectedDate, new Date()) ? '예' : '아니오'}</p>
</div>
</div>
);
}
5.2 날짜 범위 처리
'use client';
import { useState } from 'react';
import { format, eachDayOfInterval, startOfWeek, endOfWeek } from 'date-fns';
import { ko } from 'date-fns/locale';
export default function DateRangeUtils() {
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(addDays(new Date(), 7));
const daysInRange = eachDayOfInterval({
start: startDate,
end: endDate,
});
const weekStart = startOfWeek(startDate, { locale: ko });
const weekEnd = endOfWeek(startDate, { locale: ko });
return (
<div className="p-4 space-y-4">
<div>
<p>시작일: {format(startDate, 'yyyy-MM-dd')}</p>
<p>종료일: {format(endDate, 'yyyy-MM-dd')}</p>
</div>
<div>
<p className="font-medium">범위 내 날짜들:</p>
<ul className="list-disc pl-5">
{daysInRange.map((day) => (
<li key={day.toISOString()}>
{format(day, 'yyyy-MM-dd (EEE)', { locale: ko })}
</li>
))}
</ul>
</div>
<div>
<p>주 시작: {format(weekStart, 'yyyy-MM-dd')}</p>
<p>주 종료: {format(weekEnd, 'yyyy-MM-dd')}</p>
</div>
</div>
);
}
6. Next.js와 통합
6.1 동적 임포트로 번들 최적화
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const Calendar = dynamic(() => import('react-calendar'), {
ssr: false,
loading: () => <div className="h-64 bg-gray-100 rounded animate-pulse" />,
});
export default function OptimizedCalendar() {
const [date, setDate] = useState(new Date());
return (
<div className="p-4">
<Calendar onChange={setDate} value={date} />
</div>
);
}
6.2 서버 컴포넌트와 클라이언트 컴포넌트 분리
// app/calendar/page.js (서버 컴포넌트)
import CalendarClient from './CalendarClient';
export default function CalendarPage() {
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-4">달력</h1>
<CalendarClient />
</div>
);
}
// app/calendar/CalendarClient.js (클라이언트 컴포넌트)
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function CalendarClient() {
const [date, setDate] = useState(new Date());
return <Calendar onChange={setDate} value={date} />;
}
7. Tailwind CSS 스타일링
7.1 react-calendar 스타일링
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function StyledCalendar() {
const [date, setDate] = useState(new Date());
return (
<div className="p-4">
<Calendar
onChange={setDate}
value={date}
className="!border-2 !border-gray-300 !rounded-lg !p-4"
tileClassName="hover:!bg-blue-100 !rounded"
activeStartDate={date}
/>
<style jsx global>{`
.react-calendar {
font-family: inherit;
}
.react-calendar__tile--active {
background: #3b82f6;
color: white;
}
.react-calendar__tile--now {
background: #dbeafe;
}
`}</style>
</div>
);
}
7.2 커스텀 스타일 컴포넌트
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function CustomStyledCalendar() {
const [date, setDate] = useState(new Date());
return (
<div className="p-4">
<div className="bg-white rounded-xl shadow-lg p-6 max-w-md mx-auto">
<Calendar
onChange={setDate}
value={date}
className="!border-0 !w-full"
/>
</div>
</div>
);
}
8. 실전 예제: 예약 시스템 달력
8.1 예약 가능한 날짜만 표시
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function BookingCalendar() {
const [selectedDate, setSelectedDate] = useState(null);
// 예약 가능한 날짜 (실제로는 API에서 가져옴)
const availableDates = [
new Date(2025, 4, 15),
new Date(2025, 4, 16),
new Date(2025, 4, 20),
new Date(2025, 4, 25),
];
const isDateAvailable = (date) => {
return availableDates.some(
(availableDate) =>
date.toDateString() === availableDate.toDateString()
);
};
return (
<div className="p-4 max-w-md mx-auto">
<h2 className="text-2xl font-bold mb-4">예약 날짜 선택</h2>
<Calendar
onChange={setSelectedDate}
value={selectedDate}
tileDisabled={({ date }) => !isDateAvailable(date)}
tileClassName={({ date }) => {
if (isDateAvailable(date)) {
return 'bg-green-100 text-green-800 font-semibold';
}
return 'opacity-50';
}}
minDate={new Date()}
/>
{selectedDate && (
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
<p className="font-semibold">선택한 날짜:</p>
<p>{selectedDate.toLocaleDateString('ko-KR')}</p>
</div>
)}
</div>
);
}
9. 접근성 고려사항
9.1 키보드 네비게이션
'use client';
import { useState } from 'react';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function AccessibleCalendar() {
const [date, setDate] = useState(new Date());
return (
<div className="p-4">
<div role="application" aria-label="달력">
<Calendar
onChange={setDate}
value={date}
nextLabel="다음 달"
prevLabel="이전 달"
next2Label="다음 해"
prev2Label="이전 해"
/>
</div>
</div>
);
}
10. 성능 최적화
10.1 메모이제이션
'use client';
import { useState, useMemo, useCallback } from 'react';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
export default function OptimizedCalendar() {
const [date, setDate] = useState(new Date());
const handleDateChange = useCallback((newDate) => {
setDate(newDate);
}, []);
const tileClassName = useCallback(({ date, view }) => {
if (view === 'month') {
if (date.toDateString() === new Date().toDateString()) {
return 'bg-blue-500 text-white';
}
}
return null;
}, []);
return (
<div className="p-4">
<Calendar
onChange={handleDateChange}
value={date}
tileClassName={tileClassName}
/>
</div>
);
}
11. 결론
React 달력 라이브러리는 각각의 특징과 장단점이 있습니다. 프로젝트의 요구사항에 따라 적절한 라이브러리를 선택하는 것이 중요합니다.
선택 가이드:
- 간단한 날짜 선택: react-calendar
- 복잡한 날짜 입력: react-datepicker
- 접근성 중요: react-day-picker
- 날짜 계산: date-fns
Next.js와 함께 사용할 때는 동적 임포트를 활용하여 번들 크기를 최적화하고, 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 분리하여 성능을 향상시킬 수 있습니다. 또한 Tailwind CSS와 함께 사용하면 일관된 디자인을 쉽게 구현할 수 있습니다.