ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Redux] Hooks - useSelector, useDispatch
    Front-End(Web)/React - 라이브러리들 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  

    반응형
Designed by Tistory.