[React] Redux (+Redux Toolkit)
03_Redux
3-1. What is Redux?
- 크로스 컴포넌트 또는 앱 와이드 상태를 위한 상태 관리 시스템
상태의 종류
Local State
데이터가 변경되어 하나의 컴포넌트에 속하는 UI에 영향을 미치는 상태
ex) 토글버튼, 입력필드 ..
useState()
,useReducer()
로 관리
Cross-Component State ->
Redux 로 관리
다수의 컴포넌트에 영향을 주는 상태
ex) 모달 컴포넌트
prop chains
,prop drilling
으로 관리
App-Wide State ->
Redux 로 관리
애플리케이션의 모든 컴포넌트에 영향을 주는 상태
ex) 사용자 인증 상태
prop chains
,prop drilling
으로 관리
3-2. Why use Redux?
기존
React Context
의 단점을 보완하기 위해React Context
의 단점?설정과 관리의 복잡성
- 프로젝트의 규모가 커질 수록 심하게 중첩된 JSX문을 사용해야 할 수 있음
성능
- 고빈도 상태변경에 대해 최적화 되어있지 않음
3-3.Redux Basic
Redux 핵심기능
Centeral Data(State) Store (중앙 저장소)
가 존재하여, 어플리케이션 전체에서 해당 중앙 저장소에 있는 데이터를 공유해서 사용할 수 있다.리덕스에서는 한 애플리케이션당 하나의 스토어를 만든다.
Flow
Central Data Store
를Components
는 Subscription 하여 변경점이 있을 경우 알림을 준다.(데이터 상태 변경을 원할 시)
Action
을dispatch
하여 trigger하고, 이때 변경 명령을 준다해당 명령에 따라
Action
은Reducer Function
에 이를 전달하고, 여기서 변경을 처리한다변경된 데이터 상태는 다시
Central Data Store
에 반영된다.Central Data Store
에 반영된 변경된 데이터는 상태가 변경된 것을 구독한Component
에 알림을 주고, 변경사항을 적용받고 그것이 UI에 적용된다.
시작하기
터미널에 아래 명령문 작성 ->
package.json
파일 생성npm init -y
터미널에 아래 명령문을 작성하여, redux 설치
npm install redux
기본 구조 생성
아래의 코드는
counter
객체가 1씩 plus되는 기능을 간단히 구현// 0. node.js 문법으로 redux 라이브러리 호출 const redux = require("redux"); // 2. define reducer function // -> input : (기존 상태={객체의 초기값 설정}, 디스패치된 액션) // -> output : 새로운 상태의 객체 const counterReducer = (state = { counter: 0 }, action) => { // action.type에 따른 동작 분기처리 switch (action.type) { case 'increment': return { counter: state.counter + 1 }; case 'decrement': return { counter: state.counter - 1 }; default: return state; } }; // 1. create central data store const store = redux.createStore(counterReducer); // 3. create subscriber // -> 저장소의 상태가 업데이트 될 때마다 트리거된다 // -> 트리거되면 `getState()` 로 인해 최신 상태를 전송받음 const counterSubscriber = () => { const latestState = store.getState(); console.log(latestState); }; // subscriber 함수를 실행하라는 명령 store.subscribe(counterSubscriber); // action을 dispatch(발송)하는 메서드 // action : javascript object (action type 설정) store.dispatch({type:'increment'}); // { counter: 1 } store.dispatch({type:'increment'}); // { counter: 2 } store.dispatch({type:'increment'}); // { counter: 3 } store.dispatch({type:'decrement'}); // { counter: 2 } store.dispatch({type:''}); // { counter: 2 }
3-4.Using Redux with React
package 설치
npm install redux eate-redux
store 패키지에
index.js
작성import { createStore } from "redux"; // reducer function const counterReducer = (state = { counter: 0 }, action) => { switch (action.type) { case "increment": return { counter: state.counter + 1, }; case "decrement": return { counter: state.counter - 1, }; default: return state; } }; // create store const store = createStore(counterReducer); export default store;
- dispatch나 action은 쓰지 않음 => 개별 컴퍼넌트에서 선언
최상위 단에 있는
index.js
에 store를 제공받도록 설정import React from "react"; import ReactDOM from "react-dom/client"; import { Provider } from "react-redux"; import "./index.css"; import App from "./App"; import store from "./store/index"; // store가 정의되어 있는 위치 const root = ReactDOM.createRoot(document.getElementById("root")); root.render( // 어플리케이션 전역에서 store 공유 가능하도록 아래와 같이 감싸주기 <Provider store={store}> <App /> </Provider> );
내부 컴포넌트에서
useSelector
를 이용해 데이터에 접근가능하도록 설정action
을dispatch
하기
import { useSelector, useDispatch } from "react-redux"; import classes from "./Counter.module.css"; const Counter = () => { // useDispatch // -> redux store에 대한 action을 전송 const dispatch = useDispatch(); const incrementHandler = () => { dispatch({ type : 'increment' }); // 새로운 action을 보내기 위해 실행 }; const decrementHandler = () => { dispatch({ type : 'decrement' }); // 새로운 action을 보내기 위해 실행 }; // useSelector // -> 저장소가 관리하는 데이터에 접근 // -> 함수는 react-redux 가 실행 // (useSelect를 정의한 컴포넌트에 자동으로 subscribe를 설정함) // userSelector(관리된 데이터 => 접근하고자 하는 상태의 부분) const counter = useSelector(state => state.counter); const toggleCounterHandler = () => {}; return ( <main className={classes.counter}> <h1>Redux Counter</h1> <div className={classes.value}>{counter}</div> <div> <button onClick={incrementHandler}>Increment</button> <button onClick={decrementHandler}>Decrement</button> </div> <button onClick={toggleCounterHandler}>Toggle Counter</button> </main> ); }; export default Counter;
UI 실행 화면
5-1.
Increment
버튼 클릭
5-2.
{counter}
값 +1
5-3. `Decrement` 버튼 클릭
5-4. `{counter}` 값 -1
3-5. Redux Toolkit
시작하기
terminal에 아래의 명령문 수행
npm install @reduxjs/toolkit
package.json
에서redux
설정 부분 삭제store.js 에 아래와 같이 import
import { createSlice } from "@reduxjs/toolkit";
코드에 적용해보자
기존 react-redux로 reducer funcion을 정의한 코드
- store/index.js
const initialState = { counter: 0, showCounter: true }; const counterReducer = (state = { ...initialState }, action) => { switch (action.type) { case INCREMENT: return { ...state, counter: state.counter + 1, }; case INCREASE: return { ...state, counter: state.counter + action.amount, }; case DECREMENT: return { ...state, counter: state.counter - 1, }; case TOGGLE: return { ...state, showCounter: !state.showCounter, }; default: return { ...state }; } }; // create store const store = createStore(counterReducer);
- component/Counter.js
// useDispatch // -> redux store에 대한 action을 전송 const dispatch = useDispatch(); // useSelector // -> 저장소가 관리하는 데이터에 접근 // -> 함수는 react-redux 가 실행 // (useSelect를 정의한 컴포넌트에 자동으로 subscribe를 설정함) // userSelector(관리된 데이터 => 접근하고자 하는 상태의 부분) const counter = useSelector((state) => state.counter); const show = useSelector((state) => state.showCounter); const incrementHandler = () => { dispatch({ type: INCREMENT }); // 새로운 action을 보내기 위해 실행 }; const IncreaseHandler = () => { dispatch({ type: INCREASE, amount: 10 }); }; const decrementHandler = () => { dispatch({ type: DECREMENT }); }; const toggleCounterHandler = () => { dispatch({ type: TOGGLE }); };
같은 코드를 redux-toolkit의
createSlice
를 이용하여 정의- store/index.js
const initialState = { counter: 0, showCounter: true }; // call createSlice // createSlice : redux toolkit 내부의 immer에 의해 기존의 상태를 변경시키지 않고 원래 상태를 복제하여 새로운 상태 객체를 생성한다. const counterSlice = createSlice({ name : 'counter', initialState, // 초기상태 설정 (별도 지정하지 않으면 initialState의 값을 이용) reducers : { increment(state) { state.counter++; }, decrement(state) { state.counter--; }, increase(state, action) { state.counter = state.counter + action.payload; }, toggleCount(state) { state.showCounter = !state.showCounter; } } }); // create store // configureStore : createStore처럼 store생성, 단 여러 개의 리듀서를 하나의 리듀서로 합칠 수 있음(리듀서 병합) const store = configureStore({ reducer: counterSlice.reducer, // 전역 상태를 담당하는 주요 리듀서로 지정 // 여러 개의 리듀서를 쓰는 경우 아래와 같이 쓸 수도 있음 // reducer: { counter: counterSlice.reducer, select: selectSlice.reducer } }); // action생성자: action 객체 생성 export const counterActions = counterSlice.actions;
- component/Counter.js
// useDispatch // -> redux store에 대한 action을 전송 const dispatch = useDispatch(); // useSelector // -> 저장소가 관리하는 데이터에 접근 // -> 함수는 react-redux 가 실행 // (useSelect를 정의한 컴포넌트에 자동으로 subscribe를 설정함) // userSelector(관리된 데이터 => 접근하고자 하는 상태의 부분) const counter = useSelector((state) => state.counter); const show = useSelector((state) => state.showCounter); const incrementHandler = () => { dispatch(counterActions.increment()); // 새로운 action을 보내기 위해 실행 }; const IncreaseHandler = () => { dispatch(counterActions.increase(10)); // {type : SOME_UNIQUE_IDENTIFIER, payload : 10} }; const decrementHandler = () => { dispatch(counterActions.decrement()); }; const toggleCounterHandler = () => { dispatch(counterActions.toggleCount()); };