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);
'Archive' 카테고리의 다른 글
[TIL220220] MongoDB 사용 (0) | 2022.02.21 |
---|---|
[Next.JS] Dynamic Routes (0) | 2022.02.15 |
[TIL220128] Image uploader (0) | 2022.01.28 |
[TIL220127] localStorage (0) | 2022.01.28 |
[React] React 동작원리 ( + useMemo / useCallback ) (0) | 2022.01.27 |