์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ™œ์„ฑํ™” ํ•ด์ฃผ์„ธ์š”

Redux

 ·  โ˜• 6 min read · ๐Ÿ‘€... ์กฐํšŒ์ˆ˜

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๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ …

image
A component์—์„œ ์‚ฌ์šฉํ•˜๋Š” comment๋ผ๋Š” ์ •๋ณด๋ฅผ B์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด component๋ฅผ ํƒ€๊ณ  ํƒ€๊ณ  ํ•ด์„œ ์ •๋ณด๋ฅผ ๋ฐ›์•„์•ผํ•œ๋‹ค.
ํ•˜์ง€๋งŒ ๋ฆฌ๋•์Šค๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด store์— ์ €์žฅํ•˜๊ณ  ํ•„์š”ํ•œ ๊ณณ์—์„œ ๊บผ๋‚ด ์“ฐ๋ฉด ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

Redux Data Flow

image
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์„ ํ†ตํ•ด์„œ ์ ‘๊ทผํ•ด์•ผํ•จ
    1. Store์— ๋ญ”๊ฐ€ ํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ Action์„ ๋ฐœํ–‰
    2. 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;
    }
}
๊ณต์œ ํ•˜๊ธฐ

brinst
๊ธ€์“ด์ด
brinst
Backend Developer

๋ชฉ์ฐจ