본문 바로가기
프론트엔드/React

리액트 Redux: 쿠팡의 서울본점 창고를 기억하자!

by maverick11471 2024. 9. 17.

  서울 쿠팡의 본점 창고를 운영한다고 가정하자. 이 쿠팡의 창고는 거대한 창고 내에 지역별(인천, 경기도, 전라도, 부산 등....)로 구분되어 있다. 소비자가 주문을 클릭하면 택배상자에는 어느 지역으로 가는지와, 어떤 상품이 들어있는지 구분되어 있다. 소비자가 주문을 하면 각 물건을 제조하는 회사에서 쿠팡의 창고로 택배차가 날라 줄 것이다. 택배기사는 물건주문인지, 반송인지를 구분하고 택배상자를 싣는다. 그리고 최종적으로 택배창고의 물건목록을 최신화한다. 이 쿠팡 창고는 택배의 '상태 관리'를 위해 존재한다고 할 수 있다.

  이를 Redux에 접목시켜 보자. Redux는 Store라는 중앙집권화 방식(서울 본점 창고)으로 운영되며 상태를 관리하는데, 이는 각각의 Slice를 결합하여 관리한다(인천, 경기도, 전라도 부산 등....). 사용자가 화면을 클릭하는 등의 Action이 발생하게 될 때 Action의 속에는 type('액션 유형')과 payload('데이터')가 들어있다(택배기사는 물건주문인지, 반송인지를 구분하고 택배상자를 싣는다.). dispatch(택배)가 이 Action을 Store에 가져다 줄 것이다. Action 함수는 Store 안에 있는 middleware를 거칠 수도 있는데, thunk middleware는 비동기 액션을 생성할 수도 있고(배송지가 적절한지 동시에 확인하는 작업), saga middleware는 특정 액션에 에러 핸들링을 할 수도 있다(문제가 생긴 택배를 찾아 즉시 대처하고 고객에게 알려주기도 한다). 전부 끝나게 되면 Reducer가 액션을 기반으로 상태를 업데이트하게 된다(그리고 최종적으로 택배창고의 물건목록을 최신화한다). 이러한 특징 때문에 Redux를 '상태 관리'를 위해 필요하다고 할 수 있다.

1. store: 쿠팡 중앙 창고
2. slice: 중앙창고 내 지역단위로 분류해 놓은 창고
3. action: 소비자의 요청(주문? 반송?)을 구분해주는 시스템
	3-1. type: 택배가 주문인지 반송인지 구분
    3-2. payload: 택배상자
4. dispatch: 물건을 갖다주는 택배차
5. reducer: 물건을 주문한 소비자에게 갖다주는 택배기사
6. middleware: thunk(동시다발적으로 시행), saga(문제점 식별)

  Props, Context API, Redux 모두 단방향이지만 Redux는 다른 점이 모든 컴포넌트에서 Store에서 저장되어 있는 액션 함수를 사용하기 때문에 '예측 가능하다'라고 볼 수 있다. 예측 가능하다는 말은 Store가 하나만 운영되고, Store 안에 Reducer 함수는 '현재 상태 + 액션 함수'를 받아 구현되기 때문에 출력값이 예측 가능하다는 뜻이다.

  Reducer 함수는 기능이 매우 많다. reducer 함수는 앞서 말한듯이 '현재 상태 + 액션 함수'를 받아 새로운 상태를 구현하는 'Reducer 함수의 기능'이자 동시에 액션객체를 반환하는 '액션 생성자 함수'의 역할도 수행하고 있다. '액션 생성자 함수'는 dispatch로 해당 reducer함수가 호출될 때 액션객체를 생성한다고 보면 된다. 또한 Reducer는 상태를 직접 변경하는 것이 아닌 객체를 복사한 후 변경하기 때문에 '불변성'을 유지할 수 있다. 또한 화면단에서는 useSelector로 Store에 저장되어 있는 상태를 구독(Subscribe)할 수 있는데 이는 읽기 전용 '훅(Hook)'이기 때문에 수정을 할 수 없다. 진행과정을 수정하려면 Store에 있는 Reducer를 수정해야 한다.

// '액션 생성자 함수'로서의 역할
const action = get_todos(response.data.items);
// action = { type: 'todos/get_todos', payload: response.data.items }

// '리듀서' 로서의 역할
const newState = get_todos(currentState, action);

 

1. Store.js 생성(쿠팡창고 생성)

  쿠팡창고를 만들 차례다. 각 지역창고에 어떤 택배기사를 배정해줄지를 'combineReducers'라고 하고, 서울에 쿠팡창고를 짓는 것을 configureStore이라고 한다.

  우선 reducjs/toolkit을 설치해준다. reduxjs/toolkit은 위 2개의 기능과 더불어 불변성(내용이 변하지 않게 해주는, 예로 ...state가 있다) 의 기능도 포함하고 있다.

npm install @reduxjs/toolkit react-redux
import {combineReducers, configureStore} from "@reduxjs/toolkit";

