useCalendar
μ μΈμ μ΄κ³ νμ μμ ν λ¬λ ₯ Hookμ λλ€. UIλ μμ ν μμ λ‘κ² κ΅¬μ±νκ³ , λ°μ΄ν°μ λ‘μ§λ§ μ 곡λ°μ΅λλ€.
μ λ§λ€μλμ?
κΈ°μ‘΄ λ¬λ ₯ λΌμ΄λΈλ¬λ¦¬λ€μ UIκ° κ°μ λκ±°λ, date κ³μ° λ‘μ§μ μ§μ ꡬνν΄μΌ νλ λΆνΈν¨μ΄ μμμ΅λλ€.
// β κΈ°μ‘΄ λ°©μ 1: UIκ° κ°μ λ¨
<ReactCalendar />; // μ€νμΌ μ»€μ€ν°λ§μ΄μ§ μ΄λ €μ
// β κΈ°μ‘΄ λ°©μ 2: λ‘μ§μ μ§μ ꡬν
const [currentMonth, setCurrentMonth] = useState(new Date());
const daysInMonth = getDaysInMonth(currentMonth); // μ§μ κ³μ°
const firstDayOfWeek = getDay(startOfMonth(currentMonth)); // 볡μ‘...
// μ΄μ λ¬/λ€μλ¬ λ μ§λ...? π±
// β
useCalendar: λ°μ΄ν°λ§ λ°κ³ , UIλ μμ λ‘κ²
const cal = useCalendar();
// cal.days β 42κ° λ μ§ (μ΄μ λ¬+νμ¬λ¬+λ€μλ¬ ν¬ν¨)
// cal.prev(), cal.next() β λ€λΉκ²μ΄μ
// λλ¨Έμ§λ λ΄ λ§μλλ‘!μ£Όμ νΉμ§
1. Headless UI (UI μμ μμ )
μ€νμΌ, λ μ΄μμ, μ λλ©μ΄μ λͺ¨λ μ§μ μ μ΄ν μ μμ΅λλ€.
2. νμ μμ μ±
TypeScriptλ‘ μμ±λμ΄ IDE μλμμ±κ³Ό νμ 체ν¬λ₯Ό μ 곡ν©λλ€.
3. SSR μμ
Next.js, Remix λ± SSR νκ²½μμλ hydration μλ¬ μμ΄ λμν©λλ€.
4. μ±λ₯ μ΅μ ν
λΆνμν μ¬κ³μ°μ λ°©μ§νκ³ , μμ μ μΈ μ°Έμ‘°λ₯Ό μ μ§ν©λλ€.
μ€μΉ
npm install @frontend-toolkit-js/hooks
# or
pnpm add @frontend-toolkit-js/hooksκΈ°λ³Έ μ¬μ©λ²
1. κ°μ₯ κ°λ¨ν μμ
import { useCalendar } from '@frontend-toolkit-js/hooks';
function MyCalendar() {
const cal = useCalendar();
return (
<div>
{/* ν€λ: μ΄μ /λ€μ λ²νΌ */}
<div>
<button onClick={cal.prev}>β μ΄μ </button>
<h2>
{cal.currentDate.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
})}
</h2>
<button onClick={cal.next}>λ€μ βΆ</button>
</div>
<button onClick={cal.today}>μ€λ</button>
{/* μμΌ ν€λ */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
{cal.weekdays.map(weekday => (
<div key={weekday.index}>{weekday.label}</div>
))}
</div>
{/* λ μ§ κ·Έλ¦¬λ */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
{cal.days.map((day, i) => (
<div
key={i}
style={{
opacity: day.isCurrentMonth ? 1 : 0.3,
fontWeight: day.isToday ? 'bold' : 'normal',
color: day.isWeekend ? 'red' : 'black',
}}
>
{day.day}
</div>
))}
</div>
</div>
);
}API Reference
Options
interface UseCalendarOptions {
defaultDate?: Date | string | number;
weekStartsOn?: 0 | 1;
onChange?: (date: Date) => void;
}defaultDate
μ΄κΈ° νμν λ μ§μ λλ€. λ€μν νμμ μ§μν©λλ€.
// Date κ°μ²΄
useCalendar({ defaultDate: new Date(2024, 0, 15) });
// ISO λ¬Έμμ΄
useCalendar({ defaultDate: '2024-01-15' });
// λ
-μλ§ (μΌμ μλμΌλ‘ 1μΌ)
useCalendar({ defaultDate: '2024-01' });
// timestamp
useCalendar({ defaultDate: 1705276800000 });
// URLμμ κ°μ Έμ€κΈ°
const params = new URLSearchParams(window.location.search);
useCalendar({ defaultDate: params.get('date') });- νμ
:
Date | string | number - κΈ°λ³Έκ°:
new Date()(μ€λ)
weekStartsOn
μ£Όμ μμ μμΌμ μ€μ ν©λλ€.
// μΌμμΌ μμ (νκ΅ λ¬λ ₯)
useCalendar({ weekStartsOn: 0 });
// μμμΌ μμ (ISO 8601 νμ€)
useCalendar({ weekStartsOn: 1 });- νμ
:
0 | 1 - κΈ°λ³Έκ°:
0(μΌμμΌ)
onChange
λ μ§κ° λ³κ²½λ λ νΈμΆλλ μ½λ°±μ λλ€.
useCalendar({
onChange: date => {
console.log('λ μ§ λ³κ²½:', date);
// URL μ
λ°μ΄νΈ
const params = new URLSearchParams();
params.set('date', format(date, 'yyyy-MM'));
window.history.pushState({}, '', `?${params}`);
},
});- νμ
:
(date: Date) => void - κΈ°λ³Έκ°:
undefined
λ°νκ°
{
days: CalendarDay[];
weekdays: Weekday[];
currentDate: Date;
prev: () => void;
next: () => void;
today: () => void;
setDate: (date: Date) => void;
}days: CalendarDay[]
λ¬λ ₯μ νμν 42κ°μ λ μ§ λ°°μ΄μ λλ€. (6μ£Ό Γ 7μΌ)
interface CalendarDay {
date: Date; // μ 체 Date κ°μ²΄
day: number; // μΌ (1-31)
isCurrentMonth: boolean; // νμ¬ νμ μμ μνλκ°?
isToday: boolean; // μ€λμΈκ°?
isWeekend: boolean; // μ£Όλ§μΈκ°? (ν /μΌ)
}μ 42κ°?
- λλΆλΆμ λ¬μ 35κ°(5μ£Ό)λ‘ μΆ©λΆνμ§λ§, 6μ£Όκ° νμν κ²½μ°λ μμ
- κ³ μ ν¬κΈ°λ‘ λ μ΄μμ μμ μ± ν보
- μ΄μ λ¬/λ€μ λ¬ λ μ§λ ν¬ν¨νμ¬ μμ ν μ£Ό λ¨μ νμ
// μ΄μ λ¬ λ μ§ ν리κ²
{
cal.days.map(day => (
<div style={{ opacity: day.isCurrentMonth ? 1 : 0.3 }}>{day.day}</div>
));
}weekdays: Weekday[]
μμΌ ν€λλ₯Ό μν 7κ°μ μμΌ λ°°μ΄μ λλ€.
interface Weekday {
date: Date; // Date κ°μ²΄
label: string; // μ§§μ λ μ΄λΈ ("μ", "ν", ...)
labelLong: string; // κΈ΄ λ μ΄λΈ ("μμμΌ", "νμμΌ", ...)
index: number; // μμΌ μΈλ±μ€ (0-6)
}// μμΌ ν€λ
{
cal.weekdays.map(weekday => (
<div key={weekday.index}>
{weekday.label} {/* "μ", "ν", ... */}
</div>
));
}currentDate: Date
νμ¬ νμ μ€μΈ λ¬μ Date κ°μ²΄μ λλ€.
<h2>
{cal.currentDate.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'long',
})}
</h2>
// β "2024λ
1μ"prev(): void
μ΄μ λ¬λ‘ μ΄λν©λλ€.
<button onClick={cal.prev}>β μ΄μ λ¬</button>next(): void
λ€μ λ¬λ‘ μ΄λν©λλ€.
<button onClick={cal.next}>λ€μ λ¬ βΆ</button>today(): void
μ€λμ΄ μν λ¬λ‘ μ΄λν©λλ€.
<button onClick={cal.today}>μ€λ</button>setDate(date: Date): void
νΉμ λ μ§λ‘ μ§μ μ€μ ν©λλ€.
<button onClick={() => cal.setDate(new Date(2024, 11, 25))}>
ν¬λ¦¬μ€λ§μ€λ‘ μ΄λ
</button>μ€λ¬΄ μμ
1. λ μ§ μ ν κΈ°λ₯
import { useState } from 'react';
import { useCalendar } from '@frontend-toolkit-js/hooks';
import { isSameDay } from 'date-fns';
function DatePicker() {
const cal = useCalendar();
const [selected, setSelected] = useState<Date | null>(null);
return (
<div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
{cal.days.map((day, i) => {
const isSelected = selected && isSameDay(selected, day.date);
return (
<button
key={i}
onClick={() => setSelected(day.date)}
style={{
opacity: day.isCurrentMonth ? 1 : 0.3,
fontWeight: day.isToday ? 'bold' : 'normal',
backgroundColor: isSelected ? 'blue' : 'transparent',
color: isSelected ? 'white' : day.isWeekend ? 'red' : 'black',
}}
>
{day.day}
</button>
);
})}
</div>
{selected && <p>μ νλ λ μ§: {selected.toLocaleDateString('ko-KR')}</p>}
</div>
);
}2. URL λκΈ°ν (Next.js App Router)
'use client';
import { useSearchParams, useRouter } from 'next/navigation';
import { useCalendar } from '@frontend-toolkit-js/hooks';
import { format } from 'date-fns';
function CalendarWithURL() {
const searchParams = useSearchParams();
const router = useRouter();
const cal = useCalendar({
defaultDate: searchParams.get('date') ?? undefined,
onChange: date => {
const params = new URLSearchParams(searchParams);
params.set('date', format(date, 'yyyy-MM'));
router.push(`?${params.toString()}`);
},
});
return (
<div>
<p>νμ¬ URL: ?date={searchParams.get('date')}</p>
{/* λ¬λ ₯ UI */}
</div>
);
}λμ:
- λ€λΉκ²μ΄μ ν΄λ¦ β URL μλ μ λ°μ΄νΈ
- URL μ§μ λ³κ²½ β λ¬λ ₯ μλ λ°μ
- λΈλΌμ°μ λ€λ‘κ°κΈ° β μ΄μ λ¬ λ³΅μ
3. λ²μ μ ν (체ν¬μΈ/체ν¬μμ)
import { useState } from 'react';
import { useCalendar } from '@frontend-toolkit-js/hooks';
import { isWithinInterval, isSameDay } from 'date-fns';
function DateRangePicker() {
const cal = useCalendar();
const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState<Date | null>(null);
const handleClick = (date: Date) => {
if (!startDate || (startDate && endDate)) {
// μλ‘ μμ
setStartDate(date);
setEndDate(null);
} else {
// μ’
λ£μΌ μ€μ
if (date < startDate) {
setEndDate(startDate);
setStartDate(date);
} else {
setEndDate(date);
}
}
};
return (
<div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
{cal.days.map((day, i) => {
const isStart = startDate && isSameDay(startDate, day.date);
const isEnd = endDate && isSameDay(endDate, day.date);
const isInRange =
startDate &&
endDate &&
isWithinInterval(day.date, { start: startDate, end: endDate });
return (
<button
key={i}
onClick={() => handleClick(day.date)}
style={{
backgroundColor:
isStart || isEnd ? 'blue' : isInRange ? 'lightblue' : 'white',
}}
>
{day.day}
</button>
);
})}
</div>
</div>
);
}4. μ΄λ²€νΈ νμ
import { format } from 'date-fns';
function EventCalendar() {
const cal = useCalendar();
const events = {
'2024-01-15': ['νμ 1', 'νμ 2'],
'2024-01-20': ['μμΌ νν°'],
};
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
{cal.days.map((day, i) => {
const dateKey = format(day.date, 'yyyy-MM-dd');
const dayEvents = events[dateKey] || [];
return (
<div key={i}>
<div>{day.day}</div>
{dayEvents.map((event, j) => (
<div key={j} style={{ fontSize: '10px', color: 'blue' }}>
β’ {event}
</div>
))}
</div>
);
})}
</div>
);
}κ³ κΈ μ¬μ©λ²
μμμΌ μμ (ISO 8601)
const cal = useCalendar({ weekStartsOn: 1 });
// μμΌ ν€λ: μ ν μ λͺ© κΈ ν μΌλ€κ΅μ΄ μ§μ
import { ko, enUS } from 'date-fns/locale';
// νκ΅μ΄
{
cal.weekdays.map(w => <div>{format(w.date, 'EEE', { locale: ko })}</div>);
}
// β μ, ν, μ, ...
// μμ΄
{
cal.weekdays.map(w => <div>{format(w.date, 'EEE', { locale: enUS })}</div>);
}
// β Mon, Tue, Wed, ...λ΄λΆ λμ μ리
μ 42κ° λ μ§λ₯Ό μμ±νλμ?
- 5μ£Όλ§μΌλ‘λ λΆμ‘±ν κ²½μ°κ° μμ (μ: 2025λ 3μ)
- κ³ μ ν¬κΈ° β λ μ΄μμ shift μμ
- μ΄μ /λ€μ λ¬ λ μ§ ν¬ν¨ β μμ ν μ£Ό λ¨μ νμ
μ Flat Array ꡬ쑰μΈκ°μ?
// β 2D λ°°μ΄ (μ£Ό λ¨μ)
const weeks = [
[day1, day2, ...], // 1μ£Ό
[day8, day9, ...], // 2μ£Ό
// ...
];
// β
Flat λ°°μ΄ (1μ°¨μ)
const days = [day1, day2, ..., day42];μ΄μ :
- CSS Gridλ‘ λ μ΄μμνκΈ° νΈν¨ (
gridTemplateColumns: 'repeat(7, 1fr)') .map()ν λ²μΌλ‘ λ λλ§ κ°λ₯- λ©λͺ¨λ¦¬ ν¨μ¨μ (μ€μ²© λ°°μ΄ μμ)
μ±λ₯ νΉμ±
λ²λ€ ν¬κΈ°
@frontend-toolkit-js/hooks μ체:
- ESM: 2.6 KB (minified) / 1.06 KB (gzipped)
- CJS: 2.8 KB (minified) / 1.05 KB (gzipped)
Dependencies:
date-fns: ~78 KB (νμν ν¨μλ§ tree-shaking)react: peer dependency (λ³λ μ€μΉ νμ)
π‘ μ°Έκ³ : date-fnsλ tree-shakingμ μ§μνλ―λ‘, μ€μ μ¬μ©νλ ν¨μλ§ λ²λ€μ ν¬ν¨λ©λλ€. useCalendarλ μ½ 10κ°μ date-fns ν¨μλ₯Ό μ¬μ©ν©λλ€.
λ λλ§ μ΅μ ν
useMemoλ‘ λΆνμν μ¬κ³μ° λ°©μ§useCallbackμΌλ‘ μμ μ μΈ ν¨μ μ°Έμ‘°- λ μ§ λ³κ²½ μμλ§ μ¬κ³μ°
λ©λͺ¨λ¦¬
- useState 3κ°λ§ μ¬μ©
- νμ΄λ¨Έ μμ (λ©λͺ¨λ¦¬ λμ μμ)
μ£Όμμ¬ν
β νν μ€μ
1. isCurrentMonth μ²΄ν¬ μ ν¨
// β μ΄μ /λ€μ λ¬ λ μ§λ μ§νκ² νμλ¨
{
cal.days.map(day => <div>{day.day}</div>);
}
// β
μ΄μ /λ€μ λ¬μ ν리κ²
{
cal.days.map(day => (
<div style={{ opacity: day.isCurrentMonth ? 1 : 0.3 }}>{day.day}</div>
));
}2. keyμ indexλ§ μ¬μ©
// β οΈ λμμ νμ§λ§ κΆμ₯νμ§ μμ
{
cal.days.map((day, i) => <div key={i}>{day.day}</div>);
}
// β
λ μμ μ
{
cal.days.map((day, i) => (
<div key={`${day.date.getTime()}-${i}`}>{day.day}</div>
));
}3. onChangeμμ 무ν 루ν
// β 무ν 루ν!
const [date, setDate] = useState(new Date());
useCalendar({
onChange: newDate => {
setDate(newDate);
cal.setDate(newDate); // β μ΄λ¬λ©΄ μ λ¨!
},
});
// β
onChangeλ side effectλ§
useCalendar({
onChange: newDate => {
// URL μ
λ°μ΄νΈ, λ‘κΉ
λ±λ§
updateURL(newDate);
},
});TypeScript
λͺ¨λ νμ μ΄ μλμΌλ‘ μΆλ‘ λ©λλ€.
const cal = useCalendar();
// ^? {
// days: CalendarDay[];
// weekdays: Weekday[];
// currentDate: Date;
// ...
// }
cal.days.forEach(day => {
day.isToday;
// ^? boolean (μλ μΆλ‘ )
});λΈλΌμ°μ μ§μ
- Chrome, Firefox, Safari, Edge (μ΅μ 2λ )
- React 18+
- date-fns 4.x
κ΄λ ¨ λ¬Έμ
- useIsMounted - SSR μμ μ±
- useDebouncedCallback - onChange μ΅μ ν