[React&TS] 2. Todo를 Calendar에 기록남기기

파이·2021년 12월 9일
1

이번 웹앱을 만드는 김에 Redux를 활용해보고 싶었습니다.
그러던도중, Todo List에서 달성한 부분들을 Calendar에 표현 하면 좋겠다는 생각이 들었습니다.
때문에 fullCalendar 라는 모듈을 다운받아 사용하게 되었습니다.


6. 저장 로직 구성 및 localStorage 사용

기존 데이터들을 어떻게 표현 할 수 있을까 고민하던 중, 몇 가지 로직을 세웠습니다.

  1. Todo에서 checked 한 데이터만 따로 뽑아준다.

  2. 이를 Calendar 컴포넌트에 추가해준다.

  3. 날짜가 지나가면 모든 Todo를 삭제하고, 이미 check되었던 값들만 따로 저장한다.

  4. 이후 이미 check되었던 이전 값 + 오늘 check값을 Calender에 표시한다.



todo.tsx 날짜 체크 및 데이터 저장부분

  // 날짜를 비교하여 Todo를 Reset하고, Checked를 따로 저장해주는 함수 
  const checkDate = () => {
    if ( String(new Date().getDate()) === localStorage.getItem('todayDate') ) return // 1
    
    localStorage.removeItem('TodoList') // Todo 다 날린다. // 2
    localStorage.setItem('todayDate', String( new Date().getDate() )) // 그리고 날짜도 새로 저장 

    let newCalendarData = todoReducer.filter(v => v.checked  === true) // 이제 저장할 checked 된 값들과 // 3
    let oldCalenderData = JSON.parse(localStorage.getItem('savedCalendarData')!) // 이미 저장된 checked 값들을 가져와서  // 4
    let calendarDatas = newCalendarData;
    if (oldCalenderData) {
      calendarDatas = [...oldCalenderData, ...newCalendarData] // 하나의 배열로 만들어주고
    }
    localStorage.setItem('savedCalendarData', JSON.stringify(calendarDatas)) // 새로 저장해준다. // 5
  }
  useEffect(() => {
    checkDate() // 6
  }, [])
  1. 우선 오늘과 저장된 date값이 같으면 함수를 바로 종료합니다.

  2. 날짜가 넘어갔다면, 저장되어있던 todo를 모두 삭제 및, 날짜 데이터를 오늘로 지정해줍니다. (앞으로 비교하기 위함)

  3. reducer로 지정된 todo값들 중, checked 된 값들만 따로 가져옵니다.

  4. 이미 checked로 저장된 값들을 받아오는데, 이 값들이 있다면 spread연산자로 하나의 배열로 만들어줍니다.
    혹시 없다면 오늘 값만 이용합니다.

  5. 그리고 해당 값들을 '저장된 값들' 에 새로 추가해줍니다.

  6. 렌더 될때마다 check를 진행합니다. 물론 오늘 몇번이든 렌더 된다고 해서, 바뀌는 건 없습니다.


굉장히 복잡하게 작성된 것 같지만, 사실 간단합니다.
날짜가 지났으면 저장되있던 체크된 값 + 새로운 체크된 값을 만들어 주고,
기존의 값들은 싹 날려주는 겁니다.



7. fullCalendar에 저장된 값 표시

export interface IEvents {
  title: string,
  start: string
}

