๋ค์ ํ๋ฒ ๋งํ์ง๋ง ๋ฆฌ๋์ค๋ ๋ณ๋ก ์ฌ๋ฏธ๊ฐ ์๋ค. ๊ตฌ์ฒด์ ์ผ๋ก ๋งํ์๋ฉด ๋ฆฌ๋์ค๋ฅผ ๋ฐฐ์ฐ๋ ๊ณผ์ ์ด ์ฌ๋ฏธ ์๋ค.
ํ์ง๋ง ํ๋ฒ ๋ฐฐ์ ๋๊ณ ์ฌ์ฉํด๋ณด๋ฉด ๋ฆฌ๋์ค๋ ์ฐธ ๋ ๋ ํ๋ค. ์ข์ ์น๊ตฌ ๋ฆฌ๋์ค๋ฅผ ๋ฆฌ์กํธ ํ๊ฒฝ์์ ์ฌ์ฉํ๋ ๋ฒ์ ์์๋ณด์.
์ผ๋ฐ js ํ๊ฒฝ๊ณผ CRA ํ๊ฒฝ์์ ๋ฆฌ๋์ค๋ฅผ ๋ค๋ฃจ๋ ๊ฑด ํฐ ์ฐจ์ด๊ฐ ์๋ค. ์ ์ฒด์ ์ธ ๋งฅ๋ฝ๋ ๊ฐ๋ค. ์คํ๋ ค CRA์์ ๋ ํธํ๊ฒ ๋๊ปด์ง๋ ๋ถ๋ถ๋ ์๋ค.
yarn add redux
yarn add react-redux
์ฐ์ ๋ ๊ฐ์ ํจํค์ง๋ฅผ ์ค์นํด์ผํ๋ค.
redux๋ ์ด์ ํฌ์คํธ์์ ์ค๋ช ํ๋ ๊ฒ์ฒ๋ผ redux core์ ํด๋นํ๋ ํจํค์ง์ด๊ณ , react-redux๋ ๋ฆฌ์กํธ ํ๊ฒฝ์์ ๋ฆฌ๋์ค๋ฅผ ๋ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ ์ ์๊ฒ ๋์์ฃผ๋ ํจํค์ง์ด๋ค.
react-redux๋ store๋ฅผ ์ ๊ณตํ๋ ๊ณ ์ฐจ ์ปดํฌ๋ํธ์ redux-hooks ๋ฑ์ ์ ๊ณตํด์ค๋ค.
counter์ todos๋ฅผ ๊ตฌํํ ์ฑ์ด๋ค. ๋ฆฌ์กํธ์์๋ ๋ง์ฐฌ๊ฐ์ง๋ก ์ก์ ์์ฑ์, ๋ฆฌ๋์๊ฐ ํ์ํ๋ค. dispatch๋ ๋ฆฌ๋์ค ํ , ์คํ ์ด๋ ๊ณ ์ฐจ ์ปดํฌ๋ํธ์์ ์ ๊ณตํ๋ค.
// counter.js
const INCREMENT = "counter/INCREMENT";
const DECREMENT = "counter/DECREMENT";
export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
const initialState = 0;
function counter(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
}
export default counter;
// todos.js
const ADD_TODO = "todos/ADD_TODO";
const DELETE_TODO = "todos/DELETE_TODO";
const CHECK_TODO = "todos/CHECK_TODO";
export const addTodo = (todo) => ({ type: ADD_TODO, todo });
export const deleteTodo = (id) => ({ type: DELETE_TODO, id });
export const checkTodo = (id) => ({ type: CHECK_TODO, id });
let nextId = 0;
const initialState = {};
function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return { ...state, [++nextId]: action.todo };
case DELETE_TODO:
const newState = { ...state };
delete newState[action.id];
return newState;
case CHECK_TODO: {
console.log(!state[action.id].done);
const newState = {
...state,
[action.id]: { ...state[action.id], done: !state[action.id].done },
};
return newState;
}
default:
return state;
}
}
export default todos;
๊ฐ๊ฐ ๊ธฐ๋ฅ์ ๋ง๊ฒ ์ก์ ํ์ , ์ก์ ์์ฑ์, ์ด๊ธฐ ์ํ, ๋ฆฌ๋์๋ฅผ ๋ง๋ค์๋ค. ์ก์ ์ ๊ฒฝ์ฐ ํ์ํ ์ธ์๋ค์ ์ถ๊ฐ๋ก ๋ฐ์ ์ ์๋๋ฐ ๋ณดํต์ payload๋ผ๋ ์ด๋ฆ์ผ๋ก ์ ์ธํ๋ค.
ํ์ง๋ง ์ด ํฌ์คํธ์์๋ ์ญํ ์ ๋ง๊ฒ ์ด๋ฆ์ ๋ถ์ฌํ๊ฒ ๋ค.
export๋ ๋ฆฌ๋์๋ ๋ฆฌ๋์ค ํจํค์ง์ combineReducer ๋ฉ์๋๋ก ํฉ์น ์ ์๋ค.
// module/index.js
import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";
const rootReducer = combineReducers({ counter, todos });
export default rootReducer;
// index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { createStore } from "redux";
import rootReducer from "./module";
import { Provider } from "react-redux";
const store = createStore(rootReducer);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Provider๋ store๋ฅผ ์ฃผ์ ํ๋ ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ค. ํ์ ์ปดํฌ๋ํธ๋ค์ ๋ฆฌ๋์ค ํ ์ ์ด์ฉํด ์ํ๋ฅผ ๊ตฌ๋ ํ ์ ์๋ค.
๋ฌธ๋ createStore์ ์ทจ์์ ์ด ์๊ฒจ ์๊ธธ๋ ํ์ธํด๋ดค๋๋ redux-toolkit์ผ๋ก ์ฌ์ฉํ๊ธธ ๊ถ์ฅํ๋ค๋ ๋ฉ์์ง๊ฐ ๋ณด์๋ค.
deperecated ๋์๋ค๊ณ ํ๋ ์กฐ๋ง๊ฐ ์ฌ์ฉ๋ฒ์ ์ตํ์ผ๊ฒ ๋ค.
// counter.jsx
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { increment, decrement } from "../module/counter";
const Counter = () => {
const number = useSelector((state) => state.counter);
const dispatch = useDispatch();
const onIncrement = () => {
dispatch(increment());
};
const onDecrement = () => {
dispatch(decrement());
};
return (
<>
<strong style={{ display: "block" }}>{number}</strong>
<button onClick={onIncrement}>Plus</button>
<button onClick={onDecrement}>Minus</button>
</>
);
};
export default Counter;
counter ์ปดํฌ๋ํธ๋ค. ํน๋ณํ ๊ธฐ๋ฅ์ ์๊ณ ๊ตฌ๋ ํ state์์ ๋ฐ์ ๊ฐ์ ๋ ๋๋งํ๋ค.
์ด๋ useSelector ํ ๊ณผ useDispatch ํ ์ ์ฌ์ฉํ๋๋ฐ useSelector๋ ์ํ๋ฅผ ๊ตฌ๋ ํด์ฃผ๋ ์ญํ , useDispatch ํ ์ผ๋ก ์์ฑ๋ dispatch๋ ์ก์ ์ dispatchํ์ฌ state๋ฅผ ๋ณ๊ฒฝํ๋ ์ญํ ์ ํ๋ค.
// todos.jsx
import React, { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addTodo, checkTodo, deleteTodo } from "../module/todos";
const Todo = React.memo(({ id, todo, onDelete, onCheck }) => {
return (
<li>
<h3>{todo.title}</h3>
<input type='checkbox' checked={todo.done} onChange={() => onCheck(id)} />
<button onClick={() => onDelete(id)}>์ญ์ </button>
</li>
);
});
const Todos = () => {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
const insertTodo = useCallback(
(e) => {
e.preventDefault();
const title = e.target[0].value;
dispatch(addTodo({ title, done: false }));
e.target.reset();
},
[dispatch]
);
const onDelete = useCallback(
(id) => {
dispatch(deleteTodo(id));
},
[dispatch]
);
const onCheck = useCallback(
(id) => {
dispatch(checkTodo(id));
},
[dispatch]
);
return (
<>
<ul>
{Object.keys(todos).map((id) => (
<Todo
id={id}
key={id}
todo={todos[id]}
onDelete={onDelete}
onCheck={onCheck}
/>
))}
</ul>
<form onSubmit={insertTodo}>
<input type='text' />
<button type='submit'>์ถ๊ฐ</button>
</form>
</>
);
};
export default Todos;
todos์ ์ํ๋ฅผ object ํํ๋ก ์๊ฐํ๋๋ ์ฝ๋๊ฐ ์ข ์ง์ ๋ถํด์ง ๋๋์ด๋ค. ๋ฐฐ์ด๋ก ํ๋ค๋ฉด ์ข ๋ ๊น๋ํ๊ฒ ๋ณํ ๊ฒ ๊ฐ๋ค.
์ด๋ Todos์ ์ด๋ฒคํธ ํธ๋ค๋ฌ์ useCallback, ๊ทธ๋ฆฌ๊ณ state์ ๋ฐ๋ผ ๋ ๋๋ง๋๋ Todo์ React.memo๋ฅผ ์ค์ ํด์คฌ๋๋ฐ ์ด๊ฒ์ผ๋ก ๋ถํ์ํ ๋ฆฌ๋ ๋๋ง์ ๋ฐฉ์งํ ์ ์๋ค.
์ถํ ๋ฆฌ์กํธ๊ฐ ๋ฆฌ๋ ๋๋ง ๋๋ ์์ธ๊ณผ ๊ทธ์ ๋ฐ๋ฅธ ์ต์ ํ ๋ฐฉ๋ฒ๋ ํฌ์คํ ํด์ผ๊ฒ ๋ค.
์ ๋ง ๊ฐ๋จํ Counter์ Todo์ง๋ง ๋ฆฌ๋์ค๋ฅผ ์ด์ฉํด ๊ตฌํํด๋ดค๋๋ ์ฝ์ง ์์๋ค.
๊ทธ๋ฆฌ๊ณ ๋ฆฌ๋์ค๋ ์ก์ ์ ๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ค. ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ํด์๋ ๋ฆฌ๋์ค ์ธ๋ถ์์ ๋น๋๊ธฐ ์์ ์ ์ฒ๋ฆฌํ๋ ๋ก์ง์ ๋ง๋ค๊ณ dispatch๋ฅผ ํด์ผํ๋ค.
์ด ์์ ์ ๋ณต์กํ ๋ก์ง์ด๋ผ๋ฉด ๊ต์ฅํ ๋ถํธํด์ง ์ ์๋๋ฐ ๋คํํ ๋ฆฌ๋์ค๋ ์ก์ ์ ํด๋นํ๋ ์ ๋ฐ์ดํธ๋ฅผ ์ํํ๊ธฐ ์ ๋ฏธ๋ค์จ์ด๋ฅผ ๋์ ํด ์ฒ๋ฆฌํ ์ ์๋ค.
๋ค์์ ๋ฆฌ๋์ค ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ์ฃผ๋ก ์ฐ์ด๋ redux-saga์ ๋ํด์ ํฌ์คํ ํ ์์ ์ด๋ค.
ใธ