728x90

리덕스(Redux), 리덕스 툴킷(Redux Toolkit)

들어가며

  • 리액트(React.js)의 전역 상태 관리 라이브러리 중의 하나인 리덕스(Redux)리덕스 툴킷(Redux Toolkit)에 대해 정리해본다.

 

리덕스(Redux)

개념

  • 자바스크립트 애플리케이션에서 상태 관리를 위해 사용되는 상태 컨테이너
  • 애플리케이션의 상태를 중앙에서 관리함으로써 상태 변화를 쉽게 추적하고, 디버깅을 단순화하며, 다양한 컴포넌트 간에 상태를 공유하는 것을 쉽게 해준다.
  • 리덕스를 사용하기 위해서는 아래의 명령을 실행하여 관련 패키지를 설치해준다.
$ yarn add redux           # npm install redux
$ yarn add react-redux     # npm install react-redux

 

 

리덕스(Redux)는 리액트(React.js) 뿐만 아니라 다양한 자바스크립트 패키지에서도 사용할 수 있다.

 

 

 

액션(Action)

  • 애플리케이션에서 일어나는 사건을 설명하는 객체
  • @type@ 속성을 필수로 가지며, 상태 변경을 트리거하는 역할을 한다.
  • 예) @{ type: 'INCREMENT' }@, @{ type: 'ADD_TODO', text: 'Learn Redux' }@

 

리듀서(Reducer)

  • 액션을 처리하여 새로운 상태를 반환하는 함수
  • 이전 상태 액션 객체를 인자로 받아 새로운 상태 객체를 반환한다.
  • 순수 함수여야 하며, 입력이 같으면 출력도 항상 같아야 한다.
    • 순수 함수(Pure Function)
      • 동일한 인자가 주어졌을 때 항상 동일한 결과를 변환
      • 함수의 실행이 외부 상태에 의존하지 않음
      • 외부 상태를 변경하지 않는 함수
function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

 

스토어(Store)

  • 애플리케이션의 상태를 담고 있는 객체
  • @createStore@ 함수를 사용하여 스토어를 생성한다.
  • 스토어는 3가지 메소드를 가진다:
    • @getState()@: 현재 상태 반환합
    • @dispatch(action)@: 상태를 변경하는 액션 보내기
    • @subscribe(listener)@: 상태가 변경될 때마다 호출되는 리스너를 등록하기

 

미들웨어(Middleware)

  • 액션이 리듀서에 도달하기 전에 처리할 수 있는 기능을 제공한다.
  • 로깅, 에러 보고, 비동기 작업 처리 등에 사용된다.

 

사용법

// 1. 스토어 생성
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);


// 2. 액션 디스패치
store.dispatch({ type: 'INCREMENT' });
console.log(store.getState());    // 1


// 3. 구독
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

store.dispatch({ type: 'INCREMENT' });   // 2

unsubscribe();

 

예제 코드

store/index.js
  • 프로젝트의 @src@ 폴더 내부에 @store@ 폴더를 생성한 후, 안에 @index.js@ 파일을 생성한다.
import { createStore } from 'redux';

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

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

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

  if (action.type === "toggle") {
    return {
      showCounter: !state.showCounter,
      counter: state.counter,
    };
  }

  return state;
};

// 중앙 스토어 생성 및 리듀서 함수 연결
const store = createStore(counterReducer);

export default store;

 

Counter.js
import { useSelector, useDispatch } from "react-redux";

import classes from "./Counter.module.css";

const Counter = () => {
  const dispatch = useDispatch();
  const counter = useSelector((state) => state.counter);
  const show = useSelector((state) => state.showCounter);

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

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

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

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

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

export default Counter;

 

src/index.js
import React from "react";
import ReactDOM from "react-dom/client";

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

import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

 

사용 시 주의 사항

  • 리덕스를 사용할 때 절대로 다음과 같이 변수 자체를 변형(Mutation)시키지 않는다.
  if (action.type === "increment") {
    state.counter++;
    
    return state;
  }

 

  • 그 대신에 다음과 같이 객체로 오버라이딩한다.
  if (action.type === "increment") {
    return {
      counter: state.counter + 1,
      showCounter: state.showCounter,
    };
  }

 

상태 업데이트 시 일부 상태만 설정하면 나머지 상태는 undefined로 처리되어 예상치 못한 결과를 초래할 수 있다. 따라서 상태 업데이트를 할 때는 모든 기존 상태를 포함하도록 새로운 상태 객체를 반환해야 한다.

 

 

참고 : 리덕스 툴킷(Redux Toolkit)

  • 리덕스를 더 쉽고 효율적으로 사용하기 위해 리덕스 툴킷(Redux Toolkit)을 사용할 수 있다.
$ npm install @reduxjs/toolkit   # yarn add @reduxjs/toolkit
$ npm install react-redux        # yarn add react-redux

 

  • 리덕스 툴킷은 리덕스의 일반적인 보일러플레이트 코드를 줄이고, 직관적이며 간결한 코드 작성을 가능하게 한다.
    • 기본 리덕스를 사용할 때는 많은 설정과 보일러플레이트 코드가 필요하다.
      • 예를 들어, 액션 생성자, 리듀서, 스토어 설정 등을 별도로 작성해야 하는데 리덕스 툴킷은 이러한 설정을 단순화하여 개발자의 작업을 줄여준다.
  • 리덕스 툴킷은 @configureStore@ 함수를 제공하여 스토어를 쉽게 설정할 수 있다.
    • 이 함수는 기본적인 미들웨어 설정과 디버깅 도구 설정을 자동으로 처리해준다.
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';

const store = configureStore({
  reducer: rootReducer,
});

 

 

  • 리덕스 툴킷의 @createSlice@ 함수는 액션 생성자리듀서를 한 번에 생성할 수 있게 해준다.
    • 리덕스의 핵심 개념을 더 직관적이고 간결하게 구현할 수 있다.
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: (state) => state + 1,
    decrement: (state) => state - 1,
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

 

  • 리덕스 툴킷은 비동기 작업을 간편하게 처리할 수 있는 @createAsyncThunk@ 함수를 제공한다.
    • 비동기 작업의 상태 관리(로딩, 성공, 실패 등)를 쉽게 처리할 수 있다.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';

export const fetchUser = createAsyncThunk('users/fetchUser', async (userId) => {
  const response = await axios.get(`/api/users/${userId}`);
  return response.data;
});

const userSlice = createSlice({
  name: 'user',
  initialState: {
    user: null,
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false;
        state.user = action.payload;
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  },
});

export default userSlice.reducer;

 

  • 리덕스 툴킷은 리덕스 개발자 도구(Redux DevTools)와의 통합하여 상태 변화를 시각적으로 추적하고 디버깅하는 데 큰 도움을 준다.
  • 리덕스 툴킷은 타입 스크립트(TypeScript)와의 호환성을 강화하여, 타입 안전성을 유지하면서도 리덕스를 간편하게 사용할 수 있다.
  • 리덕스 툴킷은 @immer@를 사용하여 불변성 관리를 자동으로 처리해주며, 기본적으로 Redux Thunk 미들웨어를 포함한다.
    • 이러한 설정들은 개발자가 직접 최적화할 필요 없이 기본적으로 제공되어 개발 속도를 높여준다.

 

참고 사이트

 

Redux - A JS library for predictable and maintainable global state management | Redux

A JS library for predictable and maintainable global state management

redux.js.org

 

Redux Toolkit | Redux Toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

redux-toolkit.js.org

 

728x90