const Calendar = () => {

  let loadCalendar = localStorage.getItem('todoCalendar')
    ? JSON.parse(localStorage.getItem('todoCalendar')!) // 확정할당 연산자 사용
    : [{title: '초기값', start: '2000-01-01'}]

  const [calendarData, setCalendarData] = useState<IEvents[]>(loadCalendar)
  const [modalData, setModalData] = useState<IEvents[] | null>(null)
  const [onModal, setOnModal] = useState(false)
  ...

일단 똑같이 interface를 지정하고 초기 세팅을 해줍니다.
Modal값들은 클릭시 모달을 띄워주려는건데, 지금은 크게 신경쓰지 않아도 됩니다.
특별한점 이 있다면, loadCalendar에 값을 지정해 줄 때,
! (확정할당연산자) 를 활용하여 '이 값은 무조건 있다'라고 요청한 점입니다.
때문에 없을때도 null 이 아닌 default 값으로 지정해 줬습니다.

현재 TodoData가 있는지 체크하는함수

  const getTodoData = ():void => {
    let target = localStorage.getItem('TodoList')
    if (!target) return

    let checked = JSON.parse(target).filter( (v :ITodo) => v.checked === true ) // 체크 된 것 중에
    let savedCalendarData = JSON.parse(localStorage.getItem('savedCalendarData')!) //그리고 이전에 체크로 저장된 값들 가져와서
    let calendarDatas = checked;
    if (savedCalendarData) calendarDatas = [...savedCalendarData, ...checked]    // savedCalendarData가 있다면 spread 연산자로 합쳐준다.
  
    // calendar에 추가할 데이터 형태로 가공해준다.
    let data = [] // 최종데이터가 들어갈 빈 array
    const yearData = (v: ITodo) => new Date(v.id).getFullYear()
    const monthData = (v: ITodo) => new Date(v.id).getMonth() + 1
    const dateData = (v: ITodo) => ( new Date(v.id).getDate() < 10 ) ? '0' + new Date(v.id).getDate() : new Date(v.id).getDate() // Calendar 데이터 형식에 맞춰주기 위해 10 이하에선 0을 붙여준다.
    
    let content = calendarDatas.map( (v: ITodo) => v.content )
    let start = calendarDatas.map( (v: ITodo) =>`${yearData(v)}-${monthData(v)}-${dateData(v)}` )
    for (let i in calendarDatas) data.push({ title: content[i], start: start[i] })
    setCalendarData(data)
  }
  useEffect(() => {
    getTodoData()
  }, [])
  

이번에도 아까 input때와 로직은 비슷합니다.
세번째 줄에 if (!target) return 이 없으면 null 형식을 고려하여 오류가 나므로, TS에서는 이런 케이스를 꼭 추가해줍시다.
받아온 데이터를 fullCalendar에 들어갈 형식으로 가공해줍니다. 이왕이면 날짜들을 map함수로 한번에 처리하고 싶었지만, 안되어서 최대한 간결하게 push 함수로 표현했습니다.

fullCalndar

    <FullCalendar
        plugins={[ dayGridPlugin, interactionPlugin ]}
        events= {calendarData}
        eventClick= {(arg)=> handleEventClick(arg)}
    />

fullCalendar는 정말 다양한 옵션이 있지만, 간결하게 사용했습니다.
가장 중요한 events에는 가공한 데이터를, eventClick에는

이렇게 잘려보이는 데이터를 확대하기 위한 modal이벤트를 넣어주겠습니다.



8. Modal로 자세히보기

Calendar.tsx

바로 modal컴포넌트를 적기 전에, 클릭 이벤트를 만들어 주겠습니다.

  const handleEventClick = (arg: any) => { // arg의 형식이 복잡해서 any 사용
    let target = calendarData.filter(v => v.title === arg.event._def.title)[0] // 1
    let moddalData = calendarData.filter(v => v.start === target.start ) // 2
    setOnModal(true)
    setModalData(moddalData) // 3
  }

우선 이번엔 eventClick의 arg가 복잡해서 any를 사용해줬습니다. 그리고 살짝 복잡한데,

  1. 우선 이벤트는 캘린더의 날짜가 아닌 각 data 하나하나에 들어갑니다. 때문에 누른 이벤트의 내용을 가져와주고 (저기 복잡한_def...), 기존의 data에서 같은 내용을 가진 동일한 값을 찾아냅니다. (똑같은 내용의 이벤트를 여러번 했다면 오류가 날 듯 합니다.)

  2. 이 찾아낸 값과 같은 날짜의 값들을 전부 찾아

  3. setModalData에 넣어줍니다.


EventModal.tsx

import { IEvents } from "./Calendar";

interface IEventModal {
  modalData: IEvents[] | null
  setOnModal: (v : boolean) => void;
}

const EventModal = ( {modalData, setOnModal} :IEventModal ) => {


  return (
    <div className= 'modal-box'>
      
      <button className='x-btn' onClick= { () => setOnModal(false)}> x </button>
      { modalData &&
        modalData.map( v => 
          <div className='title'> {v.title} </div>
        )
      }
    </div>
  )
}

export default EventModal

이렇게 가져온 데이터만 잘 가져와주면...!
끝입니다!



후기

저나, 혹은 이 정보를 필요한 누군가가 볼 때 보기 쉬운 코드와 설명을 적으려고 했으나 쉽지 않네요 정말.
그러나 마지막 EhandleEventClick의 오류는 이렇게 리뷰하지 않았다면 몰랐을 오류입니다.
분명 시간도 많이 걸리고, 쉽지 않은 일이지만 이렇게 남기는게 정말 의미있는 일임을 다시 알게됬군요.

깃허브 주소
배포 주소
완성품이 관심있으시다면, 한번 확인해보세요!

profile
기록

0개의 댓글