React Redux에서 ContextAPI performance issue를 해결한 방법
React Redux에서 ContextAPI performance issue를 해결한 방법
React의 ContextAPI는 퍼포먼스 이슈가 있어 복잡한 데이터를 다루기에 적합하지 않습니다.
퍼포먼스 이슈?
이 퍼포먼스 이슈를 간단히 요약하면 객체 {number: 0, text: "text"}
를 contextAPI에서 관리할 때 A Comp에서는 context의 number
값만을, B Comp에서는 context의 text
값만을 사용한다고 가정하겠습니다.
context의 number
값이 변경되면 A Comp만 리렌더링되는 것이 가장 이상적이겠지만, number
값을 사용하지 않는 B Comp 역시 리렌더링됩니다.
해결 방법..?
해결 방법으로는 위 링크에서 언급한 방법과 같이 context 분할, React.memo
, React.useMemo
로 리렌더링 방지 그리고 contextAPI를 생성할 때 calculateChangedBits
를 두 번째 인자로 주어서 해결하는 방법이 있습니다.
context 분할의 경우 복잡한 데이터를 다룰 때 context가 수없이 많아질 수 있으며, React.memo
, React.useMemo
를 이용해 해결할 경우 컴포넌트에서 다루는 데이터가 변경될 때마다 dependency
배열의 값을 수정해줘야 합니다.
근본적인 해결 방법이라고 할 수 있는 contextAPI의 calculateChangedBits
를 지정하는 방법은 contextAPI가 렌더링되는 조건을 변경하는 것인데, 위 링크에 작성된 코드의 경우 contextAPI를 이용한 상태 관리 Provider를 별도로 생성하는 코드입니다. 다만, 코드 자체의 가독성이 좋지 않고 다루는 값이 많아진다면 수행해야 할 조건이 많아져서 효율적인 방법이라고 볼 수 없습니다.
그렇다면 react-redux에서는 contextAPI를 사용하고 있는데, 어떠한 방법으로 위 contextAPI 문제를 해결하고 있는지 살펴보도록 하겠습니다.
react-redux 에서의 contextAPI
react-redux
에서는 내부 로직이 contextAPI로 동작하고 있습니다.
react-redux
는 store
를 props
로 받는 Provider
를 사용해 값을 관리합니다.
store 구조
store는 listener, state, isDispatching 값을 보관하며, state 값에 접근하기 위한 getState
메소드 제공, state
를 업데이트하기 위한 dispatch
제공, 리스너를 등록하기 위한 subscribe
을 제공하고 있습니다.
dispatch
를 진행하는 동안 isDispatching
값은 true
로 변경되며, currentReducer(currentState, action)
함수를 실행하여 반환값을 currentState
에 저장합니다. isDispatching
값이 true
일 때 getState
, subscribe
, dispatch
등이 실행된다면 에러를 반환합니다.
또한, listener가 있다면, currentListener에 nextListener를 복사한 뒤, listener 목록을 순차적으로 실행합니다.
Provider Component
Provider component를 호출할 때, store
를 props로 넘겨줘야 합니다. 넘겨받은 store
는 contextValue
를 생성할 때 사용되며, contextValue
는 useMemo
hook을 통해 store
값이 변경될 때 다시 생성됩니다. contextValue
가 갖고 있는 값은 store
, subscription
값이며, subscription
값을 생성하기 위해 Subscription
클래스를 이용합니다.
현재 값과 비교할 수 있는 previousState
값 또한 store
가 변경될 때마다 생성되는데, store
의 getState
메소드의 반환값을 갖고 있습니다.
처음 Provider
가 생성될 때와 contextValue
, previousState
값이 변경될 때 contextValue
가 갖고 있는 subscription.trySubscribe
메소드로 구독을 시도하며, previousState
값과 store.getState
값이 다르다면 subscription.notifyNestedSubs
메소드를 실행합니다.
Provider
를 생성할 때 context
를 props로 넘겨주면 custom context를 사용해 contextValue를 관리할 수 있습니다. 넘겨주지 않다면 React.createContext(null)
로 생성된 React Context를 사용해 값을 관리합니다.
Subscription class
Provider
에서 contextValue
값을 생성할 때 Subscription
class에 store
를 넘겨준다고 했습니다. store
를 넘겨받은 Subscription
은 내부 store
필드에 넘겨받은 store
를 저장합니다.
contextValue
를 생성할 때 값이 변경되는 것을 확인하기 위해 subscription.onStateChange
에 subscription.notifyNestedSubs
를 저장합니다. 이 값은 Subscription.trySubscribe
내에서 listener로 넘겨주는 handleChangeWrapper
에서 값이 변경되었음을 체크할 때 사용합니다.
Provider
컴포넌트를 생성할 때와 store
값이 변경될 때 호출되는 Effect에서 contextValue.subscription
의 trySubscribe
를 시도합니다. 그리고 이전 값이 다를 때 notifyNestedSubs
메소드를 호출합니다. 각각 다음과 같이 실행됩니다.
trySubscribe
구독 중인 상태라면 unsubscribe 상태에 this.store.subscribe(this.handleChangeWrapper)
의 반환값을 저장합니다. 그 후, listeners
필드에 createListenerCollection
함수의 반환값을 저장합니다.
createListenerCollection
함수가 반환하는 값은 다음과 같습니다.
first
필드와last
필드를 모두 제거하는clear
메소드first
값에 저장된 리스너부터 순차적으로 실행하는notify
메소드listeners
값을 모두 보여주는get
메소드callback
을 인자로 전달받아listeners
에 linked list 형태로 저장한 후,first
와last
값을 반환하는subscribe
메소드
notifyNestedSubs
listeners
필드에 저장되어 있는 notify
메소드를 실행합니다.
useSelector
React에서 Hook이 업데이트된 이후로 React Redux에서도 관련 업데이트를 통해 connect
가 아닌, useSelector
과 useDispatch
hook을 사용해 redux가 관리하고 있는 값을 가져오고 업데이트할 수 있게 되었습니다.
또한, Provider가 생성되는 과정을 살펴보면서 Provider에서는 객체 shallow Equal
비교 관련 로직이 존재하지 않는 것을 확인할 수 있습니다. 실제로 useSelector
를 사용하지 않는다면 어떠한 값을 갖고 있든 DOM에서 노출이 안 되는 것이므로 리렌더링은 일어나지 않으니 값을 가져오는 곳에서 비교 로직이 존재하는 것이 맞습니다.
useReduxContext
Provider를 생성할 때 custom context를 설정하지 않았다면 ReactReduxContext
라는 이름의 context가 생성됩니다. useReduxContext
는 ReactReduxContext
의 값을 반환하는 hook 입니다.
createSelectorHook
useSelector
hook은 createSelectorHook
함수의 반환 값입니다. context 값이 ReactReduxContext
라면 useReduxContext
, 다른 값이라면 useSelector(context)
를 통해 가져온 값을 반환하는 hook을 미리 정의합니다.
useSelector
는 두 개의 인자를 전달받는데, 하나는 값을 가져오는 데 사용될 selector
, 다른 하나는 slector
가 변경되었는지 확인하는 equalityFn
입니다. equalityFn
함수의 기본 값은 (a, b) => a === b
입니다.
간단히 useSelector
가 하는 일을 살펴보면 useSelectorWithStoreAndSubscription
hook에 selector
, equalityFn
, store
, contextSub
인자를 전달하고, 그 반환값을 반환하는 일을 수행합니다.
useSelectorWithStoreAndSubscription
useSelectorWithStoreAndSubscription
hook은 강제로 렌더링을 시키기 위한 forceRender
함수가 존재합니다. 그리고 store
또는 subscription
값이 변경되었다면 실행되는 Effect hook이 존재합니다. 이 Effect hook에서는 인자로 전달받은 equalityFn
을 사용해 이전에 갖고 있던 값과 현재 action을 통해 업데이트된 값을 비교합니다.
만약, 값이 같다면 아무런 일도 하지 않고 effect를 종료합니다. 그렇지 않고 값이 다르다면 latestSelectedState
값에 newSelectedState
를 저장하여 값을 업데이트한 뒤, forceRender
함수를 실행합니다.
latestSelectedState
값은 ref로 이뤄져 있기 때문에 forceRender
를 실행시키지 않는다면 화면의 업데이트는 일어나지 않습니다. 이러한 로직으로 ref와 forceUpdate를 활용하여 react redux에서는 불필요한 리렌더링을 막고 있습니다.
React Redux에서 제공하는 shallowEqual
함수의 로직은 다음과 같습니다. useSelector
를 사용할 때 두 번째 인자로 selectorEqual
를 넘겨주게 된다면 객체의 reference 주소가 달라졌다는 이유로 리렌더링되지 않을 것입니다.
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
export default function shallowEqual(objA, objB) {
if (is(objA, objB)) return true
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if (keysA.length !== keysB.length) return false
for (let i = 0; i < keysA.length; i++) {
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false
}
}
return true
}
'Advanced > React' 카테고리의 다른 글
[React] sagen을 사용해서 간단히 상태 관리하기 (1) | 2021.05.16 |
---|---|
[React] useEffect의 동작 원리 (1) | 2021.01.31 |
create-react-app을 사용하여 chrome extension 만들기 (2) | 2020.11.15 |
useEffect Dependency (0) | 2020.10.21 |
ContextAPI 렌더링 이슈 (4) | 2020.10.17 |
댓글
이 글 공유하기
다른 글
-
[React] useEffect의 동작 원리
[React] useEffect의 동작 원리
2021.01.31 -
create-react-app을 사용하여 chrome extension 만들기
create-react-app을 사용하여 chrome extension 만들기
2020.11.15 -
useEffect Dependency
useEffect Dependency
2020.10.21 -
ContextAPI 렌더링 이슈
ContextAPI 렌더링 이슈
2020.10.17