본문 바로가기
개발/React

[react] react-datepicker 커스텀 (ft. redux-toolkit 연결)

by 코딩하는 갓디노 2022. 3. 11.

[react] react-datepicker 커스텀 (ft. redux-toolkit 연결)

 

react-datepicker를 커스텀하여
redux-toolkit을 이용해 
데이터를 관리합니다.

 

프로젝트를 할 때 react-datepicker를 사용할 때가 빈번합니다.

커스텀하기에 가장 flexible한것 같아서 사용하고 있는데
이번에는 redux-toolkit과 연결해서
날짜를 보내주고, 날짜를 이용해 데이터를 fetch하였습니다. 

여기서 처음에는 dayjs를 감싸서 toolkit에 보내주었을 때
"A non-serializable value was detected in the state" when using redux toolkit 와 같은 warning을 발생시켜
해결 방법으로 new Date()를 이용해 겉을 더 감싸서 보내주어 문제 없이 작동했습니다. 

 

구현 영상

 

부모 코드

import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from "react-redux";
import { addChosenDate } from "../../../../features/mobile/meal/addEatSlice"
import dayjs from 'dayjs';
import 'dayjs/locale/ko';
import CalenderDialog from '../common/CalenderDialog';

const CalenderCard = () => {
    const dispatch = useDispatch()
    const [openCalender, setOpenCalender] = useState(false)
    const { chosenDate } = useSelector((state) => state.addEat) //redux-toolkit data
    const [startDate, setStartDate] = useState(new Date()) //컴포넌트 내부에서 useState로 날짜 지정
    const handleChange = (e) => {
        setStartDate(e)
    }

    const onOpenCalender = () => {
        setOpenCalender(true)
    }

    const onIncreaseDay = () => { //전날 날짜
        setStartDate(new Date(dayjs(startDate).add(1, 'day')))
        dispatch(addChosenDate(new Date(dayjs(startDate).add(1, 'day'))))
    }

    const onDecreaseDay = () => { //다음날 날짜
        setStartDate(new Date(dayjs(startDate).add(-1, 'day')))
        dispatch(addChosenDate(new Date(dayjs(startDate).add(-1, 'day'))))
    }

    const onSaveChosenDate = (date) => { //달력에서 확인 눌렀을 경우 날짜 저장
        dispatch(addChosenDate(date))
        setOpenCalender(false)
    }
    
    useEffect(() => { //redux-toolkit chosendate 변경시마다 fetchMeal 실행
        if (!chosenDate) { 
            dispatch(addChosenDate(new Date()))
        } else {
            let params = { userId, standardDate: chosenDate }
            dispatch(fetchMeal(params))
        }
    }, [chosenDate])

    return (
        <>
          <div>
            <img onClick={onDecreaseDay} src={'/images/prev_arrow.svg'} alt='이전 아이콘' />
            <div onClick={onOpenCalender}>
               <img src={'/images/calender_black.svg'} alt='달력 아이콘' />
               <span>{dayjs(startDate).format(`YYYY${'년'} M${'월'} D${'일'}`) || dayjs().format(`YYYY${'년'} M${'월'} D${'일'}`)} </span>
             </div>
          	<img onClick={onIncreaseDay} src={'/images/next_arrow.svg'} alt='다음 아이콘' />
          </div>
            {openCalender && <CalenderDialog startDate={startDate} handleChange={handleChange} onSaveChosenDate={onSaveChosenDate} />}
        </>
    );
};

export default CalenderCard;

 

자식 컴포넌트

캘린더에 애니메이션 효과를 주기 위하여 framer-motion 라이브러리를 사용하였습니다. 

 

import React from "react";
import { motion } from "framer-motion";
import DatePicker, { registerLocale } from "react-datepicker";
import ko from 'date-fns/locale/ko';
import "react-datepicker/dist/react-datepicker.css";
import Btn2 from "./Btn2";


const CalenderDialog = ({ onSaveChosenDate, startDate, handleChange }) => {
    registerLocale("ko", ko); //한국어 설정

    const formatDate = (d) => { //달력 년, 월, 일 header 
        const date = new Date(d);
        const monthIndex = date.getMonth() + 1;
        const year = date.getFullYear();
        return `${year}년 ${`0${monthIndex}`.slice(-2)}월`;
    }

    return (
        <>
            <div>
                <motion.div //애니메이션 효과
                    initial={{ opacity: 0, y: 0 }}
                    animate={{ opacity: 1 }}
                    transition={{ ease: "easeOut", duration: 1 }}
                    className="bg-white rounded text-gray-500">
                    <div className='text-center m-4'>
                        <DatePicker
                            selected={startDate}
                            onChange={handleChange}
                            disabledKeyboardNavigation //다른 월의 같은 날짜시 자동 selected 되는 현상 방지 
                            locale="ko"
                            inline
                            maxDate={new Date()}
                            popperModifiers={{ //화면을 벗어나지 않도록 하는 설정 
                                preventOverflow: { enabled: true }
                            }}
                            popperPlacement="auto" //화면 중앙에 팝업이 출현 
                            renderCustomHeader={({ date, decreaseMonth, increaseMonth }) => ( //header 커스텀 설정
                                <div>
                                    <div onClick={decreaseMonth}>
                                        <svg xmlns="http://www.w3.org/2000/svg"
                                            className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
                                        </svg>
                                    </div>
                                    <div>{formatDate(date)}</div>
                                    <div onClick={increaseMonth}>
                                        <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
                                        </svg>
                                    </div>
                                </div>)}
                        />
                        <Btn2 title={`확인`} onAction={() => onSaveChosenDate(startDate)} option={`1`} />
                    </div>
                </motion.div>
            </div >
        </>
    );
};

export default CalenderDialog;

 

react-toolkit slice

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import api from '../../../common/api';
import dayjs from 'dayjs';

const initialState = {
    chosenDate: null,
}

const addEatSlice = createSlice({
    name: "식사입력",
    initialState,
    reducers: {
        addChosenDate: (state, action) => {
            state.chosenDate = action.payload
        },
    },

    extraReducers: { ... },
})

export const { addChosenDate } = addEatSlice.actions

export default addEatSlice.reducer

 

다시 업데이트

chosenDate 역시 날짜 형식으로 "A non-serializable value was detected in the state" when using redux toolkit 와 같은 warning을 발생시켰습니다. 

해결방법

toolkit 사용안할 경우

toolkit의 chosenDate와 addChosenDate를 없애고,
최상단 부모에서 stateDate로만 사용하여 props로 자식에게 내려주어 사용하였습니다. 

 

toolkit 사용할 경우

react-toolkit에서 Sun Mar 13 2022 10:37:01 GMT+0900 (한국 표준시)와 같은 날짜 타입 데이터 전송시
위와 같은 에러가 발생하여 dayjs 를 이용해 220313 으로 데이터를 전송한 후
컴포넌트에서 가져올때 다시 new Date('220313')을 date 타입으로 바꿔줘야 합니다. 

반응형

댓글