사용자 정의 UI 빌드하기

이 가이드는 헤드리스 훅 위에 자신만의 날짜 선택기 UI를 빌드하는 패턴을 안내합니다.

패턴

모든 헤드리스 훅은 동일한 패턴을 따릅니다:

  1. 훅 호출: 옵션(value, onChange, configuration)과 함께 훅을 호출합니다.
  2. 반환 값 분해: 상태, 계산된 값, 핸들러를 얻기 위해 반환 값을 구조 분해합니다.
  3. 자체 마크업 렌더링: 반환된 값을 사용하여 자신만의 마크업을 렌더링합니다.
  4. 상호작용 연결: 제공된 핸들러를 호출하여 상호작용을 연결합니다.

훅은 캘린더 생성, 날짜 계산, 열기/닫기 상태, 키보드 탐색, 범위 로직 등 모든 복잡성을 관리하며, 여러분은 순수하게 표현에만 집중하면 됩니다.

예제: 사용자 정의 날짜 선택기

import { useState } from "react";
import { useDatePicker } from "react-date-range-picker-headless";
function CustomDatePicker() {
const [value, setValue] = useState<Date | null>(null);
const {
isOpen,
calendar,
getDayProps,
displayValue,
hasValue,
canConfirm,
locale,
handleToggle,
handleDateClick,
handleConfirm,
handleCancel,
handleClear,
handlePrevMonth,
handleNextMonth,
handleKeyDown,
containerRef,
popupRef,
} = useDatePicker({ value, onChange: setValue });
return (
<div ref={containerRef} style={{ position: "relative", display: "inline-block" }}>
{/* Trigger */}
<button
onClick={handleToggle}
style={{
padding: "8px 16px",
border: "1px solid #d1d5db",
borderRadius: 6,
background: "white",
cursor: "pointer",
minWidth: 180,
textAlign: "left",
}}
>
{displayValue || locale.placeholder}
</button>
{/* Popup */}
{isOpen && (
<div
ref={popupRef}
onKeyDown={handleKeyDown}
style={{
position: "absolute",
top: "100%",
left: 0,
marginTop: 4,
padding: 16,
background: "white",
border: "1px solid #e5e7eb",
borderRadius: 8,
boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
zIndex: 50,
}}
>
{/* Header */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
}}
>
<button onClick={handlePrevMonth}>{locale.prevMonth}</button>
<span style={{ fontWeight: 600 }}>{locale.formatMonthYear(calendar.month)}</span>
<button onClick={handleNextMonth}>{locale.nextMonth}</button>
</div>
{/* Weekday headers */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 36px)",
gap: 2,
textAlign: "center",
}}
>
{locale.weekdays.map((wd) => (
<span key={wd} style={{ fontSize: 12, color: "#9ca3af", padding: 4 }}>
{wd}
</span>
))}
</div>
{/* Calendar grid */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(7, 36px)", gap: 2 }}>
{calendar.weeks.flat().map((day, i) => {
if (!day) return <span key={i} />;
const dp = getDayProps(day);
return (
<button
key={i}
onClick={() => handleDateClick(day)}
disabled={dp.isDisabled}
style={{
width: 36,
height: 36,
borderRadius: "50%",
border: dp.isFocused ? "2px solid #0ea5e9" : "none",
background: dp.isSelected ? "#0ea5e9" : dp.isToday ? "#f0f9ff" : "transparent",
color: dp.isSelected ? "white" : dp.isDisabled ? "#d1d5db" : "inherit",
cursor: dp.isDisabled ? "not-allowed" : "pointer",
fontWeight: dp.isToday ? 600 : 400,
}}
>
{dp.day}
</button>
);
})}
</div>
{/* Footer */}
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 12 }}>
{hasValue && (
<button onClick={handleClear} style={{ color: "#ef4444" }}>
{locale.clear}
</button>
)}
<button onClick={handleCancel}>{locale.cancel}</button>
<button
onClick={handleConfirm}
disabled={!canConfirm}
style={{
background: canConfirm ? "#0ea5e9" : "#e5e7eb",
color: canConfirm ? "white" : "#9ca3af",
padding: "4px 16px",
borderRadius: 4,
border: "none",
}}
>
{locale.confirm}
</button>
</div>
</div>
)}
</div>
);
}

핵심 개념

getDayProps

getDayProps 함수는 캘린더 렌더링의 핵심입니다. 각 날짜 셀에 대해 셀의 상태를 설명하는 18개의 불리언/숫자 플래그가 포함된 DayProps 객체를 반환합니다:

const dp = getDayProps(day);
// 선택 상태
dp.isSelected; // 현재 선택된 날짜
dp.isToday; // 오늘 날짜
// 범위 상태 (범위 선택기용)
dp.isInRange; // 시작일과 종료일 사이
dp.isRangeStart; // 범위의 첫 번째 날짜
dp.isRangeEnd; // 범위의 마지막 날짜
dp.isInHoverRange; // 호버 미리보기 범위 내
dp.isHoverTarget; // 호버된 날짜 자체
dp.isRangeSingle; // 범위 시작일과 종료일이 모두 해당 (단일 날짜 범위)
// 시각적 연결 도우미
dp.hasLeftConnection; // 이전 날짜와 연결됨 (범위 배경용)
dp.hasRightConnection; // 다음 날짜와 연결됨
dp.isConsecutiveRange; // 여러 날로 구성된 범위의 일부
// 상태
dp.isDisabled; // minDate/maxDate/isDateUnavailable에 의해 비활성화됨
dp.isFocused; // 키보드 포커스를 가짐
dp.isOutsideDay; // 인접한 달에 속함 (showOutsideDays가 활성화된 경우)
dp.isHighlighted; // highlightDates 배열에 포함됨
// 데이터
dp.date; // Date 객체
dp.day; // 월의 일자 번호
dp.dayOfWeek; // 0-6 (일요일-토요일)

외부 클릭을 위한 Refs

훅은 containerRefpopupRef를 반환합니다. 이를 래퍼와 팝업 요소에 연결하세요. 훅은 이를 사용하여 외부 클릭을 감지하고 팝업을 자동으로 닫습니다.

키보드 탐색

handleKeyDown 핸들러는 전체 키보드 탐색을 위해 화살표 키, Enter, Escape, Tab을 지원합니다. 이를 팝업 컨테이너에 연결하세요.

로케일

사용자에게 표시되는 모든 문자열은 locale 객체에서 가져옵니다. 부분적인 locale 옵션을 전달하여 모든 문자열을 사용자 정의할 수 있습니다:

const picker = useDatePicker({
value,
onChange: setValue,
locale: {
confirm: "확인",
cancel: "뒤로",
placeholder: "날짜를 선택하세요...",
},
});

다음 단계