본문 바로가기

프론트엔드/React

useSetRecoilState로 렌더링 최적화하기

  • 프로젝트: loplat X mobile
  • 키워드: recoil, 렌더링 최적화
  • 상황
    1. loplat X mobile 은 CRA 에 기반하여 CSR 방식을 사용한다.
    2. 로그인이 필요한 '캠페인 목록 페이지'에서 로그아웃 버튼을 누르면, protected router HOC에 의해 자동으로 로그인 페이지로 이동한다.
    3. 로그아웃을 시키는 것 외에 side effects 가 발생하면 안되는 상황이지만, 로그인 페이지로 이동하기 전 캠페인 목록 페이지에서 '캠페인 목록 GET API'가 의도치않게 한 번 호출되는 버그가 있다며 팀원분이 도움을 요청하셨다.
  • 해결과정
    1. 먼저 API를 어떤 코드에서 호출하는지 살펴보았다.
        const {
        ...
        data: ExampleData,
        ...
      } = useInfiniteQuery(
        ['campaigns', { status }],
        async ({ pageParam = 1 }) => {
          ...
          const { data } = await ExampleApi.fetchCampaigns(newParam);
          return data;
        },
        {
          ...options,
          refetchOnMount: 'always',
          refetchOnWindowFocus: false,
        }
      );
      위와 같이 react-query 라이브러리의 useInfiniteQuery를 사용하여 API를 호출하고 있었다.
      하지만 API가 호출되는 경우는 페이지가 mount 될 때와 pageParam이 업데이트 되는 경우 뿐이었다.
      로그인이 풀리고 로그인 페이지로 이동하는 로직에서 API가 다시 호출될 이유는 없어보였다.
    2.  그럼에도 불구하고 API가 호출된다는 것은 캠페인 목록 페이지가 의도치 않게 리렌더링(remount)된다는 증거였다. 따라서 React.memo를 사용하여 부모 컴포넌트가 리렌더링되더라도, 캠페인 목록은 리렌더링되지 않도록 감쌌다.
      https://ko.reactjs.org/docs/react-api.html#reactmemo
        export default React.memo(CampaignsPage);
      위와 같이 적용하니 API가 호출되는 버그는 사라졌다.
      하지만 공식문서에 나와있듯이, React.memo는 최적화를 위한 수단이므로 리렌더링을 막기위해서만 쓰는 것은 잘못된 사용이었다.
    3. 근본적인 원인을 찾기위해 페이지들의 부모 컴포넌트인 Router를 살펴보았다.
        <Switch>
        {routes.map((route) => (
          <Route
            key={route.path}
            path={route.path}
            component={인증관련HOC(route.component)}
            exact={route.exact}
          />
        ))}
      </Switch>
      Router의 로직과, 인증관련HOC에서는 리렌더링을 유발할만한 로직(상태 변경 등)을 찾을 수 없었다.
    4. Router의 부모 컴포넌트인 App.tsx에 와서 코드를 더 살펴보았다.
        const [, setAuth] = useRecoilState(userInfo);​
      위와 같은 코드를 발견했지만 처음 보기엔 별 문제가 없어보였다. 하지만 버그를 유발할만한 코드가 여기말고는 없었기 때문에, 이 코드가 왜 잘못된 코드인지 생각해보았다.
      로그아웃을 할 때 useInfo의 상태가 변하면 [_, setAuth] 에서 상태(_)를 명시하지 않아도 App이 리렌더링 되는걸까. 그렇다면 자식인 '캠페인 목록 페이지'도 리렌더링 될 것이다.
      recoil을 직접/많이 사용해본 경험은 없었지만, 스치듯 recoil setter 함수에 대한 문서를 본 기억이 나서 관련된 내용을 검색해보았다.
    5. https://recoiljs.org/ko/docs/api-reference/core/useSetRecoilState/
      "만약 컴포넌트가 setter를 가져오기 위해 useRecoilState()
      hook을 사용한다면 업데이트를 구독하고 atom 혹은 selector가 업데이트되면 리렌더링을 합니다."
      공식 문서를 통해 useRecoilState만 쓸 경우 불필요한 리렌더링이 일어날 수 있다는 것을 알게 되었다.
        const setAuth = useSetRecoilState(userInfo);​
       위와 같이 useSetRecoilState를 이용하여 setter함수만 리턴받으니 버그가 해결되었다!
  • 의의
    1. hook 사용시 의도치않은 리렌더링이 일어나지 않도록 주의하자
    2. 공식문서를 꼼꼼히 읽어두자