์ค๋์ React ์์ Redux ๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ํด์ ์์๋ณด๋๋ก ํ๊ฒ ๋ค.
๊ทธ๋ฌ๊ธฐ ์ํด์๋ ์ฐ์ 2๊ฐ์ง ๊ธฐ์ ์ด ์ ํ๋์ด์ผ ํ๋ค.
์ ๋ด์ฉ๋ค์ ๋ชจ๋ฅธ๋ค๋ฉด ์กฐ๊ธ ํ๋ค์ด์ง ์ ์์ผ๋ ์ ๋งํฌ์์ ํ์ธํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ธ ๊ฒ ๊ฐ๋ค.
๋ชฉ์ฐจ
- redux์ react-redux์ ์ฐจ์ด
- redux ์ ๊ธฐ๋ณธ ๊ฐ๋
- ๊ตฌ์ฑ ์์
- Provider
useDispatch()
useSelector()
- react-redux ๋ก todo list ๋ง๋ค๊ธฐ
- ํ๋ก์ ํธ ๋ง๋ค๊ธฐ
- ์์กด์ฑ ์ถ๊ฐ
- styled-components
- react-redux
- UI ์์
ํ๊ธฐ
- ํ ํ๋ฉด ๋ง๋ค๊ธฐ
- ๋ฆฌ๋์ค ์์
ํ๊ธฐ
- action type ๋ง๋ค๊ธฐ
- action creator ๋ง๋ค๊ธฐ
- reducer ๋ง๋ค๊ธฐ
- store ์ถ๊ฐํ๊ธฐ
redux์ react-redux์ ์ฐจ์ด
์ ์๋ค์ถ์ด ๋ฆฌ๋์ค๋ Vue, Angular, Ember, Vanilla JS ์๋ ๋ณ๊ฐ๋ก ๋์๊ฐ๋ ๋ ๋ฆฝ์ ์ธ Javascript ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค.
๋ฆฌ๋์ค๋ ๋ค๋ฅธ ํ๋ ์์ํฌ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ธ ๋๋ UI ๋ฐ์ธ๋ฉ์ ํด์ผ๋๋๋ฐ, ๊ทธ๊ฒ ๋ฐ๋ก React์์๋ React-Redux ๋ผ๊ณ ๋ถ๋ฆฐ๋ค.
react-redux, ๊ณต์ ํํ์ด์ง ์์ ์์ธํ ์ปจ์ ์ ํ์ธํ ์ ์์ผ๋ ํ์ธํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ธ ๊ฒ ๊ฐ๋ค.
redux์ ๊ธฐ๋ณธ ๊ฐ๋
redux ์ ๊ธฐ๋ณธ ๊ฐ๋ ๊ณผ ๊ตฌ์ฑ์์์ ๋ํด์๋ ์ง๋ ์๊ฐ ์์ ํ์ธํ ์ ์์ง๋ง ๊ฐ๋จํ๊ฒ ๋์ง๊ณ ๋์ด๊ฐ๋ณด์.
๊ตฌ์ฑ ์์
- Provider
- action
- ์์ js ๊ฐ์ฒด
- action type
- action payload
- ์์ js ๊ฐ์ฒด
- action Creator
- action ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ ํจ์
- Dispatch
- action ์ ๋ฆฌ๋์์๊ฒ ์ ๋ฌํ๋ค
- Reducer
- action ์ด (์๋ก์ด state๋ฅผ ๋ฐํ)
- Store
- ๊ด๋ฆฌํ๋ ค๋ state๋ฅผ ์ ์ฅํ๋ ๊ณณ
Provider
Provider ๋ ๋ฆฌ๋์ค์ ๊ฐ์ฅ ๊ธฐ๋ณธ์ด๋ผ๊ณ ํ ์ ์๋ค.
์ฐ๋ฆฌ๋ Redux ๋ฅผ state ๋ฅผ ๊ด๋ฆฌํ๊ณ Global ํ๊ฒ ์ฌ์ฉํ๊ธฐ ์ํด์ ์ฌ์ฉํ๋๋ฐ, ๊ทธ๋ฌ๊ธฐ ์ํด์๋ ๊ฐ์ฅ ์ต์์ ์ปดํฌ๋ํธ์ Provider ์ปดํฌ๋ํธ๋ฅผ ๋ถ๋ชจ ์ปดํฌ๋ํธ๋ก ๋ง๋ค์ด์ผ ํ๋ค.
๋ณดํต create-react-app ์ ํตํด์ ๋ฆฌ์กํธ ์ฑ์ ๋ง๋ค๊ฒ ๋๋ฉด App ์ปดํฌ๋ํธ๊ฐ ๊ฐ์ฅ ์์ ์ปดํฌ๋ํธ๊ฐ ๋๋ค.
App ์ปดํฌ๋ํธ ๋ด๋ถ์ ์๋ ์ปดํฌ๋ํธ๋ค์ด store๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํ๊ธฐ ์ํด์ <Provider />
๋ก wrapping ํด์ผ ํ๋ค.
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
๋ณดํต React ์์๋ App.js
, Next.js ์์๋ _app.js
์์ ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค.
useDispatch ์ useSelector
useDispatch()์ useSelector()๋ ๋ฆฌ๋์ค๋ฅผ ํจ์ํ ์ปดํฌ๋ํธ์์ ์ฌ์ฉํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์ด๋ค.
๋ฆฌ์กํธ๊ฐ Hook์ ๋ฐํํ ์ดํ๋ก ์ฐ๋ฆฌ๋ ์ด์ ๋ ์ด์ ํด๋์คํ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค.
ํด๋์คํ ์ปดํฌ๋ํธ์์ ๋ฆฌ๋์ค๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ connect, mapDispatchToProps, mapStateToProps์ ๊ฐ์ HOC๋ฅผ ์ด์ฉํด์ผ ํ์ง๋ง ์ด์ ๋ Hook ์ ์ด์ฉํด์ ๋์ฑ ๊ฐ๋ ์ฑ ์๊ณ ํธ๋ฆฌํ ๋ฆฌ๋์ค๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ๋ ๊ฒ์ด๋ค.
useSelector()
useSelector ๋ Redux Store ๋ก ๋ถํฐ state ๋ฅผ ๊ฐ์ ธ์ฌ ๋ ์ฌ์ฉํ๋ค.
import { useSelector } from "react-redux";
const result = useSelector((state) => state.someState);
useDispatch()
useDispatch ๋ action ์ dispatch ํ ๋ ์ฌ์ฉํ ์ ์๋ ํ ์ด๋ค.
import { useDispatch } from "react-redux";
const dispatch = useDispatch();
dispatch(ACTION);
useDispatch ์์๋ memoize๋ฅผ ์ํด์ useCallBack() ์ ํจ๊ป ์ฌ์ฉํ๊ธฐ๋ ํ๋๋ฐ, ์์ธํ ๋ด์ฉ์ ๋ฐ๋ก ์ด ๋ค์ ์๊ฐ์ ์์๋ณด๋๋ก ํ์!
์ด์ ์ค์ ์ผ๋ก ๋ค์ด๊ฐ todo list ๋ฅผ ๋ง๋ค์ด๋ณด์
react-redux ๋ก todo list ๋ง๋ค๊ธฐ
Todo List ๋ฅผ ๋ง๋ค ๊ฒ์ธ๋ฐ ์๋ง ๊ฒฐ๊ณผ๋ฌผ์ ๋ค์๊ณผ ๊ฐ์ด ์๊ฒผ์ ๊ฒ์ด๋ค.
์์๋ ์์ ๋ชฉ์ฐจ์์ ์ด์ผ๊ธฐ ํ๋ ๊ฒ ์ฒ๋ผ ๋ค์๊ณผ ๊ฐ์ ์์๋ก ๊ฐ๋ฐ ํ๋ค.
- ํ๋ก์ ํธ ๋ง๋ค๊ธฐ
- ์์กด์ฑ ์ถ๊ฐ
- ๋ฆฌ๋์ค ์์ ํ๊ธฐ
- UI ์์ ํ๊ธฐ
ํ๋ก์ ํธ ๋ง๋ค๊ธฐ & ์์กด์ฑ ์ถ๊ฐ
CRA ๋ฅผ ์ด์ฉํด์ ๊ฐ๋จํ ๋ฆฌ์กํธ ํ๋ก์ ํธ๋ฅผ ์์ฑํด๋ณด์.
$ create-react-app redux-todo
$ npx create-react-app redux-todo
๊ทธ๋ฆฌ๊ณ ์์กด์ฑ๋ค์ ์ถ๊ฐํด๋ณด์.
๊ฐ๋จํ๊ฒ redux์ styled-component ๋ฅผ ์ถ๊ฐํด๋ณด์.
$ npm install -y styled-components
$ npm install -y redux
์ด๋ ๊ฒ ๊น์ง ํ๋ค๋ฉด package.json
์ ์์กด์ฑ์ด ์ ๋ค์ด์๋์ง ํ์ธํ๊ณ ๋ฃจํธ ํด๋์์ App.js์ index.js ๋ง ๋จ๊ธฐ๊ณ ๋๋จธ์ง๋ ์ญ์ ํ ๋ค, ํ์ํ์ง ์์ ์ฝ๋๋ค์ ์ง์ ์คํ์ด ๋๋ ๊ฒ ๊น์ง ํ์ธํ์!
๋ฆฌ๋์ค ์์ ํ๊ธฐ
์ด๋ฒ ๊ธ์ ํต์ฌ์ธ ๋ฆฌ๋์ค ์์ ํ๊ธฐ ๋ถ๋ถ์ด๋ค.
๋ฆฌ๋์ค ์์ ์ ํ๊ธฐ ์ํด์๋ 3๊ฐ์ js ํ์ผ์ด ํ์ํ๋ค.
- actions.js
- action ์์ฑ์ ํ๊ณ ํด๋น action ์ด ์ด๋ค ์ผ์ ์ํํ ์ง ์ง์ ํ๋ค.
- reducer.js
- action ์ ์คํ์ํฌ reducer
- action ๊ฐ์ฒด๋ฅผ ๋ฐ์ state ๋ฅผ ๋ณ๊ฒฝ์ํจ๋ค.
- store.js
- reducer ๋ก global store ๋ฅผ ๋ง๋ค redux store
commons
๋๋ ํ ๋ฆฌ๋ฅผ ์์ฑํ๊ณ ๋ค์ 3๊ฐ์ ํ์ผ์ ๊ฐ๊ฐ ์์ฑํ๋๋ก ํ์.
actions.js
export const ADD = "ADD_TODO";
export const DELETE = "DELETE_TODO";
let id = 1;
export const add_todo = (todo) => {
return {
type: ADD,
todo: {
id: id++,
title: todo.title,
isComplete: todo.isComplete,
},
};
};
export const delete_todo = (id) => {
return {
type: DELETE,
id,
};
};
์ฐ๋ฆฌ๋ ์ด๋ฒ todo list ์์ 2๊ฐ์ ์ก์ ๋ง์ ์ฌ์ฉํ ๊ฒ์ด๋ค.
- todo ์์ฑ
- todo ์ญ์
todo๋ฅผ ์์ฑํ๊ธฐ ์ํด์๋ ๋งค๊ฐ๋ณ์๋ก ๋ฐ์ todo ๊ฐ์ฒด๋ฅผ reducer ์๊ฒ ๋ฐํ ํ๊ณ , todo๋ฅผ ์ญ์ ํ๊ธฐ ์ํด์๋ ์ญ์ ํ๋ ค๋ todo์ id ๋ง์ ๋๊ฒจ์ฃผ๋๋ก ํ๋ค.
์์ธํ ์์ฑ, ์ญ์ ๋ก์ง์ reducer ์๊ฒ ์์ํ๋๋ก ํ๋ ๊ฒ์ด action์ ์ฑ ์์ ๋ง์ง๋ง์ด๋ค.
reducer.js
import { ADD, DELETE } from "./actions";
const initialState = {
todos: [],
};
export const reducer = (state = initialState, action) => {
if (action.type === ADD) {
return {
// ๋ง์ฝ ๋ค๋ฅธ state ๊ฐ ์กด์ฌํ๋ค๋ฉด ์ ๊ฐ ์ฐ์ฐ ...state ๋ฅผ ํด์ผํจ
// ํ์ง๋ง ํ์ฌ state ์๋ todos ํ๋ ๋ฟ์ด๋ผ todos ๋ง ๋ฐํํ๋ฉด ๋จ
todos: [...state.todos, action.todo],
};
} else if (action.type === DELETE) {
return {
todos: [...state.todos.filter((todo) => todo.id !== action.id)],
};
} else {
return state;
}
};
action-creator ์๊ฒ ๋ฐ์ action.type, action.payload๋ฅผ ๊ฐ์ง๊ณ ์ด์ ์ค์ ๋ก ํด๋น ์ก์ ์ด ์ด๋ค ๊ฒฐ๊ณผ๋ฅผ ๋ด์ด์ผ ํ๋์ง๋ฅผ ์ด reducer ์์ ์ ์ํ๋๋ก ํ๋ค.
์์ฑ์ ์ํด์๋ initialState ์ ์กด์ฌํ๋ todo ๋ฐฐ์ด์ ์๋ก์ด ํญ๋ชฉ์ ์ถ๊ฐํ ์๋ก์ด state๋ฅผ ๋ฐํํ๋๋ก ํ๋ค.
๊ทธ๋ฆฌ๊ณ ์ญ์ ์์๋ action์ด ๋๊ฒจ์ค id๋ฅผ ๊ฐ์ง๊ณ Array.filter()
๋ฉ์๋๋ฅผ ์ด์ฉํด์ id๊ฐ ๋์ผํ todo ๊ฐ์ฒด๋ฅผ ์ญ์ ํ todos๋ฅผ ๋ฐํํ๋๋ก ํ๋ค.
store.js
import { createStore } from "redux";
import { reducer } from "./reducer";
const store = createStore(reducer);
export default store;
๋ฆฌ๋์ค ์คํ ์ด์ reducer๋ฅผ ๋ฃ์ด์ฃผ๋ ์์ ์ ํด๋น ํ์ผ์์ ์ํํ๋ค.
UI ์์ ํ๊ธฐ
UI ๋ 4๊ฐ์ ์ปดํฌ๋ํธ๋ฅผ ์ด์ฉํด์ ๊ตฌ์ฑํ ๊ฒ์ด๊ณ style ์ ์์ฃผ ๊ฐ๋จํ๊ฒ ์ ์ฉํ๋ ค ํ๋ค. ๊ฐ๊ฐ์ ์ปดํฌ๋ํธ๋ ๊ณ ์ ํ styleใด.js ํ์ผ์ ๊ฐ๊ฒ ๋๋ค.
- App.js
- InputForm.js
- TodoItem.js
- TodoList.js
App.js & App.styles.js
// App.js
import React from "react";
import * as S from "./App.styles";
import InputForm from "./components/InputForm";
import TodoList from "./components/TodoList";
function App() {
return (
<S.Container>
<S.Wrapper>
<h1>Redux ๋ก ๋ฐฐ์ฐ๋ Todo List</h1>
<InputForm />
<TodoList />
</S.Wrapper>
</S.Container>
);
}
export default App;
// App.styles.js
import styled from "styled-components";
export const Container = styled.div`
width: 100vw;
height: 200vh;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
background: #e9ecef;
input {
appearance: none;
outline-style: none;
border: none;
}
`;
export const Wrapper = styled.div`
margin-top: 300px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
`;
App.js ๋ ์ฐ๋ฆฌ๊ฐ ๋ง๋ค InputForm, TodoItem, TodoList๋ฅผ ํฌํจํ๋ ์ปดํฌ๋ํธ์ด๋ค.
๊ฐ๋จํ ์คํ์ผ์ ์ ์ฉํ์์ง๋ง ๋ง์ฝ styled-component๋ css ์ ๋ํด์ ์์ง ๋ชจ๋ฅธ๋ค๋ฉด ์คํ์ผ์ ๊ทธ๋ฅ ๋์ด๊ฐ๊ฑฐ๋ ๋ณต์ฌํด์ ์ฌ์ฉํด๋ ์ข๋ค.
InputForm.js & InputForm.styles.js
ํด๋น ํ์ผ์์๋ TodoItem ์ ์์ฑํ๋ ์์ฑ ํผ ์ ๊ตฌํํ ๊ฒ์ด๋ค.
// InputForm.js
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import * as S from "./InputForm.styles";
import { add_todo } from "../commons/actions";
const InputForm = () => {
const dispatch = useDispatch();
const [text, setText] = useState("");
const handleChange = (e) => {
const { value } = e.target;
setText(value);
};
const handleClick = () => {
const todo = {
title: text,
isComplete: false,
};
dispatch(add_todo(todo));
setText("");
};
const handleKeyPress = (e) => {
if (e.key === "Enter") {
handleClick();
}
};
return (
<S.Container>
<S.InputBox
type="text"
placeholder="ํ ์ผ์ ์
๋ ฅํ์ธ์!!"
onChange={handleChange}
value={text}
onKeyDown={handleKeyPress}
/>
<S.Button onClick={handleClick}>์ถ๊ฐ ํ๊ธฐ</S.Button>
</S.Container>
);
};
export default InputForm;
// InputForm.styles.js
import styled from "styled-components";
export const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
export const InputBox = styled.input`
width: 295px;
height: 40px;
margin: 10px 5px;
border-radius: 15px;
font-size: 1.2rem;
background: white;
padding: 5px 25px;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.34);
`;
export const Button = styled.button`
width: 100px;
height: 50px;
font-size: 1.2rem;
background: white;
border: none;
border-radius: 15px;
color: #20c997;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.34);
cursor: pointer;
`;
InputForm ์์๋ ํ ์คํธ ํ๋๊ฐ ๋ณ๊ฒฝ๋ ๋, ์ถ๊ฐ ๋ฒํผ์ ํด๋ฆญํ์ ๋ state๋ฅผ ์กฐ์ํ๋ค.
์ฌ๊ธฐ์ ๋์ฌ๊ฒจ ๋ด์ผ ํ ๊ฒ์ด ์ฐ๋ฆฌ๊ฐ ๋ฆฌ๋์ค๋ฅผ ์ฌ์ฉํ๋ค๊ณ ํด์ ๋ชจ๋ state๋ฅผ ๋ฆฌ๋์ค๋ก ๊ด๋ฆฌํ ํ์๊ฐ ์๋ค.
๊ฐ๋จํ state ๊ฐ์ ๊ฒฝ์ฐ์๋ ๊ทธ๋ฅ ๋ฆฌ์กํธ ํ ์ธ useState() ๋ง ์ฌ์ฉํด๋ ์ถฉ๋ถํ๊ณ global ํ๊ฒ ์ฌ์ฉ๋ ์ฌ์ง๊ฐ ์๋ todo ๋ ๋ฆฌ๋์ค๋ก ๊ด๋ฆฌํ๋๋ก ํ๋ค.
TodoItem.js & TodoItem.styles.js
์ฐ๋ฆฌ๊ฐ ์์์ reducer ์์ ์ ์ํ state ์ ์กด์ฌํ๋ todos๋ฅผ ๋ ๋๋ง ํ ui ์ปดํฌ๋ํธ์ด๋ค.
// TodoItem.js
import React from "react";
import { useDispatch } from "react-redux";
import * as S from "./TodoItem.styles";
import { delete_todo } from "../commons/actions";
const TodoItem = ({ todo }) => {
const dispatch = useDispatch();
const { id, title, isComplete } = todo;
const handleClick = () => {
dispatch(delete_todo(id));
};
return (
<S.Container>
<S.TextColumn>
<div>
<S.Text>{title}</S.Text>
</div>
<S.X onClick={handleClick}>{isComplete || "X"}</S.X>
</S.TextColumn>
</S.Container>
);
};
export default TodoItem;
// TodoItem.styles.js
import styled from "styled-components";
export const Container = styled.div`
margin: 10px 10px;
padding: 10px 10px;
display: flex;
justify-content: flex-start;
align-items: center;
border-radius: 4px;
`;
export const TextColumn = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
`;
export const Text = styled.span`
margin: 0 8px;
`;
export const X = styled.button`
color: red;
border: none;
background: white;
font-size: 1rem;
cursor: pointer;
`;
์ฌ๊ธฐ์๋ todo item ์ ์ญ์ , ์ฆ ํด๋น todo๋ฅผ ์๋ฃํ์ ๊ฒฝ์ฐ๋ฅผ ์ํด์ DELETE action์ dispatch ํ๋ค.
์ด๋ฅผ ์ํด์ ์์ ๋ฐฐ์ด useDispatch()๋ฅผ ์ฌ์ฉํ์๋ค.
TodoList.js & TodoList.styles.js
TodoItem ์ ๋ ๋๋งํ๋ ์์ ์ปดํฌ๋ํธ ๊ฒฉ์ธ TodoList ์ปดํฌ๋ํธ์ด๋ค.
// TodoList.js
import React from "react";
import * as S from "./TodoList.styles.js";
import TodoItem from "./TodoItem.js";
import { useSelector } from "react-redux";
const TodoList = () => {
const todos = useSelector((state) => state.todos);
return (
<S.Container>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</S.Container>
);
};
export default TodoList;
// TodoList.styles.js
import styled from "styled-components";
export const Container = styled.div`
width: 400px;
padding: 30px;
display: flex;
flex-direction: column;
background: white;
border-radius: 15px;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.34);
`;
ํด๋น ์ปดํฌ๋ํธ์์๋ reducer ์ ์กด์ฌํ๋ state์ธ todos๋ฅผ ์ด์ฉํด TodoItem ์ ๋ ๋๋งํ ์์ ์ปดํฌ๋ํธ์ด๋ค.
useSelector ๋ฅผ ์ด์ฉํด์ state ๋ฅผ ๊ฐ์ ธ์จ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์ด๋ ๊ฒ ์ค๋์ Redux๋ฅผ ์ด์ฉํด์ TodoList๋ฅผ ๊ฐ๋จํ๊ฒ ๊ตฌํํด๋ณด์๋ค.
์์ผ๋ก Redux ๊ด๋ จ๋ ๋ฆฌํฉํ ๋ง์ด๋ ์ถ๊ฐ ๊ฐ๋ ๋ค์ ๊ธฐ๋ณธ base๊ฐ ๋ ๊ฒ์๋ฌผ์ด๋ ๋ ์์๋ณด๊ณ ์ถ๋ค๋ฉด ํด๋น ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์ฐธ๊ณ ํด๋ ์ข์ ๊ฒ ๊ฐ๋ค.
๋๊ธ