useSelector의 적절한 사용법

윤상준·2023년 3월 5일
4

Redux

목록 보기
2/2
post-thumbnail

개요

부끄럽지만 최근까지 나는 react-reduxuseSelector를 다음과 같이 구조 분해 할당하여 사용해왔다.

 const { id, name } = useSelector((state: RootState) => state.user)

이와 같이 스토어의 슬라이스를 전부 갖고 와서 그 중 필요한 값만 구조 분해 할당으로 빼왔다.

최근에 불필요한 리렌더링을 잡기 위해 데브툴을 켜놓고 만져보던 도중, 리렌더링이 전혀 발생하지 않아야 할 곳들이 반짝반짝하는걸 봤다.

useSelector가 문제 될 줄은 전혀 모르고 삽질만 하다가, "혹시 ?" 하는 마음에 검색해봤다가 알게 되었다.

useSelector + 구조 분해 할당

여러분은 지금까지 useSelector를 잘못 사용해왔습니다.

useSelectorstate.user 까지만 가져온 후 구조 분해 할당으로 값을 빼올 경우, state.user 값 중 하나라도 변경되면 리렌더링이 발생한다.

state.user를 전부 갖고 왔기 때문에 name, photo만 사용한다고 하더라도 state.user가 업데이트되면 같이 리렌더링 된다.

따라서 useSelector + 구조 분해 할당은 useSelector의 대표적인 안티 패턴이다.

대응 방법

공식 문서에 따르면 3가지 방법을 제시한다.

1. 그냥 useSelector를 여러번 사용한다.

한 컴포넌트에서 useSelector를 여러 번 사용할 수 있습니다. 사실, 굉장히 좋은 방법입니다. useSelector는 항상 가능한한 제일 작은 크기의 값을 반환해야 합니다.

공식 문서에서 사실상 추천하는 방법이라고 생각한다.

2. equalityFn을 사용한다.

React-Redux에서 제공하는 shallowEqual 함수를 equalityFn으로써 useSelector에 전달한다.

equalityFn

eaulityFnuseSelector의 두번째 인자로 전달할 수 있는 비교 함수이다.

equalityFn?: (left: any, right: any) => boolean

equalityFn을 사용하면 이전 값과 다음 값을 equalityFn을 통해 비교해서 false가 나올 때만 리렌더링 한다.

shallowEqual

shallowEqual은 이름에서 알 수 있듯이 얕은 비교를 수행하는 함수이다.

import { shallowEqual, useSelector } from 'react-redux'

// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)

하지만 다음과 같은 중첩 객체가 있을 때 shallowEqual은 children이 변할 때만 감지하고, 그 안에 son이나 daughter의 변화 여부는 감지하지 않는다.

const object = {
  id: 1,
  name: 'Jason',
  children: {
  	son: {
      id: 2,
      name: 'Bob'
    },
    daughter: {
      id: 3,
      name: 'Emily'
    }
  }
};

깊은 비교는?

깊은 비교가 필요할 경우 lodash의 isEqual을 사용하거나 다음과 같이 커스텀 함수를 만들어야 한다.

import { useSelector } from 'react-redux'

// equality function
const customEqual = (oldValue, newValue) => oldValue === newValue

// later
const selectedData = useSelector(selectorReturningObject, customEqual)

3. 메모이제이션을 사용한다.

Reselect 또는 비슷한 라이브러리를 사용해서 메모이즈된 셀렉터를 생성합니다. 이 셀렉터는 한 객체에 다수의 값을 반환하지만, 값 중 하나라도 변경이 될 때에만 새로운 객체를 반환합니다.

즉, 값이 변경될 때에만 새로운 객체를 반환하고, 그 외에는 메모이즈된 객체를 반환한다.

이 방법은 단순히 값만 불러오는 것 보다는, 불러오는 동시에 복잡한 연산이 있을 때 또는 새로운 객체를 반환해야 할 때 권장되는 방법인 것 같다.

다음과 같이 idList를 반환하는 배열의 경우, useSelector가 사용될 때마다 (값이 변동 여부와는 상관 없이) 항상 새로 계산되어 새로운 객체가 반환된다.

 const idList = useSelector((state: RootState) => state.user.idList.map((id) => id === targetId));

이럴 경우 메모이제이션은 불필요한 리소스 방지에 큰 도움이 될 수 있다.

Redux Toolkit의 createSelector

Reselect 라이브러리의 메모이제이션을 사용할 수 있지만, Redux Toolkit에는 기본적으로 createSelector가 내장되어 있다.

 const idSelector = createSelector(state, list => {
  return list.map(({ id }) => id === targetId);
});

 const idList = useSelector(idSelector);

결론

조사하면 할수록 나는 상당히 혼란스러웠다. 그래서 도대체 뭘 쓰라는거지? 싶었다.

useSelector를 여러 번 쓰면 간편하겠지만, 가독성을 해칠 것 같고 코드 볼륨이 커지지 않을까 걱정되었다.

equalityFn은 얕은 비교, 깊은 비교를 매번 생각하면서 만들어야하기 때문에 공수가 너무 많이 들 것 같았다.

createSelector는 앞에서 말했듯이 단순히 값만 불러오는 것 보다 불러오는 동시에 복잡한 연산이 있을 때 권장되는 방법인 것 같았다. 값만 불러오는데도 메모이제이션을 사용한다면, 메모이제이션을 너무 많이 사용하는게 아닐지 우려가 되었다. 또한 오히려 사용성이 더 복잡해지는게 아닐까 걱정되었다.

여기 저기 알아본 결과, 나는 다음과 같이 결론 내렸다.

  1. 단순히 값만 불러오는 경우 그냥 useSelector를 여러번 사용하는 것이 제일 낫다.
  2. 불러오는 동시에 복잡한 연산이 들어갈 경우 createSelector의 메모이제이션을 사용한다.
  3. 일일이 equalityFn을 설정해주기 번거롭거나 얕은 비교만으로 커버가 가능할 경우 shallowEqual 사용을 고려한다.

답답한 마음에 제로초님께도 질문을 올렸고, 1번을 제일 선호하신다는 답변을 받았다.

profile
하고싶은건 많은데 시간이 없다!

0개의 댓글