사용자 정의 UI 빌드하기
이 가이드는 헤드리스 훅 위에 자신만의 날짜 선택기 UI를 빌드하는 패턴을 안내합니다.
패턴
모든 헤드리스 훅은 동일한 패턴을 따릅니다:
- 훅 호출: 옵션(
value,onChange,configuration)과 함께 훅을 호출합니다. - 반환 값 분해: 상태, 계산된 값, 핸들러를 얻기 위해 반환 값을 구조 분해합니다.
- 자체 마크업 렌더링: 반환된 값을 사용하여 자신만의 마크업을 렌더링합니다.
- 상호작용 연결: 제공된 핸들러를 호출하여 상호작용을 연결합니다.
훅은 캘린더 생성, 날짜 계산, 열기/닫기 상태, 키보드 탐색, 범위 로직 등 모든 복잡성을 관리하며, 여러분은 순수하게 표현에만 집중하면 됩니다.
예제: 사용자 정의 날짜 선택기
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
훅은 containerRef와 popupRef를 반환합니다. 이를 래퍼와 팝업 요소에 연결하세요. 훅은 이를 사용하여 외부 클릭을 감지하고 팝업을 자동으로 닫습니다.
키보드 탐색
handleKeyDown 핸들러는 전체 키보드 탐색을 위해 화살표 키, Enter, Escape, Tab을 지원합니다. 이를 팝업 컨테이너에 연결하세요.
로케일
사용자에게 표시되는 모든 문자열은 locale 객체에서 가져옵니다. 부분적인 locale 옵션을 전달하여 모든 문자열을 사용자 정의할 수 있습니다:
const picker = useDatePicker({ value, onChange: setValue, locale: { confirm: "확인", cancel: "뒤로", placeholder: "날짜를 선택하세요...", },});