[Redux] createSelector에 관하여

강동욱·2024년 4월 21일
0

들어가기 앞서

createSelector

A library for creating memoized "selector" functions. Commonly used with Redux, but usable with any plain JS immutable data as well.

createSelecotr란 Reselect 라이브러리에서 파생되어진 것이며 selector 함수를 메모이제이션을 더한것이라고 볼 수 있습니다. 보통 useSelector는 useSelector가 선택한 state가 변경될 때만 리렌더링을 합니다. 그런데 이것은 왜 필요할까요?

createSelector is only necessary when your selector is doing heavy computation or creating a new object.
- RTK co-maintainer

selector가 무거운 연산을 필요로 할때 또는 새로운 객체를 반환할때 createSelector를 사용하면 됩니다.

useSelector의 동작원리

1. useSelector 실행

useSelector는 컴포넌트가 렌더링 될 때 , 또는 action이 dispatch되서 stor state가 업데이트 될 때 실행됩니다.(모든 컴포넌트에 있는 useSelector 함수를 실행시킴)

2. useSelector의 함수값 비교

redux는 ===(참조 동등성 검사)를 사용해서 이전 함수 반환값과 비교를 합니다.

부연 설명을 하자면 참조 동등성은 참조값을 비교합니다. 객체의 속성들이 안 바뀌어도 새로운 객체는 새로운 참조값이 부여되므로 이전 값과 객체 속성이 똑같더라도 계속 리렌더링이 됩니다. 이럴때는 useSelector 두번째 인자의 shollowEqual을 사용해서 얕은 비교 옵션을 추가해주면 됩니다.

주의할 점은 shoallowEqual은 1depth에서만 비교하기때문에 만약 값이 원시값이 아니라 객체라면 shallowEqaual을 해도 계속해서 리렌더링이 발생할 것 입니다..

const person = {
  name: 'Jake',
  age: '15'
}

// 🚨 actiondl dispatch될 떄 마다 리렌더링 발생
const {name, age} = useSelector((state) => ({...state.person}))

// ✅ shallowEqaul을 사용해서 리렌더링 방지
const {name, age} = useSelector((state) => ({...state.person}), shallowEqual)

const person2 = {
  name: 'Jake',
  age: '15',
  liked: {
  	food: '김밥'
  }
}

// 🚨 action이 dispatch될 떄 마다 리렌더링 발생 shallowEqaul은 1depth에서만 비교하기 때문에 liked의 값은 매번 다른 참조값을 가진다. 
const {name, age} = useSelector((state) => ({...state.person2}), shallowEqual)

3. 컴포넌트 리렌더링

이전 함수의 반환값과 다르다면 컴포넌트를 리렌더링을 합니다.

트러블 슈팅

저는 모달창 패턴을 구현하면서 사용할 모달의 객체를 Redux를 사용해서 modalSlice안에서 전역적으로 관리를 하고 있었는데 그 객체들은 다음과 같은 형식입니다.

modals: {
  'LoginModal': {
    id: 'LoginModal',
  	open:true
  },
  'TestModal': {
  	id: 'TestModal',
    open: false, 
  }
}

여기서 modal들의 객체중 open이 true인 모달들의 객체들만 선택을 하기위해서 처음에 다음과 같은 코드를 구성했습니다.

// modal.slice.ts
export const getOpenModals = (state: RootState) => {
  return Object.keys(state.modal.modals).filter(
    (modalName) => state.modal.modals[modalName].open,
  );
};

// ModalProvider.tsx
export default function ModalProvider({ children }: Props) {
  const modals = useAppSelector(getOpenModals);

  return (
    <>
      {modals?.map((<modalName) => (
        <LazyModal key={modalName} fileName={modalName} />
      ))}
      {children}
    </>
  );
}

제 생각에는 open이 true인 값의 모달객체만 가져오는 줄 알았습니다. 하지만 이 코드는 에러를 발생시켰습니다.

🚨 ERROR
Selector getOpenModals returned a different result when called with the same parameters. This can lead to unnecessary rerenders.
Selectors that return a new reference (such as an object or an array) should be memoized:

위와 같은 에러는 store가 dispatch에 의해 변경될때마다 modals가 항상 리렌더링이 되서 생기는 문제입니다. 왜냐하면 모든 useSelector는 dispatch가 일어날때마다 항상 실행되는데 이때마다 modals의 useSelector함수는 filter 메서드에 의해 항상 새로운 배열을 반환하고 배열안에 요소들이 같더라도 참조값이 다르기 때문에 이는 컴포넌트의 렌더링으로 이어지기 때문입니다.

해결방안(createSelector)

// modal.slice.ts
const getModals = (state: RootState): ModalMapType => state.modal.modals;

export const getOpenModals = createSelector(getModals, (modals) =>
  Object.keys(modals).filter((modalName) => modals[modalName].open),
);

// ModalProvider.tsx
export default function ModalProvider({ children }: Props) {
  const modals = useAppSelector(getOpenModals);

  return (
    <>
      {modals?.map((modalName) => (
        <LazyModal key={modalName} fileName={modalName} />
      ))}
      {children}
    </>
  );
}

createSelector 스토어에 n-1번째 인자까지는 새로운 값을 계산하는데 필요한 값을 넣어줍니다.(useMemo의 의존성 배열이랑 비슷하다고 보시면 될 것 같습니다)

n번째에는 함수를 넣어주는데 이 함수의 반환값은 우리가 사용할 가공된 state값입니다.

n-1번째의 인자의 값이 바뀔때만 n번째 함수가 새로운 값을 반환하고 그렇지 않은 경우에는 캐싱해둔 이전의 반환값을 사용하므로 리렌더링이 일어나지 않습니다.

export const getOpenModals = createSelector(getModals, getName, (modals, name) =>
  Object.keys(modals).filter((modalName) => modals[modalName].open),
);

주의할 점은 마지막 함수의 매개변수의 순서는 createSelector에 n-1번까지 넣어준 매개변수의 순서와 동일해야합니다. 예를 들어 아래와 같이 name과 modals의 순서가 바뀌면 안됩니다.

참조

https://stackoverflow.com/questions/67384049/how-exactly-useselector-works
https://stackoverflow.com/questions/76492706/selector-memoized-returned-the-root-state-when-called-redux-toolkit

profile
차근차근 개발자

0개의 댓글