Redux 총정리 (사용법 + 용어정리)

Hunter Joe·2024년 10월 30일
0

Redux를 꾸준히 공부하면서 사용해보려고 노력해봤는데

생각보다 쉽지 않아 이번 글을 통해서 내가 redux를 쓰기 위한 가이드라인을 작성해봄

두가지 방향으로 글을 작성해볼 예정
1. Redux만 사용
2. RTK(redux toolkit)를 사용

Installation

# use in React
npm install redux react-redux

CRA + Redux template

# Redux + Plain JS template
npx create-react-app my-app --template redux

# Redux + TS template
npx create-react-app my-app --template redux-typescript

Redux template Github


why Redux?

Redux = 전역 상태관리 라이브러리

  1. prop drilling을 방지 할 수 있다.
  2. Context APIProvider하위의 모든 컴포넌트를 re-render를 야기한다.
    반면 Redux는 상태 변경 시 관련된 컴포넌트만 선택적으로 업데이트가 가능하다.
  3. 상태가 중앙화 된 store에서 관리되어 일관성 있고 예측 가능한 상태 변경이 가능해진다.
  4. 모든 상태 변경 로직이 reducer에 의해 처리되므로 디버깅 + 테스팅에 용이하다.

Diagram

📌Store

1. CreateStore로 저장소 생성

import { createStore } from "redux";

/*
- createStore()
---------------
인자로 3가지 받을 수 있음
1. reducer(필수) : state 업데이트 로직을 정의하는 reducer 함수
2. preloadedState(선택) : 초기 상태
3. enhancer(선택) : 미들웨어 

*/
const store = createStore(); 

export default store; 

2. 한개의 Reducer 연결

import { createStore } from "redux";
import counterReducer from "../modules/counterReducer"; // import counterReducer

const store = createStore(counterReducer); 

export default store; 

3. 여러개의 Reducer를 연결

여러개의 리듀서가 생길 경우 combineReducers를 사용하면된다.

import { createStore, combineReducers } from "redux";
import counterReducer from "../modules/counterReducer"; // import counter Reducer
import userReducer from "../modules/userReducer"; // import user Reducer

/*
- combineReducers()
-------------------
리덕스는 action —> dispatch —> reducer 순으로 동작한다고 말씀드렸죠? 
이때 애플리케이션이 복잡해지게 되면 reducer 부분을 여러 개로 나눠야 하는 경우가 발생합니다. 
combineReducers은 여러 개의 독립적인 reducer의 반환 값을 하나의 상태 객체로 만들어줍니다.
*/
const rootReducer = combineReducers({
  counter : counterReducer,
  user : userReducer,
}); 

const store = createStore(rootReducer); 

export default store; 

4. Reducer 연결이 잘 됐는지 확인 하는법

import { createStore, combineReducers } from "redux";
import counterReducer from "../modules/counterReducer"; // import counter Reducer
import userReducer from "../modules/userReducer"; // import user Reducer

const rootReducer = combineReducers({
  counter : counterReducer,
  user : userReducer,
}); 

const store = createStore(rootReducer); 

console.log("STORE =>", store.getState()); 

export default store; 

console.log("STORE =>", store.getState()); 로 확인해보면

Provider 설정하기

애플리케이션에서 state를 사용할 수 있게 제공해주는 것

// index.js or main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

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

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

📌Reducer

Reducer로 사용할 것은 Counter 예제이다.

// src/redux/modules/counterReducer.js

/*
초기 값 
초기 값은 꼭 객체가 아니여도 된다.
배열,원시 데이터, 여러개의 key-value를 가진 객체도 가능
*/ 
const initialState = {
  number: 0,
};

/*
Reducer 매개변수 
---------------
1. state 
2. action
	2-1. type(필수)
    2-2. payload(선택)
*/
const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

export default counterReducer;

초기값은 꼭 객체가 아니여도 된다. 아래와 같이 다양하게 쓸 수 있음.

const initialState = 0

const initialState = [];

const initialState = {
  id: 1,
  name: "Joe",
  email: "example@gmail.com",
  password: "1234",
}

Reducer 함수는 2가지 매개변수를 갖는다.

  1. state : 현재상태를 나타내며, 함수가 처음 호출될 때는 기본값으로 초기 상태
    (initialState)를 사용. initialState는 Reducer가 관리할 상태의 초기값으로, Reducer가 첫 번째로 실행될 때 설정된다.

  2. action : Reducer에 전달되는 객체로, state를 어떻게 변경할지 정의하는 정보가 담김 action은 아래와 같이 두 가지 속성을 가진다.

    • type(필수) : 수행할 action의 유형을 나타냄. 이 값에 따라 Reducer가 어떤 상태를 변경할지 결정
    • payload(선택) 상태를 업데이트하는 데 필요한 값, 사용자의 입력필드와 같이 사용됨

