| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- ui thread
- expo updates
- HTML
- TS
- in app purchase
- js
- Expo
- 정보처리기사
- 속도개선
- 정처기 준비물
- collapsibletabview
- sharedvalue
- 이펙티브 타입스크립트
- 정처기 자격
- BOUNDED CONTEXT
- 애그리게이트
- rniap
- typeScript
- 비동기
- react natvie
- 코드 푸시
- nextJS
- 불변식
- 빌딩 블록
- rn
- 타입스크립트
- Aggregate
- React Native
- DDD
- IAP
- Today
- Total
nika-blog
[React] useSelector hook의 동작원리 알아보기 본문
안녕하세요.
리액트를 이용해 개발을 하면 상태관리에 신경을 써줘야합니다. 상태가 변하면 리액트 컴포넌트가 리렌더링되기 때문인데요.
이는 리덕스 훅을 사용한 컴포넌트 개발에서도 마찬가지입니다.
오늘은 상태를 조회하고 리렌더링을 트리거하는 useSelector 훅의 동작원리를 알아보겠습니다.
useSelector 훅이란?
const result: any = useSelector(selector: Function, equalityFn?: Function)
리덕스 스토어에서 상태를 조회하는 react-redux 라이브러리의 hook 입니다.
코드를 보시면 두개의 인수를 받고 있는데요.
1. selector
첫번째는 selector 함수로, 스토어에서 어떤 값을 조회할 지 결정하는 함수입니다.
selector 함수는 스토어의 상태트리 전체에 해당하는 state 값을 인자로 받아서 추출하고자 하는 값을 결정하게 됩니다.
사용예시는 다음과 같습니다.
const todosSelector = state => state.todo.todos;
const todos = useSelector(todoSelector);
여기서 주의할 점은 selector 함수는 항상 부수효과가 없는 순수함수 이어야 한다는 점입니다.
2. equalityFn
두번째로 전달받는 함수는 useSelector 에서 조회하는 상태와 이전상태를 비교하는 방법을 결정하는 함수입니다.
무슨 뜻이냐면, 컴포넌트에서 useSelector훅을 사용하게되면 useSelector 훅은 리덕스 스토어를 구독(subscribe) 하게되고
액션이 dispatch 될때마다 호출되게 됩니다. 그리고 useSelector 훅에 캐시되었던 이전 값과 현재 selector 함수에서 반환하는 값이 다르면, 컴포넌트의 리렌더링을 트리거하게되죠.
여기서, 이전 값과 현재 값은 selector 함수가 반환하는 원시값 또는 참조값 그 자체를 비교하게됩니다. 이게 기본값이죠. 별도의 두번째인수를 전달하지 않으면 이렇게 동작합니다.
값을 비교하고 업데이트하는 방식
useSelector 훅은 말씀드렸다시피, 값을 조회하고, 이전 값과 다르면 컴포넌트를 리렌더링 하는 방식으로 동작합니다.
한번 자세히 알아볼까요?
먼저 컴포넌트가 렌더링될 때, useSelector 의 인수로 전달된 selector 함수가 호출되고 계산된 결과는 useSelector hook에 의해 반환됩니다.
여기서 다음과 같은 특징이 있습니다.
액션이 디스패치되는 경우
- 리스너 함수가 호출되어 selector가 호출된 결과 값이 이전 값과 다른 경우, 리렌더링됩니다.
컴포넌트가 렌더링되는 경우
- selector 함수의 참조값이 이전 selector 함수의 참조값과 같으면, 재계산 없이 캐시된 결과를 반환합니다.
- 새로운 selector 함수의 참조값이 전달된다면 결과 값을 재계산하여 반환합니다.
- memoized selector 를 이용한다면 최적화에 도움이 될 수 있습니다.
먼저 액션이 디스패치되는 경우 useSelector 훅에서는 어떤 일이 일어날까요?
다음은 useSelector 훅의 소스코드 중 일부 코드를 발췌한 것입니다.
useIsomorphicLayoutEffect(() => {
function checkForUpdates() {
try {
const newStoreState = store.getState()
const newSelectedState = latestSelector.current!(newStoreState)
if (equalityFn(newSelectedState, latestSelectedState.current)) {
return
}
latestSelectedState.current = newSelectedState
latestStoreState.current = newStoreState
} catch (err) {
// we ignore all errors here, since when the component
// is re-rendered, the selectors are called again, and
// will throw again, if neither props nor store state
// changed
latestSubscriptionCallbackError.current = err as Error
}
forceRender()
}
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
checkForUpdates()
return () => subscription.tryUnsubscribe()
}, [store, subscription])
selector 함수의 결과값의 이전과 현재를 비교하여 리렌더링을 결정하는 함수인 checkForUpdates 함수가 리스너 함수로 전달되고 있습니다.
여기서 우리가 알아야하는 점은, useSelector 훅에서 변하지 않은 값에 대해서는 리렌더링을 유발시키면 안된다는 점입니다. 이것은 개발자의 몫이며, 다음과 같은 코드를 주의해야합니다.
const squaredNums = useSelector(state => state.numbers.map(num => num ** 2));
selector 함수가 항상 새로운 참조값을 반환하고 있기 때문에, 액션이 디스패치될때마다 해당 컴포넌트는 리렌더링 되게 됩니다. state.numbers 의 값이 변하지 않더라도 말이죠.
위와 같은 코드를 작성할 때는 useSelector 로 값을 조회한 후에 계산을 해주거나, reselect 라이브러리를 사용하여 memoize 된 값을 반환하도록 하는 것이 좋습니다.
여기까지 액션이 디스패치 될때 useSelector 의 동작에 대해 살펴보았는데요. 다음으로는 컴포넌트가 렌더링 될때 useSelector 에는 어떤일이 일어나는지 알아보겠습니다.
다음도 소스코드 중 일부를 발췌하였습니다.
try {
if (
selector !== latestSelector.current ||
storeState !== latestStoreState.current ||
latestSubscriptionCallbackError.current
) {
const newSelectedState = selector(storeState)
// ensure latest selected state is reused so that a custom equality function can result in identical references
if (
latestSelectedState.current === undefined ||
!equalityFn(newSelectedState, latestSelectedState.current)
) {
selectedState = newSelectedState
} else {
selectedState = latestSelectedState.current
}
} else {
selectedState = latestSelectedState.current
}
} catch (err) {
if (latestSubscriptionCallbackError.current) {
;(
err as Error
).message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
}
throw err
}
useSelector에 캐시된 selector함수가 새로전달된 selector 함수와 참조값이 같지 않거나, store의 상태가 변했다면,
useSelector 훅에 캐시할 정보들을 업데이트합니다.
여기서 우리가 주목할 점은 재계산의 조건으로 selector 함수의 참조값을 비교한다는 점입니다.
스토어의 상태가 변했다면 당연히 재계산을 해주는 것이 맞지만, 스토어의 상태가 변하지 않았다면 재계산을 안하는 것이 효율적이겠죠?
다음 코드를 봅시다
// 1
const todoNums = useSelector(state => state.todos.length);
// 2
const selectTodoNums = state => state.todos.length); // defined out of component
const todoNum = useSelector(selectTodoNums);
1의 경우 selector 함수가 매번 새로운 참조값을 가리키기 때문에 렌더링시마다 재계산이 이루어지는 반면
2의 경우 selector 함수가 변하지 않기 때문에, 스토어의 상태가 변하지 않았을 때는 캐시된 값을 받을 수 있습니다.
때문에 리덕스 공식문서에서도 memoized selector 를 사용하도록 권장하고 있습니다.
저도 2의 방법을 선호하지만, 성능적으로 큰 차이는 없으니 상황에 따라 잘 골라 사용하는 것이 좋을 것 같네요!
마무리하며
오늘은 react-redux 라이브러리의 useSelector 훅에 대해서 알아보았는데요.
useSelector는 동작원리에 대해 짚고 넘어가보니, useSelector 훅을 어떻게 사용하면 좋을 지, 이런 저런 고민이 들것이라고 생각합니다.
리덕스 공식문서에서도 사용을 추천하는 reselect 등의 라이브러리도 함께 공부해보면서, 효율적인 코드를 작성해보면 좋을 것 같습니다!
'FE > React' 카테고리의 다른 글
| React lifecycle, useEffect, react mount (0) | 2022.02.27 |
|---|---|
| React 개념, 장점, 컴포넌트, 동작원리, virtual dom, reconciliation (0) | 2022.02.27 |
| [React] react-router 의 location 객체 알아보기 (0) | 2021.09.05 |
| [React] 리덕스 미들웨어를 이용한 비동기 작업 처리 (0) | 2021.08.22 |