React

React Redux

sognociel 2022. 12. 16. 19:45

Redux | createStore, subscribe, getState, dispatch
React Redux | connect, useSelector, useDispatch
Redux toolkit | configureStore, createSlice, createThunk

 

 

Context
프로젝트가 커지면 커질수록 설정이 복잡해져 관리 또한 복잡해질 가능성이 있다. (Context를 여러 번 중첩해서 사용해야 할 가능성 높아짐)

 

 

Redux

유동적인 상태 관리 라이브러리로 중앙 저장소 store(딱 하나만 만들 수 있음)를 사용하여 전체 애플리케이션의 모든 상태를 저장한다. (인증 상태, 테마 등등)

관리가 어렵지 않나? -> 직접 관리할 필요가 없다

컴포넌트가 저장소를 구독(subscribe) 하고, 데이터가 변경될 때마다 저장소가 컴포넌트에 데이터를 알려준다.

리덕스 저장소의 일부를 받게 되는 것 + 해당 데이터 사용 가능

 

데이터를 변경하는 방법은?

컴포넌트가 저장소의 데이터를 저장하지 않는다. reducer를 이용하여 데이터 변경(조작)을 하며 컴포넌트에서 액션(+변경에 필요한 데이터) 발송, 액션들이 리듀서로 전달되면 일치하는 행동을 수행하며 데이터의 변경이 이루어진다.

※ redux에서는 절대 기존의 state를 변경하면 안 된다. 새로운 데이터로 반환을 해야 함 (ex. push() 같은 조작은 기존의 데이터를 변경하는 방법이기 때문에 사용 x)

 

Redux는 React에서만 쓸 수 있는 게 아니라 JavaScript 등 다른 언어와 같이 쓸 수 있다.

 

redux 설치 방법

$ npm i redux
$ npm i react-redux

store를 만들 때, createStore가 redux 안에 포함되어 있기 때문에 React에서 Redux를 사용한다 하더라도 redux를 같이 설치해 주어야 한다.

 

 

 

바닐라 자바스크립트에서의 Redux

...간단하게.....,.,.,,,

store.getState() | 현재의 상태를 반환해준다.

store.subscribe( 함수 ) | state가 변경된 값을 렌더링 한다.

const store = createStore(reducer);

const onChange = () => {
  counter.innerText = store.getState();
}

store.subscribe(onChange);

 

 

 

React에서의 Redux

store과 reducer

import { createStore } from "redux";

const reducer = (state = [], action) => {
  switch (action.type) {
    case "ADD": {
      const newTodo = { id: Date.now(), text: action.data };
      return [newTodo, ...state];
    }
    case "DELETE": {
      return state.filter((item) => item.id !== action.id);
    }
    default:
      return state;
  }
};

const store = createStore(reducer);

export default store;

컴포넌트 파일이 아닌 일반 js 파일에 만들면 되며, 만들어진 store를 index.js에서 import 받아서 사용한다. store를 전해주기 위해서는 index.js에서 Provider가 필요하다.

useContext랑 useReducer를 합친 느낌...

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

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

 

state에 접근하기 위해서는 store로부터 state를 가져와야 하는데, state를 가져오거나 dispatch를 가져오는 두 가지의 argument가 있다. 간단하게 정의해 보자면 store.getState()와 store.dispatch() 인데... 사용하는 방법이 따로 있다.

// state를 가져오기
mapStateToProps(state, ownProps?) {}

// dispatch를 가져오기
mapDispatchToProps(dispatch, ownProps?) {}

이 함수들은 connet와 반드시! 같이! 써야 한다.

※ 함수형 컴포넌트에서 connect를 쓰는 건 비추... useSelector, useDispatch를 쓰자...ㅠ

connect는 클래스형 컴포넌트에서...

connect(mapStateToProps, mapDispatchToProps)(Component)

 

connect()는 컴포넌트로 보내는 props에 정의한 내용들을 보내주는 역할을 하며(그냥... props로 생각하면 될 듯...) 컴포넌트를 export 하는 부분에서 사용된다.

더보기

connect 사용 예시 (Todo 리스트)

 

Home.js

import React, { useState } from "react";
import { connect } from "react-redux";
import Todo from "../components/Todo";

const Home = ({ todos, addToDo }) => {
  const [text, setText] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    addToDo(text);
    setText("");
  };

  return (
    <>
      <h1>Todo</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
        <button onClick={handleSubmit}>Add</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <Todo todo={todo} key={todo.id} />
        ))}
      </ul>
    </>
  );
};

function mapStateToProps(state) {
  return { todos: state };
}

function mapDispatchToProps(dispatch) {
  return {
    addToDo: (text) => dispatch({ type: "ADD", data: text }),
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(Home);

 

 Todo.js

import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";

const Todo = ({ todo, deleteTodo }) => {
  return (
    <li>
      <Link to={`/${todo.id}`}>{todo.text} </Link>
      <button onClick={() => deleteTodo(todo.id)}>DELETE</button>
    </li>
  );
};

function mapDispatchToProps(dispatch, ownProps) {
  return {
    deleteTodo: () => dispatch({ type: "DELETE", id: ownProps.todo.id }),
  };
}

export default connect(null, mapDispatchToProps)(Todo);

 

 

useSelector, useDispatch

reducer는 기존에 작성하던 대로 작성해준다.

import { createStore } from "redux";

const initialState = {
  counter: 0,
  showCounter: true,
};

const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case "INCREMENT": {
      return { ...state, counter: state.counter + 1 };
    }
    case "DECREMENT": {
      return { ...state, counter: state.counter - 1 };
    }
    case "INCREASE": {
      return { ...state, counter: state.counter + action.amount };
    }
    case "TOGGLE": {
      return { ...state, showCounter: !state.showCounter };
    }
    default: {
      return state;
    }
  }
};

