Local State : 데이터가 변경되어서 하나의 컴포넌트에 속하는 UI에 영향을 미치는 상태
Cross-Component State : 다양한 컴포넌트에 영향을 미치는 상태
App-Wide State : 애플리케이션의 모든 컴포넌트에 영향을 미치는 상태
Cross-Component State, App-Wide State는 모두 prop을 쉽게 처리하기 위해 Context API를 사용하거나 Redux를 사용해야 한다.
컴포넌트가 액션을 발송(dispatch)하고 그 액션에는 수행해야 할 작업이 서술되어있다(하지만 그것을 직접 하지는 않는다.). 그리고 액션들을 리듀서로 전달해서 액션이 원하는 것을 리듀서가 실행한다. → 리듀서는 새로운 상태를 내보내고 그것잉 중앙 데이터 저장소의 기존 상태를 대체한다. → 데이터 저장소가 업데이트가 되면 구독 중이던 컴포넌트가 알림을 받고 컴포넌트는 UI를 업데이트하게 된다.
npm initnpm install redux// 리듀서 함수
const redux = require("redux");
// 초기에 실행될 때 초기 상태값을 지정
const counterReducer = (state = { counter: 0 }, action) => {
return {
counter: state.counter + 1,
};
};
const store = redux.createStore(counterReducer); // 저장소는 어떤 리듀서가 그 저장소를 변경하는지 알아야 한다.
// 구독자
const counterSubscriber = () => {
const latestState = store.getState();
console.log(latestState);
};
store.subscribe(counterSubscriber);
store.getState() : createStore()로 생성된 저장소에서 업데이트 된 이후의 최신 상태 스냅샷을 제공한다// 액션
store.dispatch({ type: "increment" });
액션
node redux-demo.js를 터미널에 입력하여 실행 → { counter: 2 } (초기 값은 counter:1 )
// 리듀서 함수
const redux = require("redux");
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
};
}
return state;
};
const store = redux.createStore(counterReducer);
// 구독자
const counterSubscriber = () => {
const latestState = store.getState();
console.log(latestState);
};
store.subscribe(counterSubscriber);
// 액션
store.dispatch({ type: "increment" });
node redux-demo.js// 리듀서 함수
const redux = require("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 = redux.createStore(counterReducer);
// 구독자
const counterSubscriber = () => {
const latestState = store.getState();
console.log(latestState);
};
store.subscribe(counterSubscriber);
// 액션
store.dispatch({ type: "increment" });
store.dispatch({ type: "decrement" });
node redux-demo.js{ counter: 1 }
{ counter: 0 }
store.subscribe()에서 왔다.npm install redux react-redux : react-redux는 리액트 앱과 리덕스 저장소와 리듀서에 간단히 접속하게 함.npm start
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;
react-redux에서 Provider 컴포넌트를 import 할 수 있다.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.jsx";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
import classes from "./Counter.module.css";
import { useSelector } from "react-redux";
const Counter = () => {
// 해당 함수를 react-redux가 수행. 이 컴포넌트에 필요로 하는 상태 부분을 받아온다.
// useSelector를 사용할 때 react-redux는 이 컴포넌트를 위해 리덕스 저장소에 자동으로 구독을 설정함.
// 이제 이 컴포넌트는 리덕스 저장소에서 데이터가 변경될 때마다 자동으로 업데이트되고 최신 카운터를 받는다.
const counter = useSelector((state) => state.counter);
const toggleCounterHandler = () => {};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{counter}</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
useSelector : react-redux 팀이 만든 커스텀 훅으로 저장소가 관리하는 상태 부분을 우리가 자동으로 선택할 수 있다.useStore도 있으나 useSelector가 사용하기 더 편하다.useSelector 대신 connect를 사용할 수 있다.
import classes from "./Counter.module.css";
import { useSelector, useDispatch } from "react-redux";
const Counter = () => {
const counter = useSelector((state) => state.counter);
const dispatch = useDispatch();
const incrementHandler = () => {
dispatch({ type: "increment" });
};
const decrementHandler = () => {
dispatch({ type: "decrement" });
};
const toggleCounterHandler = () => {};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{counter}</div>
<div className="counter">
<button onClick={incrementHandler}>Increment</button>
<button onClick={decrementHandler}>Decrement</button>
</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
useDispatch : 실행할 수 있는 dispatch function을 반환한다.dispatch 함수는 redux store에 대한 action을 보낸다.
import { Component } from "react";
import classes from "./Counter.module.css";
import { connect } from "react-redux";
class Counter extends Component {
incrementHandler() {
this.props.increment();
}
decrementHandler() {
this.props.decrement();
}
toggleCounterHandler() {}
render() {
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{this.props.counter}</div>
<div className="counter">
<button onClick={this.incrementHandler.bind(this)}>Increment</button>
<button onClick={this.decrementHandler.bind(this)}>Decrement</button>
</div>
<button onClick={this.toggleCounterHandler.bind(this)}>
Toggle Counter
</button>
</main>
);
}
}
// 리덕스 상태를 받는 함수 => useSelector와 비슷
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);
// 커넥트가 실행되면 새로운 함수를 그 값으로 리턴한다.
mapStateToProps이고 다른 하나는 mapDispatchToProps이다.import { createStore } from "redux";
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
};
}
// 5씩 증가하기 위한 리듀서 함수 작성
if (action.type === "increase") {
return {
counter: state.counter + action.amount,
};
}
if (action.type === "decrement") {
return {
counter: state.counter - 1,
};
}
return state;
};
const store = createStore(counterReducer);
export default store;
import classes from "./Counter.module.css";
import { useSelector, useDispatch, connect } from "react-redux";
const Counter = () => {
const counter = useSelector((state) => state.counter);
const dispatch = useDispatch();
const incrementHandler = () => {
dispatch({ type: "increment" });
};
// 5씩 증가하기 위한 함수 작성 -> index.jsx에서 작성된 리듀서 함수와 같은 action 프로퍼티(amount)를 사용해야한다.
const increseHandler = () => {
dispatch({ type: "increase", amount: 5 });
};
const decrementHandler = () => {
dispatch({ type: "decrement" });
};
const toggleCounterHandler = () => {};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{counter}</div>
<div className="counter">
<button onClick={incrementHandler}>Increment</button>
{/* increaseHandler 연결 */}
<button onClick={increseHandler}>Increse by 5</button>
<button onClick={decrementHandler}>Decrement</button>
</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;

import { createStore } from "redux";
const initailState = { counter: 0, showCounter: true }; // 초기 상태
const counterReducer = (state = initailState, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
showCounter: state.showCounter,
};
}
if (action.type === "increase") {
return {
counter: state.counter + action.amount,
showCounter: state.showCounter,
};
}
if (action.type === "decrement") {
return {
counter: state.counter - 1,
showCounter: state.showCounter,
};
}
// 토글 버튼을 위한 리듀서 함수 작성
if (action.type === "toggle") {
return {
showCounter: !state.showCounter,
counter: state.counter,
};
}
return state;
};
const store = createStore(counterReducer);
export default store;
import classes from "./Counter.module.css";
import { useSelector, useDispatch, connect } from "react-redux";
const Counter = () => {
const counter = useSelector((state) => state.counter);
const show = useSelector((state) => state.showCounter); // showCounter 상태 받아옴
const dispatch = useDispatch();
const incrementHandler = () => {
dispatch({ type: "increment" });
};
const increseHandler = () => {
dispatch({ type: "increase", amount: 5 });
};
const decrementHandler = () => {
dispatch({ type: "decrement" });
};
// 토글 동작
const toggleCounterHandler = () => {
dispatch({ type: "toggle" });
};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
{show && <div className={classes.value}>{counter}</div>}
<div className="counter">
<button onClick={incrementHandler}>Increment</button>
<button onClick={increseHandler}>Increse by 5</button>
<button onClick={decrementHandler}>Decrement</button>
</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;

// index.jsx
const counterReducer = (state = initailState, action) => {
if (action.type === "increment") {
return {
counter: state.counter + 1,
showCounter: state.showCounter, // 꼭 이런식으로 계속해서 오버라이딩을 해줘야 한다.
};
}
return state;
};
// index.jsx
// 🚨 잘못된 방법 🚨
const counterReducer = (state = initailState, action) => {
if (action.type === "increment") {
state.counter++;
return state;
}
return state;
};
state.counter++처럼 절대 기존의 state를 변형해서는 안된다!!항상 새로운 state 객체를 반환해서 재정의해야한다.
리덕스에서 관리해야 할 상태가 더 많아질 경우
위와 같은 문제점을 해결하기 위해서 Redux toolkit을 사용한다.
npm install @reduxjs/toolkitnpm startimport { createSlice } from "@reduxjs/toolkit";
const initailState = { counter: 0, showCounter: true };
// 전역 상태의 slice 미리 만들기
createSlice({
name: "counter",
initialState: initailState,
reducers: {
increment(state) {
// 바로 상태를 변경할 수 있다. => 그러나 상태를 직접 변경하는 것처럼 보일 뿐이다.
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter = state.counter + action.amount;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
createSliceimport { createSlice, configureStore } from "@reduxjs/toolkit";
const initailState = { counter: 0, showCounter: true };
// 전역 상태의 slice 미리 만들기
const counterSlice = createSlice({
name: "counter",
initialState: initailState,
reducers: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter = state.counter + action.amount;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
const store = configureStore({
reducer: {
// 여러개의 리듀서를 가질때
counter: counterSlice.reducer,
},
});
export default store;
configureStore : createStore처럼 store를 만든다.import { createSlice, configureStore } from "@reduxjs/toolkit";
const initailState = { counter: 0, showCounter: true };
const counterSlice = createSlice({
name: "counter",
initialState: initailState,
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 store = configureStore({
reducer: counterSlice.reducer,
});
// 액션 생성자 메서드를 사용하여 리듀서 매서드와 이름이 같으면 액션을 전달한다.
export const counterActions = counterSlice.actions;
export default store;
import classes from <"./Counter.module.css";
import { useSelector, useDispatch } from "react-redux";
import { counterActions } from "../store/index"; // action들 가져옴
const Counter = () => {
const counter = useSelector((state) => state.counter);
const show = useSelector((state) => state.showCounter);
const dispatch = useDispatch();
const incrementHandler = () => {
dispatch(counterActions.increment());
};
const increseHandler = () => {
dispatch(counterActions.increase(5)); // {type: SOME_UNIQUE_IDENTIFIER, payload: 5}
};
const decrementHandler = () => {
dispatch(counterActions.decrement());
};
const toggleCounterHandler = () => {
dispatch(counterActions.toggleCounter());
};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
{show && <div className={classes.value}>{counter}</div>}
<div className="counter">
<button onClick={incrementHandler}>Increment</button>
<button onClick={increseHandler}>Increse by 5</button>
<button onClick={decrementHandler}>Decrement</button>
</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
import { createSlice, configureStore } from "@reduxjs/toolkit";
const initailCounterState = { counter: 0, showCounter: true };
const counterSlice = createSlice({
name: "counter",
initialState: initailCounterState,
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 initialAuthState = {
isAuthenticated: false,
};
const authSlice = createSlice({
name: "auth",
initialState: initialAuthState,
reducers: {
login(state) {
state.isAuthenticated = true;
},
logout(state) {
state.isAuthenticated = false;
},
},
});
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
auth: authSlice.reducer,
},
});
export const counterActions = counterSlice.actions;
export const authActions = authSlice.actions;
export default store;
import Counter from "./components/Counter";
import Header from "./components/Header";
import Auth from "./components/Auth";
import UserProfile from "./components/UserProfile";
import { useSelector } from "react-redux";
function App() {
const isAuth = useSelector((state) => state.auth.isAuthenticated);
return (
<>
<Header />
{!isAuth && <Auth />}
{isAuth && <UserProfile />}
<Counter />
</>
);
}
export default App;
import { useDispatch, useSelector } from "react-redux";
import { authActions } from "../store/index";
import classes from "./Header.module.css";
const Header = () => {
const isAuth = useSelector((state) => state.auth.isAuthenticated);
const dispatch = useDispatch();
const onLogout = () => {
dispatch(authActions.logout());
};
return (
<header className={classes.header}>
<h1>Redux Auth</h1>
{isAuth && (
<nav>
<ul>
<li>
<a href="/">My Products</a>
</li>
<li>
<a href="/">My Sales</a>
</li>
<li>
<button onClick={onLogout}>Logout</button>
</li>
</ul>
</nav>
)}
</header>
);
};
export default Header;
import { useDispatch } from "react-redux";
import { authActions } from "../store/index";
import classes from "./Auth.module.css";
const Auth = () => {
const dispatch = useDispatch();
const onLogin = (event) => {
event.preventDefault();
dispatch(authActions.login());
};
return (
<main className={classes.auth}>
<section>
<form onSubmit={onLogin}>
<div className={classes.control}>
<label htmlFor="email">Email</label>
<input type="email" id="email" />
</div>
<div className={classes.control}>
<label htmlFor="password">Password</label>
<input type="password" id="password" />
</div>
<button>Login</button>
</form>
</section>
</main>
);
};
export default Auth;
import { createSlice } from "@reduxjs/toolkit";
const initailCounterState = { counter: 0, showCounter: true };
// 전역 상태의 slice 미리 만들기
const counterSlice = createSlice({
name: "counter",
initialState: initailCounterState,
reducers: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter = state.counter + action.payload;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
export const counterActions = counterSlice.actions;
export default counterSlice.reducer;
import { createSlice } from "@reduxjs/toolkit";
const initialAuthState = {
isAuthenticated: false,
};
const authSlice = createSlice({
name: "auth",
initialState: initialAuthState,
reducers: {
login(state) {
state.isAuthenticated = true;
},
logout(state) {
state.isAuthenticated = false;
},
},
});
export const authActions = authSlice.actions;
export default authSlice.reducer;
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counter.js";
import authReducer from "./auth.js";
const store = configureStore({
reducer: {
counter: counterReducer,
auth: authReducer,
},
});
export default store;