📌Action

Reducer에게 state를 변경하라고 명령을 내려야하는데 그 명령을 action이라고 한다.

액션 객체는 반드시 type이라는 key를 가져야한다.
Why? → action 객체를 reducer에게 보냈을 때 reducer는 객체 안에서 type이라는 key를 확인한다.

// 액션 객체의 구조
const action ={ 
  type : "INCREMENT" // required
  payload: {... data thing} // optional
}

Redux에 있는 state를 변경하기 위해서는 그에 해당하는 action 객체를 모두 만들어줘야 한다.

아래서 Action value와 Action creator를 만들어볼텐데 애네를 쓰는 이유는 코드의 유지보수성과 가독성을 높이고, 휴먼에러를 방지하기 위해서 쓴다.

Action value

Action value는 type에 대해 어떤 행동을 수행할지 나타낼 문자열 값

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

Action creator

Action creator는 action 객체를 생성하기 위한 함수이다.

Action creator는 onClick 등 이벤트 핸들러에서 dispatch와 함께 사용되어 reducer에게 액션 객체를 전달한다.

// action value
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

/*
action creator
--------------
export 하는 이유는 React에서 onClick과 같은 이벤트 핸들러에 부착하기 위해
*/ 
export const incrementAction = () => {
  return { type: INCREMENT };
};

export const decrementAction = () => {
  return { type: DECREMENT };
};
----------------------------------------------------
// 만약 사용자 지정 값인 payload를 넣는다면
const PAYLOAD_INCREMENT = "PAYLOAD_INCREMENT";

export const payloadIncrementAction = (payload) => {
  return {
  	type: PAYLOAD_INCREMENT,
    payload: payload,
  }
};

Reducer 함수에 부착하기

// src/redux/modules/counterReducer.js

// action value
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// action creator
export const incrementAction = () => {
  return { type: INCREMENT };
};

export const decrementAction = () => {
  return { type: DECREMENT };
};


// initial state
const initialState = {
  number: 0,
};

// reducer 
const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case INCREMENT: 
      return { ...state, number: state.number + 1 }
    case DECREMENT:
      return { ...state, number: state.number - 1 }
    default:
      return state;
  }
};

export default counterReducer;

⚠️ 주의 : 불변성 ⚠️

  1. return {} : 객체 반환
  2. ...state : ...state는 새로운 객체를 만들기 위한 복사본

    만약 state{ number: 1 }이라면 ...state = { number: 1 }

  3. number : state.number + 1 : 새로운 객체에 덮어쓰기 (불변성 유지)

    ...state로 복사한 새로운 객체를 가져왔고, 그 뒤에 같은 key값을 가진 객체를 넣어 덮어쓰기해서 기존 값을 직접 변경시키는게 아닌 복사본 참조를 하게됨

Redux를 사용할때는 state의 불변성을 지켜줘야한다.!! 이는 공식문서에 적혀있는 내용
-docs

단, RTK를 쓰면 불변성 신경안써도 됨 알아서 해줌


📌useSelector & useDispatch

useSelector,useDispatch : Redux에서 제공하는 Hook

위에서store, reducer, action을 다 만들었으니 이제 React에서 사용가능하다.

useSelector(불러오기) : store에 저장된 state를 불러오는 hook
useDispatch(내보내기) : action을 디스패치하여 storestate를 변경하는 hook.

useSelector

import { useSelector } from "react-redux";
import { incrementAction } from "./redux/modules/counterReducer";

function App() {
  // counter reducer에 있는 값 가져오기
  const count = useSelector((state) => state);
  console.log("STATE =>", count); 
  
  return (
    <div>{count.counter.number}</div>
  )
}
export default App

useDispatch

import { useDispatch, useSelector } from "react-redux";
import { incrementAction, decrementAction } from "./redux/modules/counterReducer";

function App() {
  const count = useSelector((state) => state);

  console.log("STATE =>", count);

  // action 객체를 reducer한테 보내는 역할
  const dispatch = useDispatch();

  return (
    <div>
      <h1>Redux store에서 값 가져오기</h1>
      <h2>{count.counter.number}</h2>
      <button onClick={() => dispatch(incrementAction())}>+1</button>
      <button onClick={() => dispatch(decrementAction())}>-1</button>
    </div>
  );
}

export default App;

() => dispatch(incrementAction()): dispatch 함수를 사용하여 incrementAction으로 생성된 액션을 reducer에게 전달한다.

📌payload 활용한 더하기 예제

App.jsx

import { useDispatch, useSelector } from "react-redux";
import { 
  incrementAction,
  decrementAction,
  payloadIncrementAction
} from "./redux/modules/counterReducer";
import { useState } from "react";

