[Redux] Redux로 전역 상태 관리하기 (feat. RTK(Redux Toolkit)

mhlog·2023년 7월 8일
1

React

목록 보기
8/10
post-thumbnail

Redux 공식문서에서는 현재 Redux 로직을 작성하고 있다면 RTK(Redux Toolkit)을 이용하여 코드를 작성하는 것을 추천하고 있다. 하지만 RTK를 배우기 전에 Redux의 개념을 복습하고, RTK와 어떠한 차이가 있는지 몸소 느끼는 것이 좋을 것이라 생각해 Redux와 RTK 모두 다루어볼 생각이다. 벨로퍼트님의 블로그Redux 공식문서를 참고하였습니다.

요즘 트렌드는 RTK?

자세한 내용은 Redux 공식문서(Redux Toolkit이 오늘날 Redux를 사용하는 방법인 이유)을 참고하면 된다. 간단히 요약하자면

Redux를 사용하기 위해서는 다음과 같은 동작들이 보통 포함되게 된다.

  • 액션 객체를 생성하는 액션 생성자
  • 부수 효과를 가능하게 하는 미들웨어
  • 부수 효과를 가진 동기 또는 비동기 로직을 포함하는 Thunk 함수
  • ID로 항목 조회를 가능하게 하는 정규화된 상태

등등.. 상태관리를 위해서 부수적으로 해줘야할 작업들이 너무 많다는 것이다. 이러한 장황하고 반복적인 코드를 줄이기 위해서 나온 것이 RTK(Redux Toolkit)이다.

Redux에서 사용되는 키워드

액션 (Action)

  • 상태에 변화가 필요할 때 발생 (객체로 표현)
  • type을 필수로 그외의 값들은 개발자 마음대로 생성

액션 생성함수 (Action Creator)

  • 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함.
  • 필수는 아님.

리듀서 (Reducer)

  • 변화를 일으키는 함수
  • 현재의 상태와 액션을 참조하여 새로운 상태를 반환

스토어 (Store)

  • 한 애플리케이션 당 하나의 스토어
  • 현재의 앱 상태와 리듀서, 내장함수 포함

디스패치 (dispatch)

  • 스토어의 내장 함수
  • 액션을 발생시키는 것

구독 (subscribe)

  • 스토어의 내장함수
  • subscribe 함수에 특정 함수를 전달해주면, 액션이 디스패치 되었을 때 마다 전달해준 함수가 호출 (리액트에서는 connect 함수 또는 useSelector Hook 을 사용)

Store 생성하기

용어만 처음 들었을 때 이해하기는 쉽지 않다. 간단한 Counter 예제로 위에서 설명한 용어와 함께 접목해서 이해하면 훨씬 이해가 쉬울 것이다.

우선 설치부터.

npm i redux
yarn add redux

Redux를 사용할 때 여러가지 패턴이 있지만, Reducer와 Action 관련 코드들을 하나의 파일에 몰아서 작성하는 패턴인 Duck 패턴을 이용해서 예시를 들도록 하겠다.

// src/modules/counter.ts
/* 액션 타입 만들기 */
// Ducks 패턴을 따를땐 액션의 이름에 접두사를 넣어주세요.
// 이렇게 하면 다른 모듈과 액션 이름이 중복되는 것을 방지 할 수 있습니다.
/* 
  ** 여기서 as const라는 키워드를 사용하지 않으면 ReturnType을 사용하게 됐을 때 
  ** type의 타입이 무조건 string으로 처리되기 때문에 
  ** Reducer를 제대로 구현할 수 없습니다. 
*/
const SET_DIFF = 'counter/SET_DIFF' as const;
const INCREASE = 'counter/INCREASE' as const;
const DECREASE = 'counter/DECREASE' as const;

/* 액션 생성함수 만들기 */
// 액션 생성함수를 만들고 export 키워드를 사용해서 내보내주세요.
export const setDiff = (diff: any) => ({ type: SET_DIFF, diff });
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

/* 초기 상태 선언 */
const initialState = {
  number: 0,
  diff: 1
};

// ReturnType 은 함수에서 반환하는 타입을 가져올 수 있게 해주는 유틸 타입입니다.
type CounterAction = 
  | ReturnType<typeof increase>
  | ReturnType<typeof decrease>
  | ReturnType<typeof setDiff>;


export default function counter(state = initialState, action: CounterAction) {
  switch (action.type) {
    case SET_DIFF: 
      return {
        ...state,
        diff: action.diff
      };
    case INCREASE: 
      return {
        ...state,
        number: state.number + state.diff
      };
    case DECREASE:
      return {
        ...state,
        number: state.number - state.diff
      };
    default: 
      return state;
  }
};

여기서 바로 아래에 createStore를 이용하여 store를 생성할 수도 있지만(아까 언급했듯이 하나의 어플리케이션에서는 하나의 스토어를 사용) 보통 프로젝트에서는 여러 Reducer가 필요하기 때문에 여러 Reducer를 합칠 수 있는 combineReducers라는 함수를 이용해서 rootReducer를 생성한 후 root 디렉토리의 index.tsx에서 store를 생성하는 방식을 많이 사용한다.

// modules/index.ts
import { combineReducers } from 'redux';
import counter from './counter';

const rootReducer = combineReducers({
  counter,
  // 다른 Reducer
});

export default rootReducer;

root 디렉토리의 index.tsx에서 store를 생성하려면 react-redux라는 라이브러리를 이용해야한다.

npm i react-redux @redux-devtools/extension
yarn add react-redux @redux-devtools/extension

그 후 Provider라는 컴포넌트를 불러와서 index.tsx에 다음과 같이 저장한다.

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { legacy_createStore as createStore } from 'redux'

import rootReducer from './modules';
import { Provider } from 'react-redux';
import { composeWithDevTools } from '@redux-devtools/extension';

const store = createStore(rootReducer, composeWithDevTools());
// console.log(store.getState());

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

chrome 검사창에서 Redux를 검사하고 싶으면 @redux-devtools/extension를 설치하고 createStore 옆에 composeWithDevTools 함수를 호출하면 된다.

App 을 감싸게 되면 우리가 렌더링하는 그 어떤 컴포넌트던지 리덕스 스토어에 접근 할 수 있다.

컴포넌트에서 리덕스 스토어에 접근하기

프리젠테이션 컴포넌트 만들기

프리젠테이셔널 컴포넌트란, 리덕스 스토어에 직접적으로 접근하지 않고 필요한 값 또는 함수를 props로만 받아와서 사용하는 컴포넌트이다.

// src/components/Counter.tsx
import React from "react";

interface CounterProps {
  number: number;
  diff: number;
  onIncrease: () => void;
  onDecrease: () => void;
  onSetDiff: (n: number) => void;
}

function Counter({
  number,
  diff,
  onIncrease,
  onDecrease,
  onSetDiff
}: CounterProps) {
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // e.target.value 의 타입은 문자열이기 때문에 숫자로 변환해주어야 합니다.
    onSetDiff(parseInt(e.target.value, 10));
  };

  return (
    <div>
      <h1>{number}</h1>
      <div>
        <input type="number" value={diff} min="1" onChange={onChange} />
        <button onClick={onIncrease}>+</button>
        <button onClick={onDecrease}>-</button>
      </div>
    </div>
  );
}

