Redux๋?
๊ฐ๋จํ๊ฒ ๋งํ์๋ฉด Redux๋ state๋ฅผ ๊ด๋ฆฌํ๋ Tool์ด๋ค.
๊ทธ๋ ๋ค๋ฉด state๋ ๋ฌด์์ผ๊น??
State
-
์์ ์ด ๋ค๊ณ ์๋ ๊ฐ์ ๋งํ๋ค.
-
์ฝ๊ธฐ์ ์ฉ์ธ props์ ๋น๊ตํด๋ณด์๋ฉด, ์ฐ๊ธฐ์ ์ฉ์ด๋ผ๊ณ ๋ณผ์ ์๋ค.
-
๋ถ๋ชจ์ปดํฌ๋ํธ์์ ์์์ปดํฌ๋ํธ๋ก data๋ฅผ ๋ณด๋ด๋ ๊ฒ์ด ์๋ component์์์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ค๋ฉด state๋ฅผ ์ฌ์ฉํด์ผํ๋ค.
ex) ๊ฒ์ ์ฐฝ์ ๊ธ์ ์
๋ ฅํ ๋ ๊ธ์ด ๋ณํ๋ ๊ฒ์ state๋ฅผ ๋ฐ๊ฟ
-
State๋ props์ ๋ค๋ฅด๊ฒ mutableํ๋ค.
-
State๊ฐ ๋ณํ๋ฉด re-render๋๋ค.
1
2
3
4
5
|
state = {
message : '',
attachFile : undefined,
openMenu : false
}
|
Props
- properties์ ์ค์๋ง
- ๋ถ๋ชจ ์ปดํฌ๋ํธ๊ฐ ์์ ์ปดํฌ๋ํธํํ
์ ๋ฌํ๋ ๋ฐ์ดํฐ๋ก, (์์ ์
์ฅ์์) ์ฝ๊ธฐ ์ ์ฉ์ด๋ค.
- flow๊ฐ ๋ถ๋ชจ ์ปดํฌ๋ํธ์์ ์์์ปดํฌ๋ํธ์ ์ ๋ฌํ๋ flow์(๋ฐ๋ ๋ถ๊ฐ)
- ๋ถ๋ชจ์์ ์์์๊ฒ 1์ด๋ผ๋ ๊ฐ์ ๋์ ธ์ฃผ๋ฉด ์ด 1๋ immutable์
1
2
3
4
5
|
//์์ ์ปดํฌ๋ํธ
<ChatMessages
messages={messages}
currentMember={member}
/>
|
Redux๋ฅผ ์ฌ์ฉํ๋ ์ด์ …

A component์์ ์ฌ์ฉํ๋ comment๋ผ๋ ์ ๋ณด๋ฅผ B์ปดํฌ๋ํธ์์ ์ฌ์ฉํ๊ณ ์ ํ๋ค๋ฉด component๋ฅผ ํ๊ณ ํ๊ณ ํด์ ์ ๋ณด๋ฅผ ๋ฐ์์ผํ๋ค.
ํ์ง๋ง ๋ฆฌ๋์ค๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด store์ ์ ์ฅํ๊ณ ํ์ํ ๊ณณ์์ ๊บผ๋ด ์ฐ๋ฉด ๋๋ ๊ฒ์ด๋ค.
Redux Data Flow

