web

[React 최적화] 실습 4. 이미지 갤러리 페이지(2) - useSelect 렌더링 문제, Redux reselect

Jaaaay 2022. 8. 4. 18:43

useSelect 렌더링 문제 해결

react devtools를 설치했다면 브라우저 개발자 도구에서 기능을 사용할 수 있다.

Components → 설정 → Highlight updates when components render 켜주기

불필요한 렌더링이 일어남을 확인할 수 있다.

useSelector 동작 원리

useSelector로 구독하는 값이 dispatch 후 결과값이 달라지면 리랜더링을 실행한다.

여기서 객체로 구독을 하면 내용이 같아도 객체 자체는 변경된 것으로 인식하기 때문에 랜더링이 일어난다.

useSelector 문제 해결 방법

  1. Object를 새로 만들지 않도록 State 쪼개기
  2. 새로운 Equality Function 사용

: equality function으로 shallowEqual을 할당해주면 obj 값을 비교할 때 내용을 비교해줌

적용해보기

// ImageModalContainer.js
import React from 'react';
import { useSelector } from 'react-redux';
import ImageModal from '../components/ImageModal';

function ImageModalContainer() {
  const { modalVisible, bgColor, src, alt } = useSelector(state => ({
    modalVisible: state.imageModal.modalVisible,
    bgColor: state.imageModal.bgColor,
    src: state.imageModal.src,
    alt: state.imageModal.alt,
  }));

...
  • 1번
import React from 'react';
import { useSelector } from 'react-redux';
import ImageModal from '../components/ImageModal';

function ImageModalContainer() {
  const modalVisible = useSelector(state => state.imageModal.modalVisible);
  const bgColor = useSelector(state => state.imageModal.bgColor);
  const src = useSelector(state => state.imageModal.src);
  const alt = useSelector(state => state.imageModal.alt);
  • 2번
import React from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import ImageModal from '../components/ImageModal';

function ImageModalContainer() {
  const { modalVisible, bgColor, src, alt } = useSelector(
    state => ({
      modalVisible: state.imageModal.modalVisible,
      bgColor: state.imageModal.bgColor,
      src: state.imageModal.src,
      alt: state.imageModal.alt,
    }),
    shallowEqual
  );
// PhotoListContainer.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PhotoList from '../components/PhotoList';
import { fetchPhotos } from '../redux/photos';

function PhotoListContainer() {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchPhotos());
  }, [dispatch]);

  const { photos, loading } = useSelector(state => ({
    photos:
      state.category.category === 'all'
        ? state.photos.data
        : state.photos.data.filter(
            photo => photo.category === state.category.category
          ),
    loading: state.photos.loading,
  }));

...
  • 2번
const { photos, loading } = useSelector(state => ({
    photos:
      state.category.category === 'all'
        ? state.photos.data
        : state.photos.data.filter(
            photo => photo.category === state.category.category
          ),
    loading: state.photos.loading,
  }), shallowEqual);

shallowEqual을 사용했는데도 리랜더링이 발생했다.

왜일까?

photos의 filter 함수가 매번 새로운 배열을 반환하기 때문에 리랜더링이 발생한다.

  • 해결

filter 함수를 바깥에서 동작하도록 한다.

import React, { useEffect } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import PhotoList from '../components/PhotoList';
import { fetchPhotos } from '../redux/photos';

function PhotoListContainer() {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchPhotos());
  }, [dispatch]);

  const { category, allPhotos, loading } = useSelector(
    state => ({
      category: state.category.category,
      allPhotos: state.photos.data,
      loading: state.photos.loading,
    }),
    shallowEqual
  );

	const photos =
	    category === 'all'
	      ? allPhotos
	      : allPhotos.filter(photo => photo.category === category);

...

Redux Reselect를 통한 렌더링 최적화

위 PhotoListContainer의 문제점

  1. category가 실질적으로 랜더링하는 과정에서 사용되지 않음
  2. useSlector State들이 개발 과정에서 점점 추가되면 photos가 변하지 않아도 State에 변화가 일어날 때마다 filter를 실행하게 됨

→ reselect를 사용

https://github.com/reduxjs/reselect

  • 적용
// PhotoListContainer.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PhotoList from '../components/PhotoList';
import { fetchPhotos } from '../redux/photos';
import selectFilterPhotos from '../redux/selector/selectFilterPhotos';

function PhotoListContainer() {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchPhotos());
  }, [dispatch]);

  const photos = useSelector(selectFilterPhotos);
  const loading = useSelector(state => state.photos.loading);

...
// selectFilterPhotos.js
import { createSelector } from 'reselect';
export default selectFilteredPhotos = createSelector(
  [state => state.photos.data, state => state.category.category],
  (photos, category) =>
    category === 'all'
      ? photos
      : photos.filter(photo => photo.category === category)
);

reselect는 memoization 기법을 사용해 함수에 똑같은 인자가 들어오면 미리 캐시된 값을 반환한다.