Front-End(Web)/React - 라이브러리들

[Redux] Hooks - useSelector, useDispatch

ttaeng_99 2021. 2. 17. 02:55
반응형

 😅 서론

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  

반응형