What is "Redux" ?
๐ป A state management system for cross-component or app-wide state.
โ๏ธ state๋ฅผ 3์ข ๋ฅ๋ก ํฌ๊ฒ ๋๋ ์ ์๋ค.
1. Local state : ํ๋์ ์ปดํฌ๋ํธ์ ์ข ์๋๋ state
2. Cross-Component state : ํ๋ ์ด์์ ์ปดํฌ๋ํธ์ ์ํฅ์ ๋ฏธ์น๋ state(prop drilling)
3. App-Wide state : app์ ์ฒด์ ๊ธฐ๋ณธ์ ์ธ ๋ชจ๋ ์ปดํฌ๋ํธ์ ์ํฅ์ ๋ฏธ์น๋ state
๐คท๐ปโ๏ธ React Context๋ ์๋๋ฐ ์ Redux? context API์ ๋จ์
> ๋๊ท๋ชจ์ app์์ ์ state๊ด๋ฆฌ๋ฅผ ์ํด
: ์๋ง์ context provider ์ปดํฌ๋ํธ๊ฐ ์ค์ฒฉ๋์ด ๋ณต์กํด์ง๋ค.
: provider๊ฐ์๋ฅผ ์ค์ด๊ณ ํ๋์ ๊ฑฐ๋ํ context provider๋ก๋ ๊ด๋ฆฌ๊ฐ ๋ถ๊ฐ๋ฅ
> state ๋ณ๋ ํ์๊ฐ ๋์ ๊ฒฝ์ฐ์ ์ฑ๋ฅ๋ฌธ์ ๊ฐ ์๋ค.
Redux ๊ธฐ๋ณธ ์๋ ๋ฐฉ์
Redux๋ app์ ํ๋์ ์ค์ ๋ฐ์ดํฐ(state) ์ ์ฅ์๋ฅผ ๊ฐ์ง๋ค. ( app์ ์ฒด๋ฅผ ์ํ ํ๋์ store! )
์ ์ญ์ํ๋ฅผ ํ๋์ ์ ์ฅ์์ ์ ์ฅํ๋ฉฐ, action(์ด๋ค ์ํ๋ณ๊ฒฝ์ด ์๊ธธ์ง ์์ ํ๋ ๊ฐ์ฒด)์ ๋ด๋ณด๋ด์(dispatch) ์ํ๋ณ๊ฒฝ์ ํ๋ค.
๊ทธ๋ฆฌ๊ณ action์ด ์ ์ฒด application์ ์ํ๋ฅผ ์ด๋ป๊ฒ ๋ณ๊ฒฝํ ์ง ๋ช ์ํ๊ธฐ ์ํด์ reducer์ ์์ฑ์ด ํ์ํ๋ค.
๐ป Redux ํ๋ฆ
1. component๊ฐ ํน์ action์ trigger ( action์ ๋จ์ ์๋ฐ์คํฌ๋ฆฝํธ ๊ฐ์ฒด - reducer๊ฐ ์ํํ๋ ์์ ์ ์ข ๋ฅ๋ฅผ ์ค๋ช )
2. actions์ reducer๋ก ์ ๋ฌํ์ฌ ์ํ๋ ์์ ํธ์ถ
3. reducer๋ ์ค์๋ฐ์ดํฐ store์ ์กด์ฌํ๋ ๋ฐ์ดํฐ๋ฅผ ๊ต์ฒดํ๋ ์๋ก์ด state๋ฅผ ๋ณด๋
4. ๋ฐ์ดํฐ store์ ์๋ state๊ฐ update
5. subscribeํ๋ component์ ์๋ ค์ ธ UI update
โ๏ธ Reducer function
ํญ์ ๋๊ฐ์ ๋งค๊ฐ๋ณ์ (๊ธฐ์กด์ํ์ dispatched action)๋ฅผ ์ ๋ ฅ๋ฐ์์ ์๋ก์ด ์ํ๋ฅผ ๋ฆฌํดํด์ผํ๋ค.
๋ฆฌ๋์ ํจ์๋ ์์ํจ์์ด๋ค. (์ ๋ ฅ๊ณผ value๊ฐ ๊ฐ๋ค๋ฉด ํญ์ ๊ฐ์ ์ถ๋ ฅ์ ์์ฐํ๋ค.)
const redux = require("redux");
//reducer function
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
};
}
if (action.type === "decrement") {
return {
counter: state.counter - 1,
};
}
};
//central Data Store
const store = redux.createStore(counterReducer);
//Subscription
const counterSubscriber = () => {
const latestSate = store.getState();
console.log(latestSate);
};
store.subscribe(counterSubscriber);
store.dispatch({ type: "increment" });
store.dispatch({ type: "decrement" });
//์ถ๋ ฅ
{ counter : 1 }
{ counter : 0 }
Redux ์ฌ์ฉ
1. store์์ฑ
store / index.js
import { createStore } from "redux";
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
};
}
if (action.type === "decrement") {
return {
counter: state.counter - 1,
};
}
return state;
};
const store = createStore(counterReducer);
export default store;
2. ๊ฐ์ฅ ๋์ component ๋ ๋ฒจ์์ store ์ ๊ณต
provider๋ก ๊ฐ์ธ์ฃผ๊ณ ์์์ store๋ฅผ props๋ก ๋๊ฒจ์ค๋ค.
Index.js
//import React from "react";//
//import ReactDOM from "react-dom";
import { Provider } from "react-redux";
//import App from "./App";
import store from "./store/index";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
3. react component์์ redux data ์ฌ์ฉ
useSelector hook์ผ๋ก store์์ state๊ฐ์ ธ์ค๊ณ ,
useDispatch hook์ผ๋ก redux store์ ๋ํ action์ ๋ณด๋ธ๋ค.
import { useSelector, useDispatch } from "react-redux";
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter);
const incerementHandler = () => {
dispatch({ type: "increment" });
};
const decerementHandler = () => {
dispatch({ type: "decrement" });
};
return (
<main className={classes.counter}>
...
<div>
<button onClick={incerementHandler}>Increment</button>
<button onClick={decerementHandler}>Decrement</button>
</div>
...
</main>
);
};
export default Counter;
๐ payload
action์ ์์ฑ ์ถ๊ฐํ๊ธฐ
(reducer์์ ํ๋์ฝ๋ฉ ํ์ง์๊ณ action์ผ๋ก๋ถํฐ ๊ฐ์ ์ป์ด์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊พผ๋ค.)
if (action.type === "increase") {
return {
counter: state.counter + action.amount,
};
}
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter);
const increseHandler = () => {
dispatch({ type: "increase", amount: 10 });
};
return (
<main className={classes.counter}>
...
<button onClick={increseHandler}>Increse</button>
...
</main>
);
๐ฅ ์ฌ๋ฌ state ์์ฑ ์์
: Reducer์์ state ์ถ๊ฐ
> store์์ ๊ธฐ์กด counter์์ showCounter์ถ๊ฐ
//store-index.js
const initialState = { counter: 0, showCounter: true };
const counterReducer = (state = initialState, action) => {
if (action.type === "toggle") {
return {
showCounter: !state.showCounter,
counter: state.counter,
};
}
...
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter);
const show = useSelector((state) => state.showCounter);
const toggleCounterHandler = () => {
dispatch({ type: "toggle" });
};
return (
<main className={classes.counter}>
{show && <div className={classes.value}>{counter}</div>}
...
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
Redux toolkit
๐ป redux๋ฅผ ์ข ๋ ์ฝ๊ฒ ์ฌ์ฉํ๊ธฐ ์ํ์ฌ ๋์จ ๋๊ตฌ๋ชจ์!
redux๋ก์ง์ ์์ฑํ๋ ํ์ค ๋ฐฉ์์ด ๋๊ธฐ ์ํ ์๋๋ก ๋ง๋ค์ด ์ก๋ค.
ํต์ฌ์ ๊ธฐ์กด ๋ฆฌ๋์ค์ ๋ณต์กํจ์ ๋ฎ์ถ๊ณ ์ฌ์ฉ์ฑ์ ๋์ด๋ ๊ฒ์ด๋ค.
๋ฆฌ๋์ค์ ๋ํ์ ์ธ ๋ฌธ์
- store ํ๊ฒฝ์ค์ ์ด ๋ณต์กํ๋ค.
- ์ ์ฉํ๊ฒ ์ฌ์ฉํ๋ ค๋ฉด ๋ง์ ํจํค์ง๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค.
- ๋ณด์ผ๋ฌํ๋ ์ดํธ (์ด๋ค ์ผ์ ํ๊ธฐ ์ํด ๊ผญ ์์ฑํด์ผ ํ๋ ์ฝ๋)๋ฅผ ๋ง์ด ์๊ตฌํ๋ค.
โ๏ธ toolkit์ redux๊ฐ ์ด๋ฏธ ํฌํจ๋์ด์์ด์, toolkt์ ์ฌ์ฉํ๋ ค๋ฉด ๊ธฐ์กด์ ์ค์น๋ redux๋ฅผ ์ญ์ ํด์ค๋ค.
npm i @reduxjs/toolkit
Redux toolkit ์ฌ์ฉ
1. createSlice
๋ฆฌ๋์ค ๋ก์ง ์์ฑ ์ ducks pattern ํํ๋ก ์์ฑ์ ๋๋ createSlice๋ฅผ ์ฌ์ฉํ๋ค.
ํน์ง
- ๊ฐ์ฒด๋ฅผ ์ธ์๋ก์ ์์ฑ.
- ์ ์ง๋ณด์ํ๊ธฐ ์ฌ์.
- ๋ฐ๋ก ์ํ๋ฅผ ๋ณต์ฌํ์ง์๊ณ ๋ฐ๋ก ๋ณ๊ฒฝ๊ฐ๋ฅ
(toolkit๋ ๋ด๋ถ์ ์ผ๋ก immer๋ผ๋ ํจํค์ง๋ฅผ ์ฌ์ฉํ์ฌ ์ฝ๋๋ฅผ ๊ฐ์งํ๊ณ ์๋์ผ๋ก ์๋ ์๋ ์ํ๋ฅผ ๋ณต์ )
- ์ํ์ํจ๊ป action๋ ๋ฐ์ (action.payload)
import { createSlice } from "@reduxjs/toolkit";
const initialCounterState = { counter: 0, showCounter: true };
const counterSlice = createSlice({
name: "counter", //๋ชจ๋ slice๋ name๊ฐ์ ธ์ผํจ(์ํ์๋ณ์)
initialState: initialCounterState, //์ด๊ธฐ๊ฐ์ค์
reducers: {
//๊ธฐ์กด์ํ๋ฅผ ๋ฐ๊พธ๋ ๊ฒ์ด์๋, ์๋์ผ๋ก ๊ธฐ์กด์ ์ํ๋ฅผ ๋ณต์ ํด์ ๋ณํ์ํด (๋ถ๋ณ์ฑ ์ ๊ฒฝ์ธ ํ์์์ด์ง)
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter = state.counter + action.payload;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
2. configureStore
: ์คํ ์ด๋ฅผ ๊ตฌ์ฑํ๋ ํจ์์ด๋ฉฐ, ์ฌ๋ฌ๊ฐ์ reducer๋ฅผ ํ๋์ reducer๋ก ์ฝ๊ฒ ํฉ์น ์ ์๋ค.
์ฌ๋ฌ slice๋ฅผ ๋ด๋ณด๋ผ ๋๋ ํ๋์ ๊ฐ์ฒด๋ก ๋ฌถ์ด์ key๊ฐ์ ์ค์ ํ์ฌ ๋ณด๋ธ๋ค.
(๊ธฐ์กด reduxdml combineReducers ํจ์์ ๊ฐ์์ญํ )
๋ฆฌ๋์ค ์ฝ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํ์ค ํจ์์ธ createStore๋ฅผ ์ถ์ํํ ๊ฒ์ด๋ค.
(๊ธฐ์กด ๋ฆฌ๋์ค์ ๋ฒ๊ฑฐ๋ก์ด ๊ธฐ๋ณธ ์ค์ ๊ณผ์ ์ ์๋ํ)
import { createSlice, configureStore } from "@reduxjs/toolkit";
const counterSlice = createSlice({
...
)}
const store = configureStore({
reducer: counterSlice.reducer,
});
export const counterActions = counterSlice.actions;
slicename.actions.keyname = actions ์์ฑ์
( ex. counterSlice.actions.increment > ์ก์ ์ ๋ฌ )
3. state๋ฅผ ์ฌ์ฉํ component์์ import
import { counterActions } from "../store/index";
import { useSelector, useDispatch } from "react-redux";
import { counterActions } from "../store/index";
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter);
const show = useSelector((state) => state.showCounter);
//store์์ key๊ฐ ์ค์ ์ ํ์ง์์๋ค๋ฉด state.counter
//key๊ฐ ์ค์ ์ ํ์ผ๋ฉด key๊ฐ์ ์ถ๊ฐํด์ state.key.counter
const incerementHandler = () => {
dispatch(counterActions.increment());
};
const decerementHandler = () => {
dispatch(counterActions.decrement());
};
const increseHandler = () => {
//{type:SOME_UNIQUE+IDENTIFIER, payload : 10}
//payload๋ toolkit๊ฐ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ฌ์ฉํ๋ ํ๋๋ช
dispatch(counterActions.increase(10));
};
const toggleCounterHandler = () => {
dispatch(counterActions.toggleCounter());
};
payload๊ฐ ํ์ํ ์ ๊ฐ์ ๊ดํธ์ ์ ๋ฌํด ์ฃผ๊ณ , reducer์์ action.payload๋ก ์ ๊ทผํ๋ค.
( toolkit๋ ์๋์ผ๋ก action ์์ฑ์๋ฅผ ์์ฑํด์ redux toolkit๊ฐ ์์ฑํ type: SOME_UNIQUE_IDENTIFIER์ ์ ๋ฌํ๊ณ ,
์ธ์๋ก์ ์คํํ๊ณ ์ ํ๋ ์ก์ ๋ฉ์๋์ ์ ๋ฌํ ๊ฐ์ ์ถ๊ฐ ํ๋๋ช ์ด payload์ธ ๊ณณ์ ์ ์ฅํ๋ค.)
4. ์ฌ๋ฌ slice๊ด๋ฆฌ
์ ๊ณผ์ ๊ณผ ๋์ผํ๊ฒ slice๋ฅผ ์ถ๊ฐํ๊ณ store์ ์ถ๊ฐ๋ง ํด์ฃผ๋ฉด ๋๋ค.
( slice๊ฐ ์ฌ๋ฌ๊ฐ๋ผ๋ redux store๋ ํ๋๋ฐ์ด์ด์ configureStore๋ฅผ ํ๋ฒ๋ง ํธ์ถํด์ผํ๋ค! )
const AuthSlice = createSlice({
name: "authentication",
initialState: { isAuthenticated: false },
reducers: {
login(state) {
state.isAuthenticated = true;
},
logout(state) {
state.isAuthenticated = false;
},
},
});
const store = configureStore({
reducer: { counter: counterSlice.reducer, auth: AuthSlice.reducer },
});
export const authActions = authSlice.actions;
//store/index.js
// import { createStore } from "redux";
//slice๋ createReducer๋ณด๋ค ๊ฐ๋ ฅํ๊ณ ํ๋ฒ์ ๋ช๊ฐ์ง๋ฅผ ๋จ์ํํ๋ค.
import { createSlice, configureStore } from "@reduxjs/toolkit";
const initialCounterState = { counter: 0, showCounter: true };
const counterSlice = createSlice({
//์ ์ญ์ํ์ slice๋ฅผ ๋ฏธ๋ฆฌ๋ง๋ ๋ค
name: "counter",
initialState: initialCounterState, //์ด๊ธฐ๊ฐ์ค์
reducers: {
//๊ธฐ์กด์ํ๋ฅผ ๋ฐ๊พธ๋ ๊ฒ์ด์๋, ์๋์ผ๋ก ๊ธฐ์กด์ ์ํ๋ฅผ ๋ณต์ ํด์ ๋ณํ์ํด (๋ถ๋ณ์ฑ ์ ๊ฒฝ์ธ ํ์์์ด์ง)
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter = state.counter + action.payload;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
const authSlice = createSlice({
name: "authentication",
initialState: { isAuthenticated: false },
reducers: {
login(state) {
state.isAuthenticated = true;
},
logout(state) {
state.isAuthenticated = false;
},
},
});
// const counterReducer = (state = initialState, action) => {
// if (action.type === "increment") {
// return {
// counter: state.counter + 1,
// showCounter: state.showCounter,
// };
// }
// if (action.type === "decrement") {
// return {
// counter: state.counter - 1,
// showCounter: state.showCounter,
// };
// }
// if (action.type === "increase") {
// return {
// counter: state.counter + action.amount,
// showCounter: state.showCounter,
// };
// }
// if (action.type === "toggle") {
// return {
// showCounter: !state.showCounter,
// counter: state.counter,
// };
// }
// return state;
// };
const store = configureStore({
reducer: { counter: counterSlice.reducer, auth: authSlice.reducer },
});
// configureStore๋ก ์ฌ๋ฌ slice๋ด๋ณด๋ผ ์ ์๋ค.
// ์ฌ๋ฌ๊ฐ ๋ด๋ณด๋ผ๋๋ ํ๋์ ๊ฐ์ฒด๋ก ๋ฌถ์ด์ key๊ฐ์ ์ค์ ํ์ฌ์ ๋ณด๋ธ๋ค.
// reducer : {counter : counterSlice.reduce}
export const counterActions = counterSlice.actions;
export const authActions = authSlice.actions;
export default store;
//Counter.js
import classes from "./Counter.module.css";
import { useSelector, useDispatch } from "react-redux";
import { counterActions } from "../store/index";
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter.counter);
const show = useSelector((state) => state.counter.showCounter);
const incerementHandler = () => {
dispatch(counterActions.increment());
};
const decerementHandler = () => {
dispatch(counterActions.decrement());
};
const increseHandler = () => {
//{type:SOME_UNIQUE+IDENTIFIER, payload : 10}
//payload๋ toolkit๊ฐ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ฌ์ฉํ๋ ํ๋๋ช
dispatch(counterActions.increase(10));
};
const toggleCounterHandler = () => {
dispatch(counterActions.toggleCounter());
};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
{show && <div className={classes.value}>{counter}</div>}
<div>
<button onClick={incerementHandler}>Increment</button>
<button onClick={decerementHandler}>Decrement</button>
<button onClick={increseHandler}>Increse</button>
</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
// class Counter extends Component {
// incerementHandler() {
// this.props.increment();
// }
// decerementHandler() {
// this.props.decrement();
// }
// toggleCounterHandler() {}
// render() {
// return (
// <main className={classes.counter}>
// <h1>Redux Counter</h1>
// <div className={classes.value}>{this.props.counter}</div>
// <div>
// <button onClick={this.incerementHandler.bind(this)}>Increment</button>
// <button onClick={this.decerementHandler.bind(this)}>Decrement</button>
// </div>
// <button onClick={this.toggleCounterHandler}>Toggle Counter</button>
// </main>
// );
// }
// }
// const mapStateToProps = (state) => {
// return {
// counter: state.counter,
// };
// };
// const mapDispatchToProps = (dispatch) => {
// return {
// increment: () => dispatch({ type: "increment" }),
// decrement: () => dispatch({ type: "decrement" }),
// };
// };
// export default connect(mapStateToProps, mapDispatchToProps)(Counter);
'โข React' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[React] React Query๋ก ์๋ฒ ์ํ ๊ด๋ฆฌ - ๊ธฐ์ด ๊ฐ๋ (0) | 2022.07.28 |
---|---|
[React] React Hook Form (0) | 2022.05.08 |
[React] React ๋์์๋ฆฌ ( + useMemo / useCallback ) (0) | 2022.01.27 |
[React] Carousel ๊ตฌํ (0) | 2022.01.18 |
[React] useRef / useMemo (0) | 2021.12.23 |