function App() {
  const [num, setNum] = useState(0);
  const count = useSelector((state) => state);

  console.log("STATE =>", count);

  // action 객체를 reducer한테 보내는 역할
  const dispatch = useDispatch();

  return (
    <div>
      <h1>Redux store에서 값 가져오기</h1>
      <h2>{count.counter.number}</h2>
      <button onClick={() => dispatch(incrementAction())}>+1</button>
      <button onClick={() => dispatch(decrementAction())}>-1</button> <br/>

      {/*
      	* Payload 활용한 더하기
        * +붙여줘서 숫자로 형변환 
      	*/}
      <input type="number" value={num} onChange={(e) => setNum(+e.target.value)}/>
      
      <button onClick={() => dispatch(payloadIncrementAction(num))}>add num</button>
    </div>
  );
}

export default App;

counterReducer.js

// initial State
const initialState = {
  number: 0,
}

// Action Value
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT"
const PAYLOAD_INCREMENT = "PAYLOAD_INCREMENT";


// Action Creator
export const incrementAction = () => {
  return { type:INCREMENT }
};

export const decrementAction = () => {
  return { type: DECREMENT };
};

export const payloadIncrementAction = (payload) => {
  return {
  	type: PAYLOAD_INCREMENT,
    payload,
  }
};

// reducer(함수)
const counterReducer = (state = initialState, action) => {
  console.log("action", action.type)
  switch (action.type) {
    case INCREMENT :
      return { ...state, number: state.number + 1 }
    case DECREMENT:
      return { ...state, number: state.number - 1 }
    case PAYLOAD_INCREMENT:
      return { ...state, number: state.number + action.payload }
    default:
      return state;
  }
};

export default counterReducer;

RTK (Redux Toolkit)

기존 Redux를 쓰면 코드양이 많으니깐 더 추상화시켜서 간단하게 만든거라고 생각하면됨

그래도 바로 RTK부터 쓰는게 아니라 무조건 일반 Redux부터 배워야함 Redux가 어떤 로직으로 움직이는 이해하는게 중요함

📌Store

configureStore() = createStore + combineReducers

createStore, combineReducers안써도 된다.
configureStore()에 만든 reducer를 붙히면 된다.

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

import counterSlice from "../slice/counterSlice"
import userSlice from "../slice/userSlice";

const store = configureStore({
  reducer: {
    counter: counterSlice,
    user : userSlice,
  }
});

console.log("STORE =>",store.getState());
export default store;

📌createSlice

createSlice API를 사용하면 Action Value, Action Creator, Reducer를 다 따로 작성하지 않아도 된다는 장점이 있다.

//createSlice API 뼈대

const counterSlice = createSlice({
	name: '', // 모듈의 이름
	initialState : {}, // 모듈의 초기 값
	reducers : {}, // 모듈의 Reducer 로직
});

카운터 예제를 위한 counterSlice.js

// src/redux/slices/counterSlice.js

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

const counterSlice = createSlice({
  name: "counter",
  initialState: { number : 0 },
  reducers: {
    increment: (state, action) => {
	  state.number = state.number + 1;
	},
    decrement: (state, action) => {
      state.number = state.number - 1;
    }
  },
});

// Action creator는 컴포넌트에서 사용하기 위해 export 
export const { increment, decrement } = counterSlice.actions;

// reducer 는 configStore에 등록하기 위해 export default 
export default counterSlice.reducer;

카운터 예제

App.jsx

import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { decrement, increment, incrementByPayload } from "./redux/slice/counterSlice";

function App() {
  const [num, setNum] = useState(0);
  const count = useSelector((state) => state);

  console.log("STATE =>", count);

  const dispatch = useDispatch();

  return (
    <div>
      <h1>Redux store에서 값 가져오기</h1>
      <h2>{count.counter.number}</h2>
      <button onClick={() => dispatch(increment())}>+1</button>
      <button onClick={() => dispatch(decrement())}>-1</button> <br/>

      {/*
      	* Payload 활용한 더하기
        * + 붙여줘서 숫자로 형변환 
      */}
      <input type="number" value={num} onChange={(e) => setNum(+e.target.value)}/> 
      <button onClick={() => dispatch(incrementByPayload(num))}>add num</button>
    </div>
  );
}

export default App;

counterSlice.js

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

const counterSlice = createSlice({
  name: "counter",
  initialState: { number : 0 },
  reducers: {
    increment: (state, action) => {
	    state.number = state.number + 1;
	  },
    decrement: (state, action) => {
      state.number = state.number - 1;
    },
    incrementByPayload: (state, action) => {
      state.number = state.number + action.payload;
    }
    
  },
});

export const { increment, decrement, incrementByPayload } = counterSlice.actions;

export default counterSlice.reducer;
profile
두 or 다이 / FE 목표
post-custom-banner

0개의 댓글