React Redux
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;