Javascript/React

React에서 Redux를 이해하며 사용해보기 1편

hamsoter 2022. 6. 1. 19:48

count state를 Redux로 관리하는 간단한 예제를 만들어보며 Redux에 대한 학습을 해보자.

이 포스트는 react가 앱에 미리 설치된 환경이라는 가정 하에 진행한다. (create-react-app 사용함)

npm 설치 (react-redux)

Redux는 바닐라 자바스크립트에서도 동작하지만. react에서의 Redux를 사용하기 위해서는 따로 react-redux도 함께 설치해주어야 한다. 

npm install redux react-redux

먼저, store가 필요합니다!

먼저 state들을 저장할 store를 만들어야 한다. 그리고 그에 인자로 부여할 reducer함수도 함께 정의해야 한다.

store란 무엇이며 reducer란 무엇일까? 차근차근 하나씩 이해해보자.

store란?

Redux store는 앱을 위한 하나의 중앙 데이터(state) 저장소라고 보면 된다.

그러니까 한 개의 커다란 store에 state들이 저장되어 있고, 컴포넌트들은 그 저장소를 구독하는 형식이다.

컴포넌트는 state의 값을 직접적으로 조작하지 않고, reducer함수를 이용하여 구독한 저장소의 데이터를 변경한다.

store를 만들기 위해서는 createStore() 메서드를 사용하면 되고, 인자로는 reducer 함수를 넣어주면 된다.

그런데 잠깐, reducer함수란 무엇일까?

reducer란?

reducer의 input/output에 대한 이해를 돕기 위한 그림

reducer함수는 사실 redux에서만 사용하는 개념이 아니다. 입력을 받고 그 입력을 변환하여 새로운 결과를 출력을 하는 함수를 의미하며 이는 프로그래밍 언어에서 넓게 다루는 개념이다.

Redux에서 의미하는 reducer함수란 state의 업데이트를 위한 함수라고 이해하면 된다.

우선 reducer함수는 파라미터로 2개의 값이 필요하다 첫 번째 인자는 기존의 state, 두 번째 인자는 발송된 action이다.

결국 reducer함수는 action을 보고 state를 어떻게 처리할지 결정한 후, 최종적으로는 새로운 최신 상태의 state객체를 리턴한다.

그렇게 state가 업데이트 되면, store를 구독 중인 컴포넌트까지 업데이트된다.

return 하는 값에 대하여

reducer 함수가 리턴하는 값은 언제나 새로운 state 객체 자체이다. 그러니까 기존의 state를 덮어버린단 말이다. redux는 원본 state를 건드리는 것을 권장하지 않는다. 덮어쓰는 방식의 업데이트를 권장한다. 그러니 이 점을 유의하며 reducer함수를 정의해야 한다.

이제 reducer함수와 store를 정의해보자.

createStore의 대안 configureStore?

createStore메서드를 이용하여 store를 만드려고 했으나, createStore는 더 이상 사용되는 메서드가 아니라는 경고가 떴다.

대신 reduxjs/toolkit의 configureStore 메서드를 사용하라고 제안한다.

찾아보니 configureStore는 createStore와 동일한 역할을 하지만 더 다양한 기능들이 확장된 버전이라고 한다. 차이가 있다면 인자로 넣어줄 때의 형식 정도이다.

기존 createStore는 createStore(reducerFnc)의 형태로 직접 인자를 주입했다면, configureStore는 configureStore({reducer: reducerFnc}) 형식으로 인자에 객체를 주입해주어야 한다.

이러면 안 쓸 이유가 없겠지… 하지만 설치하기 위해선 추가로 redux toolkit을 설치해줘야 한다.

npm 설치 (@reduxjs/toolkit)

npm install @reduxjs/toolkit

혹은 createStore를 그대로 사용하고 싶을 수도 있을 것이다. 그렇다면 굉장히 찝찝한 이름인 legacy_createStore(reducerFnc)를 사용하여 진행할 수도 있다.

이제 store를 생성하는 법을 익혔으니 한번 store를 만들어보자.

store/index.js (store)

import { configureStore } from "@reduxjs/toolkit";

const counterReducer = (state = { counter: 0 }, action) => {
  if (action.type === "increment") {
    return {
      counter: state.counter + 1,
    };
  }

  if (action.type === "decrement") {
    return {
      counter: state.counter - 1,
    };
  }

  return state;
};

const store = configureStore({ reducer: counterReducer });

export default store;

위와 같이 counter state를 관리하는 store를 만들었다.

counter state를 1씩 증가시키거나 감소시키는 reducer도 함께 정의하였다.

그리고 이제 만든 store를 react app에 연결해볼 차례이다.

App 컴포넌트를 리턴하는 최상위 파일인 src/index.js 파일에서 연결해주면 된다.

src/index.js

//  src/index.js

import { Provider } from "react-redux";
import Counter from "./components/Counter";
import store from "./store/index";

function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

export default App;

App.js에서 최상위 컴포넌트를 react-redux의 Provider로 감싸주었다.

이제 store와 연결을 마쳤다.

그러면 다른 컴포넌트에서 redux store의 데이터를 사용하기 위해선 어떻게 해야 할까?

