티스토리 뷰

REACT

big-calendar 라이브러리 사용하기 02

송우든 2024. 11. 28. 15:36
728x90

지난 포스팅에 이어 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를 활용하여 구현하였다. 한국천문연구원_특일 정보

 

한국천문연구원_특일 정보

(천문우주정보)국경일정보, 공휴일정보, 기념일정보, 24절기정보, 잡절정보를 조회하는 서비스 입니다. 활용시 날짜, 순번, 특일정보의 분류, 공공기관 휴일 여부, 명칭을 확인할 수 있습니다.

www.data.go.kr

 

처음에는 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 })}`,
};

 

 

최종 구현

728x90
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크