개발자공부 (2021.11~현재)/React

React State가 업데이트 되어도 바로 반영되지 않을 때 (api 호출)

purplecloud 2022. 11. 7. 16:25
data 뿌려줌 (get api 호출) -> 사용자가 data 업데이트 혹은 삭제(delete or post api 호출) -> DB에 데이터 전송 후, 변경된 data 다시 뿌려줌(get api 호출 다시)

 

이 동기적 작업을 프로젝트 내내 반복했는데 할때마다 매번 새롭고 매번 헤메고 있어서 삽질의 결과를 정리해두고자 함

근데 이 방법도 사실은 좀 안좋은 방식이 아닌가.. 하긴 함 ㅜㅜ

 

1. api로 받아온 데이터를 저장하는 state와 이를 복사해서 컴포넌트에 전달할 state를 별도로 설정함

  // load chapters

  const [chapters, setChapters]=useState([]);

  // chapter edit component control
  const [chapterList, setChapterList]=useState([]);

유튜브 처럼 챕터를 저장해놓고 해당 챕터별로 이동하는 기능을 구현하는 하려고 함

중요한건 state 이름은 아니고 컴포넌트에 전달해주는 state 와 api로 받아와준 state를 나눠서 두 개를 비교해서 변경을 감지해줄 것임

 

chapter : DB에서 받아온 data, 여기에 변경이 감지되는 경우 랜더링 되도록 할것임

chapterlist: 다른 컴포넌트에 전달할 data를 저장하는 곳

 

 

2. useEffect 훅 함수를 사용해서 변경이 감지될때마다 api를 재호출해줌

  
  // 맨 처음 마운트 될 때 데이터를 불러와주고 있음
  useEffect(()=>{
    loadChaptersFn();
  }, [])
  
  // chapters의 정보가 존재하면
  // chapterlist에 전달해줌
  
  useEffect(()=>{
    const makeChapterListFromChapters= async (chapters)=>{
      const list=[]
      for(let {chapterTitle, value} of await chapters){
        list.push({time:value, title: chapterTitle})
      }

      setChapterList(list)
    }
    
    makeChapterListFromChapters(chapters);
    console.log(chapters)
    console.log(chapterList)
  },[chapters])



  const loadChaptersFn= async () =>{
    await axios.get(`/api/chapters/${contentId}`).then(async (res) => {
      setChapters(res.data.result);
    });
  }
  
  

 // 실질적으로 컴포넌트에서 반영되는 data는 chapterList state 임
 return (
    <>
      <StyledEngineProvider injectFirst>
        <Header>
          <UserContent
            chapterList={chapterList}
            handleValueChangeFn={handleValueChangeFn}
            submitValueFn={submitValueFn}
          />
        </Header>
      </StyledEngineProvider>
    </>
  );

사용자가 내용을 업데이트해서 제출할때마다 loadChaptersFn을 다시 호출할 것이고

그때마다 chapters가 내용이 바뀔때마다 랜더링 해주는 useEffect가 화면을 새로운 내용으로 업데이트 해줄 것임.

 

 

2-1. 사용 예시

  const handleValueChangeFn = (e, idx)=>{
    let {name, value}= e.target;
    if(name==='time'){
      const timeArr=value.split(':');
      console.log(timeArr)
      let hour=0;
      let sec=0;
      let min=0;
      let newValue=0;
      switch(timeArr.length){
      case 2:
        min= minutesToSeconds(timeArr[0]);
        sec= parseInt(timeArr[1]);
        newValue=min+sec;
        console.log(newValue)
        break;
      case 3:
        hour= hoursToSeconds(timeArr[0]);
        min= minutesToSeconds(timeArr[1]);
        sec= parseInt(timeArr[2]);
        newValue=hour+min+sec;
        console.log(newValue)
        break;
      default:
        newValue=0;
      }
      value=newValue;
    }
    const list= [...chapterList];
    list[idx][name]=value;
    setChapterList(list)
  };

  // 제출버튼에 들어갈 function
  const submitValueFn = async () =>{
    const resultMap= new Map();
    chapterList.map((value)=>{
      resultMap.set(`${value.time}`, `${value.title}`);
    });
    
    chapterService.createChapters(contentId, resultMap).then(()=>{
      loadChaptersFn();
      handleBarOpen('정상적으로 등록되었습니다.')
    })
  };