Redux๋ ์์ ๊ฐ์ flow๋ก ๋์ํ๋ค.
ํน์ง์ ๋จ๋ฐฉํฅ์ผ๋ก data flow๊ฐ ์งํ๋๋ค๋ ๊ฒ์ด๋ค.
Redux ์ธํ
ํ๊ธฐ
Client ํด๋๋ก ์ด๋ํ์ฌ ๋ค์ dependency๋ฅผ ์ค์นํ๋ค.
npm install redux react-redux redux-promise redux-thunk --save
Redux-promise, Redux-thunk
- Redux๋ฅผ ์ ์ธ์ ์๊ฒ ๋์์ฃผ๋ middleware์ด๋ค.
- ๊ธฐ๋ณธ์ ์ธ Redux Store๋ ๋ฐ๋์ ๊ฐ์ฒด ํ์์ผ๋ก ๋ action๋ง์ ๋ฐ์ ์ ์๋ค.
- ํ์ง๋ง ํญ์ plain object ํํ๋ก ์ค๋ ๊ฒ์ด ์๋๋ค.(Promiseํํ๋ก ์ฌ์ ๋ ์๊ณ , Function ํํ๋ก ์ฌ ์๋ ์์)
- ๋ฐ๋ผ์ ์ ๋ dependency๋ ์ ๋๊ฐ์ง ํํ๋ฅผ ๋ฐ์์ ์๊ฒ ํด์ฃผ๋ ๊ฒ
- Redux-promise โ promise
- Redux-thunk โ function
Redux ์ฐ๊ฒฐํ๊ธฐ
์์์ Redux๋ฅผ ์ค์นํด์คฌ๋ค๋ฉด, Redux๋ฅผ ์ฐ๊ฒฐํ๋ ์์
๋ํ ํ์ํ๋ค.
client์ index.js๋ก ์ด๋ํ์ฌ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'antd/dist/antd.css'
import {Provider} from "react-redux";
import {applyMiddleware, createStore} from "redux";
import promiseMiddleware from 'redux-promise'
import ReduxThunk from 'redux-thunk'
import Reducer from './_reducers/index'
//middleware๊ฐ ๋น ์ง๋ค๋ฉด createStore๋ง ๋ฃ์ผ๋ฉด ๋์ง๋ง, Promise์ Function๊น์ง Store์์ ํ์ฉํด์ค์ผํ๊ธฐ๋๋ฌธ์ ์๋์ ๊ฐ์ด ์ค์ ํด์ค๋ค.
const createStoreWithMiddleware = applyMiddleware(promiseMiddleware, ReduxThunk)(createStore)
ReactDOM.render(
//Provider๋ก App์ ๊ฐ์ธ๋ฉด ์ฐ๊ฒฐํด์ฃผ๋ ์์
์ด ๋๋๋ค.
//๋ฐ๋์ Store๋ฅผ ์ค์ ํด์ค์ผํ๋๋ฐ ์์์ ๋ง๋ createStoreWithMiddleware์์ Reducer์ extension์ ๋ฃ์ด์ค๋ค.
<Provider
store={createStoreWithMiddleware(Reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)}
>
<App/>
</Provider>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
|
combineReducer ์ค์
reducer๋ฅผ ๋ง๋ค๊ฒ ๋๋ฉด ์ฌ๋ฌ๊ฐ๊ฐ ์๊ธธ ๊ฒ์ด๊ณ ์ด ๋ง๋ค์ด์ง reducer๋ฅผ ๊ด๋ฆฌํด์ค์ผํ๋ค.
์ด๋ combineReducer๋ฅผ ์ฌ์ฉํ์ฌ ๊ด๋ฆฌํ๋ค.
reducer ํ์ผ์ importํด์ ๋ฃ์ด์ฃผ๋ฉด ๋๋ค.
1
2
3
4
5
6
7
8
|
import {combineReducers} from "redux";
import user from './user_reducer'
const rootReducer = combineReducers({
user
})
export default rootReducer;
|
Action
- ๋ฌด์์ด ์ผ์ด๋ฌ๋์ง ์ค๋ช
ํ๋ ๊ฐ์ฒด
- ์ํ๋ฅผ ์๋ ค์ค๋ค….
1
2
3
4
5
6
7
8
9
|
{
type : '์ก์
์ ์ข
๋ฅ๋ฅผ ํ๋ฒ์ ์๋ณํ ์ ์๋ ๋ฌธ์์ด ํน์ ์ฌ๋ณผ',
payload : '์ก์
์ ์คํ์ ํ์ํ ์์์ ๋ฐ์ดํฐ'
}
//42๋ฒ ๊ฒ์๋ฌผ์ ์ข์์ ๋ฒํผ์ ๋๋ ๋ค..
{type : 'LIKE_ARTICLE', articleId : 42}
//3๋ฒ id, ์ด๋ฆ์ด Mary์ธ ์ฌ๋์ด Fetch_user_success ํ๋ค.
{type : 'FETCH_USER_SUCCESS', response : {id : 3, name : 'Mary'}}
|
- Store์ ์๋ State๋ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ๊ฐ ์ ๊ทผํ ์ ์๋ค. ๋ฐ๋์ Action์ ํตํด์ ์ ๊ทผํด์ผํจ
- Store์ ๋ญ๊ฐ ํ๊ณ ์ถ์ ๊ฒฝ์ฐ Action์ ๋ฐํ
- Store์ ๋ฌธ์ง๊ธฐ๊ฐ Action์ ๋ฐ์์ ๊ฐ์งํ๋ฉด, State๊ฐ ๊ฐฑ์ ๋๋ค.
Reducer
- state๊ฐ ์ด๋ป๊ฒ ๋ณํ ์ง ๋ฌ์ฌํ๋ function์ด๋ค.
- ์ด์ ์ํ์ action์ ํฉ์ณ ์๋ก์ด state๋ฅผ ๋ง๋๋ ์กฐ์์ด๋ค.
- ์ด์ state์ action object๋ฅผ ๋ฐ์ ํ next state๋ฅผ returnํ๋ค.
(previousState, action) => nextState
Store
- ์ฌ๋ฌ๊ฐ์ง State๋ฅผ ๊ฐ์ธ๊ณ ์๋ ์ญํ ์ ํ๋ค.
- State๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฌ๊ธฐ์ ์ง์ค๊ด๋ฆฌ ๋๋ค. ์ปค๋ค๋ JSON์ ๊ฒฐ์ ์ฒด ์ ๋์ ์ด๋ฏธ์ง์ด๋ค.
- ๋ค์์ STORE์ ์์ ์ด๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
{
// ์ธ์
๊ณผ ๊ด๋ จ๋ ๊ฒ
session: {
loggedIn: true,
user: {
id: "114514",
screenName: "@mpyw",
},
},
// ํ์์ค์ธ ํ์๋ผ์ธ์ ๊ด๋ จ๋ ๊ฒ
timeline: {
type: "home",
statuses: [
{id: 1, screenName: "@mpyw", text: "hello"},
{id: 2, screenName: "@mpyw", text: "bye"},
],
},
// ์๋ฆผ๊ณผ ๊ด๋ จ๋ ๊ฒ
notification: [],
}
|
Store์ ์๋ ๊ฐ ํ์ฉํ๊ธฐ
Store์ ์ ์ฅ๋์ด ์๋ ๊ฐ์ ๊ฐ์ ธ์ค๋ ค๋ฉด Hook์ ์ฌ์ฉํด์ผ ํ๋ค.
1
2
3
|
import { useSelector } from 'react-redux';
const { id, text } = useSelector((state: RootState) => state.reducer1);
|
์ฐธ๊ณ ์๋ฃ : https://react-redux.js.org/api/hooks
Redux ํ์ฉ์์
Dispatch(action)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
import React, {useState} from 'react';
import {Button, Descriptions} from "antd";
import {useDispatch} from "react-redux";
import {addToCart} from "../../../../_actions/user_actions";
function ProductInfo(props) {
const dispatch = useDispatch();
const clickHandler = () => {
//ํ์ํ ์ ๋ณด๋ฅผ Cart ํ๋์๋ค๊ฐ ๋ฃ์ด์ค๋ค.
dispatch(addToCart(props.detail._id))
}
return (
<div>
<Descriptions title="Product Info">
<Descriptions.Item label="Price">{props.detail.price}</Descriptions.Item>
<Descriptions.Item label="Sold">{props.detail.sold}</Descriptions.Item>
<Descriptions.Item label="View">{props.detail.views}</Descriptions.Item>
<Descriptions.Item label="Description">{props.detail.description}</Descriptions.Item>
</Descriptions>
<br/>
<br/>
<div style={{display: 'flex', justifyContent: "center"}}>
<Button size="large" shape='round' type='danger' onClick={clickHandler}>
Add to Cart
</Button>
</div>
</div>
);
}
export default ProductInfo;
|
Action
1
2
3
4
5
6
7
8
9
10
11
12
13
|
export function addToCart(id){
let body = {
productId : id
}
const request = axios.post(`${USER_SERVER}/addToCart`,body)
.then(response => response.data);
return {
type: ADD_TO_CART,
payload: request
}
}
|
Node.js(๋ฐฑ๋จ)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
router.post("/addToCart", auth, (req, res) => {
//๋จผ์ User Collection์ ํด๋น ์ ์ ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ธฐ
//auth ๋๋ฌธ์ req.user._id๋ฅผ ๊ฐ์ ธ์ฌ์ ์๋ค.
User.findOne({_id: req.user._id},
(err, userInfo) => {
//๊ฐ์ ธ์จ ์ ๋ณด์์ ์นดํธ์๋ค ๋ฃ์ผ๋ ค ํ๋ ์ํ์ด ์ด๋ฏธ ๋ค์ด ์๋์ง ํ์ธ
let duplicate = false;
userInfo.cart.forEach((item) => {
if (item.id === req.body.productId) {
duplicate = true
}
})
if (duplicate) {
//์ํ์ด ์ด๋ฏธ ์์๋
//๋ด๋ถ ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ ๋๋ ๋ค์๊ณผ ๊ฐ์ด ''๋ก ์ฌ์ฉํ์ฌ ๋ฌธ์๋ก ๋ง๋ค์ด์ค์ผํ๋ค.
//{new: true} ๋ update๋ ๊ฒฐ๊ณผ๊ฐ์ ๋ฐ๊ธฐ์ํด ์ค์ ํด์ฃผ๋ ์ต์
์ด๋ค.
User.findOneAndUpdate({_id: req.user._id, 'cart.id': req.body.productId},
{$inc: {'cart.$.quantity': 1}},
{new: true},
(err, userInfo) => {
if (err) return res.status(400).json({success: false, err})
return res.status(200).send(userInfo.cart)
})
} else {
//์ํ์ด ์ด๋ฏธ ์์ง ์์๋๋
User.findOneAndUpdate({_id: req.user._id},
{
$push: {
cart: {
id: req.body.productId,
quantity: 1,
date: Date.now()
}
}
},
{new: true},
(err, userInfo) => {
if (err) return res.status(400).json({success: false, err})
return res.status(200).send(userInfo.cart)
}
)
}
})
});
|
Reducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
import {
LOGIN_USER,
REGISTER_USER,
AUTH_USER,
LOGOUT_USER, ADD_TO_CART,
} from '../_actions/types';
export default function (state = {}, action) {
switch (action.type) {
case REGISTER_USER:
return {...state, register: action.payload}
case LOGIN_USER:
return {...state, loginSucces: action.payload}
case AUTH_USER:
return {...state, userData: action.payload}
case LOGOUT_USER:
return {...state}
case ADD_TO_CART:
//์ด๋ ๊ฒ ํด์ฃผ๋ ์ด์ ๋ redux ๊ธฐ์กด ์ ๋ณด์ cart ์ ๋ณด๋ฅผ ์ถ๊ฐํด์ฃผ๊ธฐ ์ํด์์ด๋ค.
return {
...state, userData: {
...state.userData,
//์์ชฝ ์ฆ action -> ๋ฐฑ๋จ ๋ฆฌํด๊ฐ์ด action.payload์ด๋ค.
cart: action.payload
}
}
default:
return state;
}
}
|