const store = createStore(counterReducer);

 

store에서 값을 가져와서 사용하기 위해서는 useSelector가, action을 보내기 위해서는 useDispatch가 필요하다.

// 컴포넌트 안에 useSelector, useDispatch 생성
const counter = useSelector((state) => state.counter);
const dispatch = useDispatch();

action 보내기

<button onClick={() => dispatch({ type: "INCREMENT" })}> Increment </button>
<button onClick={() => dispatch({ type: "INCREASE", amount: 5 })}> Increase </button>

 

 

 

Redux Toolkit

Redux toolkit이 나타난 배경
설정할게 너무 많음
미들웨어 설치 필요
반복되는 코드 (action을 dispatch 할 때)
불변성 유지의 어려움

 

적은 양의 Redux 코드를 작성할 수 있도록 도와주는 것

$ npm i @reduxjs/toolkit

redux toolkit 안에 redux가 포함되어 있기 때문에 이전에 설치한 redux를 삭제해 주면 된다.

 

 

configureStore
state에 어떤 일이 생겼는지 바로 볼 수 있다. 어떤 action이 발생하고, 언제 발생했는지.
구글 확장 프로그램 Redux Developer Tools를 설치해서 확인 가능

 

createSlice
reducer뿐만 아니라 action도 생성해 준다.

reducer에서 switch-case로 작성했던 action들을 함수로 정의한다.

기존에 redux를 사용할 때는 상태와 관련하여 불변성 신경 써야 했는데, redux toolkit은 immer라는 패키지를 사용해서 자동으로 원래 상태를 복제해 새로운 상태 객체를 생성한 후 동작을 실행한다. 내부적으로 알아서 실행하는 것.

 

 

createSlice와 configureStore

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

// 초기 상태 설정
const initialState = {
  counter: 0,
  showCounter: true,
};

// Slice를 사용하여 reducer 생성. 아래 코드는 단일 슬라이스에 대한 예시
const counterSlice = createSlice({
  name: "counter",
  initialState: initialState,
  reducers: {
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    // action에서 추가로 data를 받는 것들은 자동으로 payload라는 필드에 들어간다.
    increase(state, action) {
      state.counter = state.counter + action.payload;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

// action들을 모은 객체를 따로 변수로 저장
export const counterActions = counterSlice.actions;

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

export default store;

 

작성한 slice를 사용하기

// action들을 모은 객체를 import 해준다
import { counterActions } from "../store/index";

// counterActions에서 increase 함수에 인자 5를 전달.
// 이 값은 payload 필드에 들어가게 된다.
<button onClick={() => dispatch(counterActions.increase(5))}>Increse</button>

// 따로 data 값을 전달하지 않아도 된다면 아래와 같이 사용
<button onClick={() => dispatch(counterActions.increment())}>Increment</button>

 

 

다중 슬라이스 작업

counter 말고 auth라는 새로운 상태를 생성했을 때, store는 하나인데 어떻게 여러 가지의 상태를 관리하나? ->다중 슬라이스 작업을 진행

먼저 새로운 상태인 auth를 추가해준다.

const initialAuthState = {
  isAuth: false,
};
const authSlice = createSlice({
  name: "auth",
  initialState: initialAuthState,
  reducers: {
    login(state) {
      state.isAuth = true;
    },
    logout(state) {
      state.isAuth = false;
    },
  },
});

 

그리고 store의 reducer를 객체 형태로 수정하여 reducer를 추가해준다.

const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    auth: authSlice.reducer,
  },
});

export const counterActions = counterSlice.actions;
export const authActions = authSlice.actions;

 

다중 슬라이스 작업 완료 후, state의 값을 가져오는 컴포넌트에서 값을 수정해주어야 한다. state.리듀서에서 할당했던 식별자(여기서는 auth와 counter).키값

const isAuth = useSelector((state) => state.auth.isAuth);
const counter = useSelector((state) => state.counter.counter);

 

 

 

파일 구조 나누기

여러가지의 상태를 하나의 파일에서 관리하면 코드가 길어지기 때문에 상태별로 구조를 나누는 방법도 있다.

index.js에서는 store만 만들고, 다른 파일에서 reducer와 action을 export해서 사용하는 방법

// index.js
import { configureStore } from "@reduxjs/toolkit";
import counterSlice from "./counter";
import authSlice from "./auth";

const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    auth: authSlice.reducer,
  },
});

export default store;

// counter.js
...생략 (initialState와 Slice)
export const counterActions = counterSlice.actions;
export default counterSlice;

// auth.js
...생략 (initialState와 Slice)
export const authActions = authSlice.actions;
export default authSlice;