counter를 표시해주는 Counter 컴포넌트에서 store에 접근하여 counter state를 출력해보자.

store의 state를 가져와봅시다 (useSelect hook)

컴포넌트에서 store를 가져오는 방법은 react-redux의 hook을 이용하면 되는데 대표적으로 useSelector, useStore 둘 중 하나를 사용하면 된다.

혹시나 class 컴포넌트에서 사용할 경우는 connect를 사용한다.

이번 예제에서는 useSelector를 사용할 것이다.

useSelector로 store state를 가져오기

useSelecto는 항상 최신 상태의 state를 얻을 수 있는 간편하고 유용한 hook이다.

redux가 컴포넌트가 업데이트될 때마다 자동으로 store에서 최신의 state를 받아온다. 그리고 redux store의 state가 변경될 때 자동으로 컴포넌트의 함수도 실행이 된다.

src/Components/Counter.js

import { useSelector } from "react-redux";
import classes from "./Counter.module.css";

const Counter = () => {
  // store에서 redux가 관리하는 counter state를 가져옴
  const counter = useSelector((state) => state.counter);

  console.log(counter);
  const toggleCounterHandler = () => {};

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      <div className={classes.value}>{counter}</div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;

 

정상적으로 counter가 출력되는 모습

 

성공! 하지만 store에서 counter state를 가져와 출력하는 것뿐만 아니라 store의 couter state의 값을 바꾸기 위해서는 어떻게 해야 할까?

store의 state를 바꿔봅시다 (Dispatch)

일단 기존에 store를 생성할 때 reducer함수에 증가와 감소 기능을 구현해두었으니, 컴포넌트 측에서 reducer함수에 접근하여 올바른 action을 발송해준다면 될 것이다. 그럼 이제부터 이걸 어떻게 컨트롤할 수 있을지에 대해서 알아보자.

우선 Counter 컴포넌트에 두 가지 버튼을 생성했다. 각각 클릭할 시 store의 count가 증가, 감소되도록 할 것이다.

useDispatch

그리고 컴포넌트 측에서 store의 값을 변경하기 위해서는, useDispatch라는 hook을 호출해야 한다.

const dispatch = useDispatch();

dispatch({ type: "ACTION_TYPE" });

dispatch라는 변수에 useDispath를 호출한 후 dispatch의 인자에 객체를 넣으면 그 객체는 reducer함수의 action에 할당된다.

const counterReducer = (state, **action**) => {
		console.log(**action**.type);     // "ACTION_TYPE"
};

이제 배운 내용을 토대로 버튼에 couter 증가, 감소 이벤트를 적용해보도록 하자.

src/Components/Counter.js

import { useDispatch, useSelector } from "react-redux";
import classes from "./Counter.module.css";

const Counter = () => {
  const dispatch = useDispatch();
  // store에서 redux가 관리하는 counter state를 가져옴
  const counter = useSelector((state) => state.counter);

  const incrementHandler = () => {
    dispatch({ type: "increment" });
  };

  const decrementHandler = () => {
    dispatch({ type: "decrement" });
  };
  const toggleCounterHandler = () => {};

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      <div className={classes.value}>{counter}</div>
      <div>
        <button onClick={incrementHandler}>Increment</button>
        <button onClick={decrementHandler}>Decrement</button>
      </div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;

store에 payload 전달하기

하지만 action type 뿐만 아니라… 컴포넌트 측에서 다른 추가 정보를 store로 보내주는 방법은 없을까?

예를 들어 버튼을 하나 추가해서, 클릭했을 시 5개의 카운트를 한 번에 증가시키도록 하는 것이다.

 

물론 이 경우도 마찬가지로 dispatch시 type를 따로 주어서 reducer함수에서 if문으로 받아서 작업하면 될지도 모른다. 하지만 카운트의 수가 5로 고정된 것이 아니고 유동적으로 바뀐다면? 컴포넌트 내 input으로 입력받은 수만큼 카운트가 증가되게 하고 싶다면? 하나하나 숫자 케이스대로 reducer함수에 if문(혹은 switch문)을 하드코딩하는 건 힘들 것이다.

action객체의 이용

다행히도 dispatch를 보낼 때의 action 객체는 개발자 입장에서 유동적으로 수정할 수 있다.

type 뿐만 아니라 임의의 다른 데이터들을 전달할 수 있는 것이다.

5개의 카운트의 이름을 amount라고 하여 reducer에 전달할 경우 이런 모습이다.

const increaseHandler = () => {
    dispatch({ type: "increase", amount: 5 });
  };

그리고서 reducer함수에 임의의 숫자만큼 카운트를 증가시키는 type을 새로 정의했다.

store/index.js (store)

const counterReducer = (state = { counter: 0 }, action) => {
  if (action.type === "increment") {
    return {
      counter: state.counter + 1,
    };
  }

  if (action.type === "increase") {
    return {
      counter: state.counter + action.amount,
    };
  }

  if (action.type === "decrement") {
    return {
      counter: state.counter - 1,
    };
  }

  return state;
};

오늘은 이렇게 간단한 카운트 예제를 만들어보며 Redux에 대한 기초를 학습해보았다.

다음 2편 포스트에서는 react-toolkit에 대한 이야기를 조금 더 다뤄보려고 한다.