중간에 handleValueChangeFn이 쓸데없이 긴데, 중간 생략하고 내용만 보면

사용자가 value값을 입력할 때마다 chapterList의 내용을 변경해주는 역할임

[...chapterlist] 얕은 복사로 만약 chapterlist 내용이 이미 존재한다면 같은 data를 가진 상태에서

새로운 내용을 추가하거나 아니면 기존 내용을 삭제하거나 하기 위함임

 

그리고 최종적으로 submitValueFn 부분에서 사용자가 제출버튼을 누르면

업데이트 된 chapterList의 내용을 api request 모양에 맞춰 (map 형식으로 전달해야했음) 가공한 후 전달해주고

완료가 되었을때만 2-1에서 설명한 것처럼 새롭게 loadChaptersFn을 호출해주면 됨

 

 

3. setTimeout 함수로 시간차 공격하기

참 안좋은 방식인 것 같지만 어쨌든 임시적으로 해결한 방법이긴 하다

redux toolkit 으로 contents 를 저장해서 꺼내는 방식으로 사용했는데 (근데 지금보니 굳이 store에 저장해서 쓸 필요가 없었던것 같긴하다; 나중에 확장하려면 필요하긴 했겠지만 당장은 불필요했음) 

 

data 뿌려줌 (get api 호출) -> 사용자가 data 업데이트 혹은 삭제(delete or post api 호출) -> DB에 데이터 전송 후, 변경된 data 다시 뿌려줌(get api 호출 다시)

 

이 흐름을 api 호출이 아니라 store에 있는 state 값들을 업데이트 해야하는데 사용자가 data 변경 api 를 호출 한 후!에 다시 store를 업데이트 하는 동기적으로 잡기가 어려워서 강제로 setTimeout을 사용해서 시간이 지난 후에 store값을 업데이트 하도록 했다.

 

  // paginaion 적용
  const { contents, pagination } = useSelector((state) => state.content);

    // load contents fn
  const loadContentsFn= (page, categoryId, searchTitle, searchTags, size)=>{
    dispatch(loadContents([page, categoryId, searchTitle, searchTags, size]));
  }

 

useSeletor는 store에서 값을 불러와서 사용해 주면 되는데 

사용자가 삭제 api를 호출해주고 새로운 content를 불러와야할때마다 loadContentsFn으로 store값을 업데이트 해준다.

 

3.1 사용 예시

const deleteContentsFn =  () => {
    const arr = [];
    for (let a of checked) {
      arr.push(a.contentId);
    }

    try {
      contentService.deleteContents(arr).then((result) => {
        setTimeout(()=>{loadContentsFn(page, categoryId, searchTitle, searchTags, size);}, 500)
        
      });
    } catch (e) {
      console.log(e);
    }

    handleClose();
    setDeleteBtn(false);
    setChecked([]);
  };

시간을 두지 않고 async로 해줘도 될것 같다...? 근데 dispatch 를 단독으로 사용하면 순서대로 실행되지가 않아서 결국 async 나 timeout 같은 function 으로 감싸서 사용해줬다.

단점: 500으로 시간을 두었지만 만약 5초안에 delete 가 되지 않는다면..? 업데이트가 안되겠지..

 

 

redux store의 값을 업데이트 해주는 방식으로는 이런 방식도 있다고 함

- immer 사용법
https://kyounghwan01.github.io/blog/React/immer-js/#immer-js%E1%84%85%E1%85%A1%E1%86%AB