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
why Redux?
Redux = 전역 상태관리 라이브러리
Context API
는 Provider
하위의 모든 컴포넌트를 re-render를 야기한다.store
에서 관리되어 일관성 있고 예측 가능한 상태 변경이 가능해진다. reducer
에 의해 처리되므로 디버깅 + 테스팅에 용이하다.Diagram
📌Store
CreateStore
로 저장소 생성import { createStore } from "redux";
/*
- createStore()
---------------
인자로 3가지 받을 수 있음
1. reducer(필수) : state 업데이트 로직을 정의하는 reducer 함수
2. preloadedState(선택) : 초기 상태
3. enhancer(선택) : 미들웨어
*/
const store = createStore();
export default store;
Reducer
연결import { createStore } from "redux";
import counterReducer from "../modules/counterReducer"; // import counterReducer
const store = createStore(counterReducer);
export default store;
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;
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가지 매개변수를 갖는다.
state
: 현재상태를 나타내며, 함수가 처음 호출될 때는 기본값으로 초기 상태
(initialState
)를 사용. initialState
는 Reducer가 관리할 상태의 초기값으로, Reducer가 첫 번째로 실행될 때 설정된다.
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 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;
return {}
: 객체 반환 ...state
: ...state
는 새로운 객체를 만들기 위한 복사본만약
state
가{ number: 1 }
이라면...state
={ number: 1 }
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
을 디스패치하여 store
의 state
를 변경하는 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
활용한 더하기 예제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;
// 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;