// todoSliceTwo 지역창고에는 택배기사들을 배정해줄게
const ReducerRoot = combineReducers(
    todoSliceTwo
);

// 쿠팡창고를 설치해주세요.
const storeTwo = configureStore({
        reducer: ReducerRoot
    }
);

export default storeTwo;

 

 

2. todoSliceTwo.js 생성 (지역창고 생성)

 

  지역창고를 만들 때 이름과 처음 택배 목록들, 지역창고를 도맡는 택배기사를 지정해 둔다. 택배차가 택배를 가져오게되는데, 예를 들어 고객이 반품을 요구한 택배가 있다고 한다. 그 택배를 가져오면 택배목록을 최신화해야 한다. 택배목록을 최신화 한 것에 대해 택배기사는 인지하고 있어야 한다.

  이를 코딩으로 바꿔보면 우선 Slice를 만들고 이에 대한 이름, 처음 상태, reducers를 지정해둔다.(지역창고를 만들 때 이름과 처음 택배 목록들, 지역창고를 도맡는 택배기사를 지정해 둔다.) dispatch가 고객의 요청을 가져오면(택배차가 택배를 가져오게되는데,) reducers는 해당 액션과 정보를 확인하여 상태를 바꾼다.(예를 들어 고객이 반품을 요구한 택배가 있다고 한다. 그 택배를 가져오면 택배목록을 최신화해야 한다.)

1. createSlices(지역창고 생성)
 1-1. name(지역창고 이름)
 1-2. initialState: todos([])(택배목록)
 1-3. reducers(택배기사)
  1-3-1. get_todos(택배기사 이름)
  1-3-2. ...state(처음택배목록, initialState)
  1-3-3. action(반송), payload(택배상자)
  1-3-4. todos(택배목록 최신화)
2. export const {get_todos} = todoslice.actions; (get_todos는 지역창고에서 반송물품 가지러 출동해라!)
3. export default todoSlice.reducer; (나머지 지역창고 택배기사 전부 출동)
import {createSlice} from '@reduxjs/toolkit';

// redux에서 관리하는 상태변수 선언
const todoSlice = createSlice({
    name: 'todos',
    initialState: {
        todos: [],
        // 만약 todos의 상태가 a: 1, b: 2 라면
    },
    reducers: {
        get_todos: (state, action) => ({
            // ...state의 상태도 a: 1, b: 2가 된다.
            ...state,
            // action 생성 함수에서 매개변수로 전달되는 값들은 모두 action 객체의
            // payload 속성에 담긴다.
            // response.data.items가 전달된다.
            todos: action.payload
        })


    }
});

// 위에서 지정한 reducer 함수명으로 action creator 메소드가 생성된다.
// action creator 메소드를 이용해서 dispatch를 한다.
export const {get_todos} = todoSlice.actions; 

export default todoSlice.reducer;

 

 

3. todoList .js 생성(현장에 도착한 택배기사)

  현장에 도착한 택배기사 'get_todos'는 택배목록을 가져오고 택배차에 물건을 싣는다. 

import React, {useCallback, useEffect} from 'react';
import {useDispatch, useSelector} from "react-redux";
import axios from "axios";
import {get_todos} from "../slices/todoSlice";
import TodoListItem from "../components/TodoListItem";

const TodoList = () => {

    // 택배목록 가져오기
    const todos = useSelector(state => state.todoSlice.todos);

    // 택배차
    const dispatch = useDispatch();

    const getTodos = useCallback(async () => {
        try {
            const response = await axios.get('http://localhost:9090/todos', {
                headers: {
                    Authorization: `Bearer ${sessionStorage.getItem('ACCESS_TOKEN')}`
                }
            });

            // get_todos: 택배차에 반송되는 아이템들 싣겠습니다!
            dispatch(get_todos(response.data.items));
        } catch (e) {
            if (e.response.status === 403) {
                alert('로그인이 필요합니다.');
            }
        }
    }, [dispatch]);

    useEffect(() => {
        getTodos();
    }, []);

    return (
        <div className='TodoList'>
            {/* 택배리스트 최신화한것들 펼쳐놓기! */}
            {todos && todos.map(todo =>
                <TodoListItem key={todo.id} todo={todo}/>
            )}
        </div>
    );
};

export default TodoListTwo;

 

 

4. 실행

 

  쿠팡창고에서 제공하는 모든 일련의 과정들을 실행시키면 된다. 

import TodoInsert from "./components/TodoInsert";
import TodoTemplate from "./pages/TodoTemplate";
import TodoList from './components/TodoList';
import {Provider} from 'react-redux';
import store from "./store/store";

function App() {
  return (
    // redux 상태변수를 사용할 컴포넌트들을 Provider 컴포넌트로 감싸고
    // store를 연결해준다.
    <Provider store={store}>
      <>
        <TodoList/>
      </>
    </Provider>
  );
}

export default App;