Redux


상태 관리 라이브러리

🥏 개요

Redux 란?

  • 컴포넌트의 상태를 하나하나 Props로 전달하면 코드가 복잡하기 때문에 이를 해결할 수 있는 기능을 제공한다.
  • 컴포넌트의 상태를 각각 컴포넌트 별 State에 따라 관리하는 것이 아닌 하나의 Store라는 곳에서 관리 할 수 있다.
  • React와만 사용할 수 있는 것은 아니고 각각의 프레임 워크에 맞춘 라이브러리가 존재한다.
  • React에는 React-Redux를 사용한다.
  • Redux는 Context API와 달리 전역 상태 관리 외에 다양한 기능을 제공한다.
  • Redux 또한 Context API를 가지고 만든 라이브러리로, 전역 상태 관리 측면에서는 차이점이 거의 없다.
  • Redux를 쓰는 이유는 다음과 같다.
  • Redux의 동작 순서

  • Dispatch 함수가 실행
  • Action이 발생
  • Reducer가 이를 받음
  • State가 변경됨
  • State가 변경되었으므로 컴포넌트가 리렌더링 됨
  • Redux 기초 세팅

  • npm i redux
  • npm i react-redux
  • index.js에서 <Provider> 컴포넌트를 import 하고 해당 컴포넌트로 <App> 컴포넌트를 감싸주어야 한다.
  • 🛷 Store, Reducer

    리덕스가 여러 State들을 관리하는 창고, State를 변환시키는 함수

    기본 사용법

  • Redux에서 createStore를 임포트 한 뒤, store 생성
  • <Provider> 컴포넌트에게 store 속성에 만들어진 store를 부여
  • import React from 'react';
    import ReactDOM from 'react-dom/client';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    import { BrowserRouter } from 'react-router-dom';
    import { Provider } from 'react-redux';
    import { createStore } from 'redux';
    let store = createStore();
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
    <BrowserRouter>
    <Provider store={store}>
    <App />
    </Provider>
    </BrowserRouter>,
    );
    reportWebVitals();
  • 이후에 State를 설정하고 State를 관리하는 Reducer 함수를 생성한다.
  • 이는 간단하게 State를 전달하는 기능만 수행한다.
  • import React from 'react';
    import ReactDOM from 'react-dom/client';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    import { BrowserRouter } from 'react-router-dom';
    import { Provider } from 'react-redux';
    import { createStore } from 'redux';
    const weight = 100;
    function reducer(state = weight) {
    return state;
    }
    let store = createStore(reducer);
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
    <BrowserRouter>
    <Provider store={store}>
    <App />
    </Provider>
    </BrowserRouter>,
    );
    reportWebVitals();
  • 이렇게 사용하면 weight 변수가 state처럼 작동해서 해당 값이 변할 때마다 리랜더링이 실행된다.
  • 또한 하나의 컴포넌트에서 값 변경이 발생해도 다른 컴포넌트들에 값 변경이 적용된다.
  • 이를 컴포넌트에서 꺼내 사용할 때는 useSelector 를 사용한다.
  • import React from 'react';
    import { useSelector } from 'react-redux';
    export default function TestRedux() {
    const weight = useSelector((state) => state);
    return <h1>당신의 몸무게는 {weight} 입니다!</h1>;
    }

    Store 통합관리

  • 폴더구조는 다음처럼 가져간다.
  • src 내부에 store 폴더를 만들고 여기에 index.js와 modules 폴더를 만든다.
  • modules 안에는 각 reducer들이 존재한다. 이 모듈들은 각각 export 되고 있다.
  • // /src/store/modules/todo.js
    const initState = {
    todoList: [
    { id: 0, text: '리액트 공부하기', done: false },
    { id: 1, text: '척추 펴기', done: false },
    { id: 2, text: '프로젝트 잘 마무리하기', done: false },
    ],
    };
    export default function todo(state = initState) {
    return state;
    }
  • 이렇게 하면 복잡한 데이터들도 처리할 수 있다. 객체 안에 객체 배열이 있는 형태이다.
  • // /src/store/modules/weight.js
    const weight = 100;
    export default function weightReducer(state = weight, action) {
    if (action.type === '증가') {
    const date = new Date().getDate();
    state = state + date;
    return state;
    } else if (action.type === '감소') {
    const month = new Date().getMonth() + 1;
    state = state - month;
    return state;
    }
    return state;
    }
  • index.js 에서는 이 Reducer들을 받아 합친다.
  • // /src/store/index.js
    import { combineReducers } from 'redux';
    import todo from './modules/todo';
    import weightReducer from './modules/weight';
    export default combineReducers({
    todo,
    weightReducer,
    });
  • 이렇게 redux에서 제공하는 combineReducers를 사용하여 여러 모듈을 하나의 store로 만들어 줄 수 있다.
  • // /src/index.js
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    import { BrowserRouter } from 'react-router-dom';
    import { Provider } from 'react-redux';
    import { createStore } from 'redux';
    import combineReducer from './store';
    const reduxDevTool =
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
    const rootReducer = createStore(combineReducer, reduxDevTool);
    // console.log(rootReducer.getState());
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
    <BrowserRouter>
    <Provider store={rootReducer}>
    <App />
    </Provider>
    </BrowserRouter>,
    );
    reportWebVitals();
  • combineReducer는 /src/index.js 에서 받아 rootReducer라는 이름으로 Store로 만들어 Provider에 전달한다.
  • 여기서 reduxDevTool은 아래 주석처리 된 콘솔로그와 같은 역할인데, 크롬에서 확장 프로그램을 설치하면 아래 이미지처럼 Redux가 관리하는 State들을 볼 수 있다.
  • 이렇게 가져온 store에서 state를 꺼내 사용하려면 useSelector를 사용하면 되지만, todo같은 경우 객체로 감싸져 있기 때문에 다음과 같이 불러와야 한다.
  • // /src/components/TodoList.jsx
    import React, { useRef } from 'react';
    import { useDispatch, useSelector } from 'react-redux';
    import { create } from '../store/modules/todo';
    export default function TodoList() {
    const todoList = useSelector((state) => state.todo.todoList);
    const dispatch = useDispatch();
    const inputRef = useRef();
    ...
    }
  • state 자체가 객체이기 때문에 위와 같이 프로퍼티에 접근하여 가져온다.
  • configureStore

  • redux에서 제공하는 createStore는 연식이 좀 된 기술이라 vscode에서도 사용하지 말라고 취소선을 그어버린다.
  • npm i @reduxjs/toolkit 으로 해당 모듈을 설치한다.
  • // index.js //
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';
    import { BrowserRouter } from 'react-router-dom';
    import { Provider } from 'react-redux';
    import rootReducer from './store';
    import { configureStore } from '@reduxjs/toolkit';
    const reduxDevTool =
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
    const store = configureStore({ reducer: rootReducer }, reduxDevTool);
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
    <BrowserRouter>
    <Provider store={store}>
    <App />
    </Provider>
    </BrowserRouter>,
    );
    reportWebVitals();
  • 이렇게 하면 위쪽 코드와 똑같이 store로 사용할 수 있다.
  • 다른점은 중괄호를 열고 reducer 키에 전달해주어야 한다는 것이다.
  • 🏀 Action & Dispatch

    State를 Store에 전달해주는 배달원

    기본 사용법

  • Store의 State 값 변경을 위해서는 Action을 설정해야 한다.
  • Action은 Dispatch를 사용하여 Reducer에 값을 전달한다.
  • 이후 Reducer가 State를 변경한다.
  • 이를 위해서 먼저 reducer함수를 변경해준다.
  • function reducer(state = weight, action) {
    if (action.type === '증가') {
    state += 1;
    return state;
    } else if (action.type === '감소') {
    state -= 1;
    return state;
    } else {
    return state;
    }
    }
  • 이를 토대로 dispatch에 action을 전달하면 된다.
  • import React from 'react';
    import { useDispatch, useSelector } from 'react-redux';
    export default function TestRedux() {
    const weight = useSelector((state) => state);
    const dispatch = useDispatch();
    return (
    <>
    <h1>당신의 몸무게는 {weight} 입니다!</h1>
    <button onClick={() => dispatch(

    Action 생성 함수

  • action 생성 함수는 type 정보와 전달해야 할 정보를 payload 객체에 담아 dispatch를 통해 전달한다.
  • 이는 각 모듈에서 행하는 작업이다. 아래는 todo 모듈에서 작성한 사항이다.
  • 우선 action 타입부터 정의한다.
  • const CREATE = 'todo/CREATE';
    const DONE = 'todo/DONE';
  • 이후 이를 이용하여 action 생성 함수를 작성한다.
  • export function create(payload) {
    return {
    type: CREATE,
    payload,
    };
    }
    export function done(id) {
    return {
    type: DONE,
    id,
    };
    }
  • 여기서 export와 export default의 차이는 다음과 같다.
  • 객체의 payload는 payload: payload와 같다. (id: id 와 같다)
  • reducer는 action 함수에 들어있는 type을 확인해서 어떤 행동을 할 지 정하고 payload에 있는 데이터를 받아 처리한다.
  • 이를 이용하여 다음과 같이 reducer를 작성하면 된다.
  • action.type을 받아 reducer로 데이터 처리

    export default function todo(state = initState, action) {
    switch (action.type) {
    case CREATE:
    return {
    ...state,
    // todoList: state.todoList.concat({
    // id: action.payload.id,
    // text: action.payload.text,
    // done: false,
    // }),
    todoList: [
    ...state.todoList,
    {
    id: action.payload.id,
    text: action.payload.text,
    done: false,
    },
    ],
    };
    case DONE:
    return console.log('DONE 호출');
    default:
    return state;
    }
    }
  • 여기서는 switch문을 쓰는 것이 관습이다.
  • 해당 코드의 주석 부분은 state를 전개연산자로 펼친 이후에 todoList 키에 있는 배열에 추가할 객체를 concat으로 추가해준다.
  • 이렇게 reducer를 작성하고 컴포넌트에서는 다음과 같이 dispatch를 사용한다.
  • Dispatch에 action 생성함수 사용

    import React, { useRef } from 'react';
    import { useDispatch, useSelector } from 'react-redux';
    import { create } from '../store/modules/todo';
    export default function TodoList() {
    const todoList = useSelector((state) => state.todo.todoList);
    const dispatch = useDispatch();
    const inputRef = useRef();
    return (
    <section>
    <h1>할 일 목록</h1>
    <div>
    <input type="text" ref={inputRef} />
    <button
    onClick={() => {
    if (inputRef.current.value === '') return;
    dispatch(
    create(
  • 작성한 create 함수는 import로 가져오고, dispatch 안에서 사용한다.
  • 여기서 주황색 배경 부분이 create 함수의 payload에 해당하는 부분이다.
  • 🏜 Redux-toolkit

  • Redux-toolkit은 기존 Redux가 스토어 구성이 복잡하고, 많은 패키지를 추가해야 하며, 많은 상용구 코드가 필요하던 것을 개선한 패키지이다.
  • npm install @reduxjs/toolkit react-redux

    store 생성

  • /store/index.ts
  • import { configureStore } from '@reduxjs/toolkit';
    import counterSlice from './counterSlice';
    const store = configureStore({
    reducer: {
    counter: counterSlice.reducer,
    },
    });
    export type RootState = ReturnType<typeof store.getState>;
    export type AppDispatch = typeof store.dispatch;
    export default store;
  • 기존 리덕스에서 combineReducer가 하던 역할이다.
  • 여기서 Slice는 store의 단위이다.
  • typescript 사용 시에는 RootState와 AppDispatch를 선언하여 사용한다.

  • /store/counterSlice.ts
  • import { PayloadAction, createSlice } from '@reduxjs/toolkit';
    type counterState = {
    value: number;
    };
    const initialState: counterState = {
    value: 0,
    };
    const counterSlice = createSlice({
    name: 'counter',
    initialState,
    reducers: {
    up: (state, action: PayloadAction<number>) => {
    state.value = state.value + action.payload;
    },
    down: (state, action: PayloadAction<number>) => {
    state.value = state.value - action.payload;
    },
    init: (state) => {
    state.value = 0;
    },
    },
    });
    export default counterSlice;
    export const { up, down, init } = counterSlice.actions;