export default Counter;

프리젠테이셔널 컴포넌트에선 주로 이렇게 UI를 선언하는 것에 집중하며, 필요한 값들이나 함수는 props 로 받아와서 사용하는 형태로 구현한다.

컨테이너 컴포넌트 만들기

컨테이너 컴포넌트란, 리덕스 스토어의 상태를 조회하거나, 액션을 디스패치 할 수 있는 컴포넌트를 의미한다. 그리고, HTML 태그들을 사용하지 않고 다른 프리젠테이셔널 컴포넌트들을 불러와서 사용한다.

// src/container/CounterContainer.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease, setDiff } from '../modules/counter';

function CounterContainer() {
  // useSelector는 리덕스 스토어의 상태를 조회하는 Hook입니다.
  // state의 값은 store.getState() 함수를 호출했을 때 나타나는 결과물과 동일합니다.
  const number = useSelector(state => state.counter.number);
  const diff = useSelector(state => state.counter.diff);

  // useDispatch 는 리덕스 스토어의 dispatch 를 함수에서 사용 할 수 있게 해주는 Hook 입니다.
  const dispatch = useDispatch();
  // 각 액션들을 디스패치하는 함수들을 만드세요
  const onIncrease = () => dispatch(increase());
  const onDecrease = () => dispatch(decrease());
  const onSetDiff = (diff: number) => dispatch(setDiff(diff));
  return (
    <Counter
      // 상태와
      number={number}
      diff={diff}
      // 액션을 디스패치 하는 함수들을 props로 넣어줍니다.
      onIncrease={onIncrease}
      onDecrease={onDecrease}
      onSetDiff={onSetDiff}
    />
  );
}

export default CounterContainer;

이제 App.tsx에서 CounterContainer를 렌더링해서 카운터가 잘 동작하는지 확인한다.

크롬 개발자 도구 중 Redux를 이용하여 State의 변화 등을 관찰할 수 있다.

0개의 댓글