티스토리 뷰
지난 포스팅에 이어 big-calendar 라이브러리를 사용해서 조금 더 다양한 기능을 구현해 보았다. 일정 드래그 앤 드롭 / 공휴일 정보 제공 / 스크롤에 따른 이동 등 구현한 기능에 대해 정리해보려고 한다.
나의 캘린더 컴포넌트는 아래와 같이 설정하였다. 이전에 구현했던 캘린더 기능에 몇 가지 기능을 추가하였다.
<div style={{ height: '100vh', width: '100%' }} onWheel={handleOnWheel}>
<DragAndDropCalendar
culture='ko'
localizer={localizer}
/** 관리할 날짜 객체 */ date={date}
/** 포맷 형식 */formats={formats}
/** 보기모드 종류 */ views={views}
/** 기본 뷰 모드 */defaultView='month'
/** 달력에 표시할 일정 목록 */ events={events}
/** 드래그가 이벤트가 가능한 조건 설정 */ draggableAccessor={(event) => event.isDraggable}
/** 4개의 슬롯으로 간격 */ timeslots={4}
/** 15분 간격 */ step={15}
onEventDrop={handleMoveEvent}
onEventResize={handleResizeEvent}
onDoubleClickEvent={handleShowEventDetails}
/** 이벤트에 CSS 클래스를 추가하거나 스타일을 변경할 수 있음 */ eventPropGetter={handleEventPropGetter}
/** 더보기 기능이 드릴다운 뷰에서 동작할지 여부 설정 */ doShowMoreDrillDown
/** slot 선택 여부 */ selectable
/** 이벤트(일정) 리사이징 여부 */ resizable
/** 이벤트(일정) 팝업 형태로 제공 */ popup
style={{ height: '100%', width: '100%', background: view === 'week' || view === 'day' ? 'transparent' : 'white' }}
/** 뷰모드를 설정하는 함수 */ onView={setView}
onNavigate={handleOnNavigate}
/** 캘린더 관련 언어 메세지 */ messages={messages}
components={{
toolbar: (props) => <CustomToolBar {...props} />,
event: (props) => <CustomEvent {...props} currentView={view} />,
// timeGutterWrapper: CustomTimeGutterWrapper,
// timeGutterHeader: () => <div className='bg-blue-300 w-[4px] h-full'>d</div>,
// timeSlotWrapper: CustomTimeSlotWrapper,
week: {
header: WeekHeader,
},
}}
scrollToTime={new Date()}
enableAutoScroll={true}
/>
</div>
이벤트 드래그 앤 드롭 기능
big-calendar라이브러리에서는 아래와 같이 withDragAndDrop을 감싸서 드래그 앤 드롭이 가능한 캘린더를 만들 수 있다.
const DragAndDropCalendar = withDragAndDrop<TEvent>(Calendar);
withDragAndDrop은 react-big-calendar에서 제공하는 함수이다. 드래그 앤 드롭(Drag-and-Drop) 기능을 캘린더에 추가하기 위해 사용한다. 일정을 드래그하여 이동하거나 일정 크기(날짜 범위)를 조정할 수 있다.
declare function withDragAndDrop<TEvent extends object = Event, TResource extends object = object>(
calendar: React.ComponentType<CalendarProps<TEvent, TResource>>,
): React.ComponentType<DragAndDropCalendarProps<TEvent, TResource>>;
이벤트 객체에 isDraggable이라는 속성을 추가하여 드래그가 가능한 이벤트와 가능하지 않은 이벤트를 구분하였다. 그래고 아래와 같이 드롭이 완료되었을 때와 이벤트가 리사이즈될 때를 처리해 주었다.
/** 일정 이동을 관리한다. */
const handleMoveEvent = useCallback(({ event, start, end, isAllDay: droppedOnAllDaySlot = false }: EventInteractionArgs<TEvent>) => {
const { allDay, id } = event;
if (!allDay && droppedOnAllDaySlot) event.allDay = true;
if (allDay && !droppedOnAllDaySlot) event.allDay = false;
setEvents((prev) => {
const existing = prev.find((_event) => _event.id === id) ?? {};
const filtered = prev.filter((_event) => _event.id !== id);
return [...filtered, { ...existing, start, end, allDay } as TEvent];
});
}, []);
/**
* event(일정) 크기를 리사이징한다.
*/
const handleResizeEvent = useCallback(({ event, start, end }: EventInteractionArgs<TEvent>) => {
setEvents((prev) => {
const { id } = event;
const existing = prev.find((_event) => _event.id === id) ?? {};
const filtered = prev.filter((_event) => _event.id !== id);
return [...filtered, { ...existing, start, end } as TEvent];
});
}, []);
마우스 휠(Wheel)을 통한 달(month) 이동 기능
구글 캘린더를 참고하여, 마우스 휠(Wheel)을 통한 달(month) 이동 기능을 구현하려고 했다. 구글 캘린더의 월별 보기에서 마우스 휠을 사용해 이전 또는 다음 달로 쉽게 이동할 수 있는 기능을 제공하는데, 이 기능 구현을 위해 디바운스(debounce) 기법을 적용했다.
마우스 휠 이벤트가 발생할 때마다 이전/다음 달 데이터를 즉시 불러와서 다시 그린다면 성능에 부담을 줄 수 있다는 점을 고려하였다. 이를 위해 디바운스를 적용하여 불필요한 반복적인 렌더링을 방지하고, 일정 시간 간격으로만 이벤트를 처리하도록 하여 성능을 최적화하였다.
const handleOnWheel = useCallback(
(e: React.WheelEvent<HTMLDivElement>) => {
if (timeoutRef.current) return;
if (view !== 'month') return;
handleOnNavigate(e.deltaY < 0 ? subMonths(date, 1) : addMonths(date, 1));
timeoutRef.current = setTimeout(() => {
timeoutRef.current = undefined;
}, 800);
},
[date, handleOnNavigate, view]
);
공휴일 표시 기능
실제 캘린더를 구현한다면, 편의를 위해서라도 공휴일 데이터를 보여줄 수 있어야 한다고 생각했다. 공휴일 데이터는 공공 데이터 API를 활용하여 구현하였다. 한국천문연구원_특일 정보
처음에는 big-calendar 라이브러리를 사용하여 월별 데이터가 변경될 때마다 공휴일 정보를 매번 불러오는 방식으로 구현했다. 하지만 이 방식은 월을 변경할 때마다 불필요한 API 호출이 반복되어 비효율적이라는 생각이 들었다. 그래서 캘린더가 처음 렌더링될 때 현재 날짜의 연도를 기준으로 공휴일 정보를 한 번만 불러올 수 있도록 변경하였다.
export default function useFetchHolidays() {
const [holidays, setHolidays] = useState<TEvent[]>([]);
const fetchHolidaysByYear = async (year: string) => {
try {
const data = await fetch(`${BASE_URL}?solYear=${year}&ServiceKey=${SERVICE_KEY}&numOfRows=20&_type=json`);
const { response } = await data.json();
const _holidays = response?.body?.items?.item;
const formatHolidays = (_holidays as THoliday[]).map((item, index) => {
const date = formatDateType(item.locdate); // Date 객체
return { id: index, title: item.dateName, start: date, end: date, isDraggable: false, type: 'HOLIDAY' as TEvent['type'], allDay: true };
});
setHolidays(formatHolidays);
} catch (e) {
// ERROR 처리 or Throw ERROR
console.error(e);
}
};
return { holidays, fetchHolidaysByYear };
}
날짜 및 시간 포맷팅
기본적으로 제공되는 날짜나 시간 포맷과 다르게 구현해야 할 때가 있다. big-calendar 라이브러리에서는 여러 행태의 사용자 지정 포맷을 지원한다. 나는 아래와 같이 필요한 곳만 설정해 주었다.
const formats = {
dayFormat: 'EEE dd',
monthHeaderFormat: 'yyyy년 MM월',
dayHeaderFormat: 'MM월 dd일(EEE)',
timeGutterFormat: 'a hh:mm',
dayRangeHeaderFormat: ({ start, end }: { start: Date; end: Date }) => `${format(start, 'MM월 dd일(EEE)', { locale: ko })} - ${format(end, 'MM월 dd일(EEE)', { locale: ko })}`,
};
최종 구현
'REACT' 카테고리의 다른 글
big-calendar 라이브러리 사용하기 01 (2) | 2024.10.28 |
---|---|
createBrowserRouter 사용하기(React Router v6.4) (0) | 2024.08.11 |
Tanstack Router 사용하기 03 (0) | 2024.08.04 |
Tanstack Router 사용하기 02 (0) | 2024.07.25 |
Tanstack Router 사용하기 01 (6) | 2024.07.24 |
- Total
- Today
- Yesterday