[Redux] Hooks - useSelector, useDispatch
😅 서론
react-redux의 connect() 함수와, mapStateToProps, mapDispatchToProps 를 열심히 공부했는데...
Dev Ed님 유튜브 영상을 복습하면서, useSelector, useDispatch 라는 간편한 Hooks들을 지원해주는 것을 알았다.
(하마터면 connect() 함수로 프로젝트 진행할뻔)
이를 계기로, react-redux에서 지원하는 Hooks 들의 종류와 기능에 대해 한번 공부해보았다.
💜 React-Redux의 Hooks 적용
React-Redux 7버전 이후로, React 에 대한 Redux 관련 Hooks API들을 제공하게 되었다.
이에 따라, 이전의 connect() HOC(Higher-Order Component) 작업이 불필요해진 것이다. (mapStateToProps 등)
즉, Container 컴포넌트 Wrapping 할 필요가 없어졌으며, 컴포넌트 자체에서 스토어의 참조와 디스패치가 가능하단 것이다.
- 필수: Provider Wrapping
const store = createStore ( rootReducer )
ReactDOM.render (
<Provider store={store}>
<App />
</Provider> ,
document.getElementById('root')
)
기존 React-Redux와 마찬가지로 최상위를 Provider로 랩핑해준다. 그 내부의 컴포넌트들은 Hooks를 통해 스토어에 접근할 수 있다.
💜 useSelector()
스토어에 저장된 상태를 참조하는 Hooks 이다. (mapStateToProps 역할과 유사)
import React from 'react'
import { useSelector } from 'react-redux'
export const CounterComponent = () => {
const counter = useSelector(state => state.counter)
return <div>{counter}</div>
}
우선, useSelector() Hooks를 react-redux 에서 import 해준다.
그리고, 필요한 state를 변수에 담는다. 콜백함수를 인자로 받으며, state(스토어)에서 필요한 필드를 가져오면 된다.
// Bad
const {user, isLoggedIn} = useSelector((state) => state.user);
// Good
const isLoggedIn = useSelector((state) => state.user.isLoggedIn);
위 경우가 Bad인 이유는, 만약 현재 컴포넌트에서 user state가 불필요한 경우이다.
스토어에서 state가 수정되면 이를 참고하는 모든 컴포넌트가 리렌더되기 때문에, 최적화를 위해 아래처럼 필요한 state만 가져오는 것이다.
- useSelector() 최적화
useSelector() 는 스토어의 상태를 참고하는 Hooks 라고 했다.
이 상태가 변경되면, 연계된 컴포넌트들이 리렌더되므로 이를 최적화하는 3단계를 소개해보겠다.
1) 독립 선언(useSelector 여러 번 사용)
// Bad
const { count, prevCount } = useSelector((state: RootState) => ({
count : state.countReducer.count,
prevCount: state.countReducer.prevCount,
}));
// Good
const count= useSelector((state: RootState) => state.countReducer.count);
const prevCount= useSelector((state: RootState) => state.countReducer.prevCount);
Bad 경우는, 두 state를 하나의 객체처럼 선언한 경우이다. 물론 메모리적인 이점도 있겠지만.
state 객체 하나만 수정해도 두 필드가 연결된 컴포넌트들이 모두 리렌더되기 때문에, Good 경우처럼 셀렉터를 여러 번 쓰는 것이 유리하다.
2) equalityFn 파라미터
useSelector는 두 번째 인자로, equalityFn 이라는 함수를 받는다.(선택옵션) 문법은 다음과 같다.
equalityFn?: (prev, next) => boolean
prev와 next는 전후의 state 이며, 이를 비교한 boolean이 true면 리렌더를 생략, false면 리렌더를 진행한다.
3) shallowEqual 함수
useSelector의 equalityFn에 대입할 수 있는 함수이다. 전후비교의 개념은 같지만, 최상위 값만 비교한다.
state = {
selectedId: 0,
username: 'taeng',
friends: [
{id: 0, name: 'hyemi'},
{id: 1, name: 'taejin'},
],
hobby: {
indoor: 'game',
outdoor: 'surfing',
}
}
이러한 state를 예로 들면, 최상위 요소인 selectedId, username 변경여부만 확인할 것이다. (friends 배열, hobby 객체 무시)
💜 useDispatch()
디스패치 함수를 실행하는 Hooks 이다. (mapDispatchToProps 역할과 유사)
import React from 'react'
import { useDispatch } from 'react-redux'
import { openLoginModalScreen } from "../reducers/global";
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch(openLoginModalScreen)}>
Increment counter
</button>
</div>
)
}
마찬가지로, useDispatch() Hooks를 react-redux 에서 import 해준다. 그리고, 변수에 처리없이 담는다. (Hooks 자체가 디스패치)
디스패치 함수이므로, 내부 인자는 action 객체를 받는다. 그렇기 때문에, action 객체나 생성함수를 별도로 만들어주면 유용하다.
* 규모가 작은 프로젝트는 각 Reducer 파일에 action 함수를 포함, 큰 프로젝트는 Action 디렉토리에 별도로 action 함수들만 모은 파일로 관리
- useDispatch() 와 useCallback()
공식문서는, 자식 컴포넌트에 디스패치를 props로 하달할 때, useCallback() Hooks로 메모이즈된 콜백형태를 권장한다.
import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
const incrementCounter = useCallback(
() => dispatch({ type: 'increment-counter' }),
[dispatch]
)
return (
<div>
<span>{value}</span>
<MyIncrementButton onIncrement={incrementCounter} />
</div>
)
}
export const MyIncrementButton = React.memo(({ onIncrement }) => (
<button onClick={onIncrement}>Increment counter</button>
))
- JSX 인라인 코딩에서 분리하여 가독성을 높인다.
- 자식 컴포넌트의 불필요한 리렌더를 방지할 수 있을 것 같다. (디스패치 변동이 없다면 리렌더 발생하지 않음)
💜 useSelector, useDispatch 활용 예시
Dev Ed의 클론 강의에서, App.js 에 적용된 useSelector와 useDispatch의 React 적용 예시이다.
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './Actions/Action';
function App() {
const counter = useSelector(state => state.counter.count)
const isLogged = useSelector(state => state.logged.isLogged)
const dispatch = useDispatch();
return (
<div className="App">
<h1>Counter: {counter}</h1>
{isLogged && <h3>Valuable Information I shouldn't see.</h3>}
<button onClick={() => dispatch(increment(1))}>+</button>
<button onClick={() => dispatch(decrement(1))}>-</button>
</div>
);
}
export default App;
* combineReducers
reducer 함수들을 기능별로 분리했고, 이들이 스토어에 참조되기 위해 하나의 리듀서로 묶여야 한다. 이 때 사용되는 메서드이다.
스토어는, 모든 리듀서가 묶인 RootReducers 만 참조하면 된다.
// Reducer.js
import { combineReducers } from 'redux';
import counterReducer from './counter';
import loggedReducer from './logged';
const RootReducers = combineReducers({
counter: counterReducer,
logged: loggedReducer,
})
export default RootReducers;
// Store.js
import { createStore } from 'redux';
import RootReducers from './Reducers/Reducer';
const store = createStore(
RootReducers,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
);
export default store;
하마터면, 컨테이너 컴포넌트들 만드느라 이골이 날 뻔 했다!!! 😅😅😅
Redux Hooks 들이라는 유용한 기능으로 좀 더 편리하고 트렌디한 투두 리스트를 만들어봐야 겠다.
리덕스의 장점중 하나가 바로 생태계이고, 이를 기반으로 많은 기능들을 지원해준다. (이를 미들웨어라고 칭한다.)
Redux 스택에 종종 같이 등장하는, Redux-thunk, Redux-saga 와 같은 미들웨어들에 대해서 내일 공부해보려고 한다!!
[출처]
- Redux 공식문서: react-redux.js.org/api/hooks
- HotHandCoding 님의 블로그: darrengwon.tistory.com/559
- 황은지 님의 블로그: velog.io/@hwang-eunji/%EB%A6%AC%EB%8D%95%EC%8A%A4%EB%A6%AC%EC%95%A1%ED%8A%B8-Redux-with-React-8-useSelector-useDispatch-hooks