개발일지

useReducer의 initialState와 props 문제

탁재민 2024. 2. 5. 01:28

이슈

PC버전에서 사이드 메뉴를 통해 최초 이동 이후 페이지 이동 시 문제가 바뀌지 않는 버그가 있었습니다.

 

분석

//ExamPage.tsx

  const {
    data: questionList,
    isError,
    isFetching,
    isSuccess,
    error,
  } = useGetExamQuery(round);
  
  
  const renderContent = () => {
    if (isFetching) {
      return <Loading image="question" />;
    }

    if (isError && error) {
      return (
        <ErrorUI error={error} message={`기출문제 불러오기에 실패하였습니다.`} />
      );
    }

    if (isSuccess && questionList.length === 0) {
      return <EmptyUI message={`기출문제가 비었습니다.`} />;
    }

    if (isSuccess) {
      return (
        <Exam examList={questionList} />
      );
    }

    return null;
  };
  
//Exam.container.tsx
  
function Exam({ examList }: QuestionProps){
  
  const [state, dispatch] = useReducer(reducer, {
    questionList: [...examList].map(createQuestion),
    isFinish: false,
    currentNumber: 0,
    score: 0,
  });
  
  ...

 

ExamPage 컴포넌트에서 문제 목록을 받아와 reducer의 initialState를 초기화 시켰는데

페이지 이동시 다시 초기화 되지 않는 게 문제였습니다.

 

페이지가 이동하면 컴포넌트가 다시 생성될 줄 착각하고 있었는데

useNavigate를 사용하는 경우 브라우저 주소의 경로만 바꾸고 새로고침이 일어나지 않으므로

상태 변화에 따른 rerender만 일어나고 mount는 일어나지 않아 초기화 되지는 않았던 것입니다.

 

그리고 최초로 페이지 이동 시에는 문제가 바뀌었던 것도 최초로 불러올 때는

isFetching이 true가 되어 컴포넌트가 unmount되었다가 다시 mount 되지만,

한번 문제를 불러온 이후에는 rtk query에 캐시되어 있는 값을 불러오기 때문에

isFetching이 false 상태를 유지해 unmount가 일어나지 않기 때문이었다.

 

 

React Component의 Rerender 조건과 Unmount 조건

Rerender

  1. 컴포넌트의 state 변경 시
  2. 부모 컴포넌트가 리렌더링 시

Unmount

  1. 컴포넌트가 DOM에서 제거될 때
  2. 같은 key를 가진 컴포넌트가 원래 DOM 위치 에서 사라졌을 때
<div className="before" title="stuff" />
/* 변경 -> */ 
<div className="after" title="stuff" />
/* 동일 요소로 판단해 변경된 부분만 업데이트 */


<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>
/* 변경 -> */ 
<ul>
  <li key="1111">Villanova</li>
  <li key="2015">Duke</li>
</ul>

/* key가 같은 Duke는 그대로 두고 Villanova는 기존 요소와 다른 것으로 판단 */

해결

아래와 같이 Quiz 컴포넌트에 key값을 추가해 remount가 일어나도록 수정했다.

<Exam examList={questionList} key={id} />

 

이처럼 remount시 컴포넌트를 처음부터 다시 생성해야 하므로 rerender보다 성능 면에서 떨어질 수도 있지만

기존 Exam 컴포넌트가 가지는 기존의 정답, 오답, 점수, 관련 키워드, 주제 정보 같은 state를 확실히 초기화 시켜

혹시 모를 버그에 대비하는 것이 좋다고 생각했다.

그 외 선택지

1. 새로고침 하기

useNavigate 대신 location.replace()를 사용해 페이지 새로고침을 유도하는 것이가장 먼저 시도했던 해결법이었다.

하지만 이 방법은 말 그대로 새로고침을 유발해 성능 면에서 끔찍하기 때문에 바로 선택지에서 뺐다.

2. useEffect로 props 상태 추적

이 방법은 useEffect의 dependency에 props를 추가해 props가 업데이트 될 때 마다 reducer state를 업데이트 시켜주는 방법이었다.

이럴 경우 어차피 props는 페이지 이동시 초기화 목적으로 한번만 쓰이는데

Quiz 컴포넌트가 rerender될 때마다 매번 비교해야 하므로 좋지 않은 패턴이라고 생각했다.

 

그리고 useState라면 간단하게 set 함수를 이용해서 상태를 업데이트 할 수 있었겠지만

useReducer의 경우에는 초기화 action과 reducer case까지 새로 만들어야 하므로 비효율적이었다.