Redux

MINIMI·2023년 4월 25일
0

REACT

목록 보기
20/20
post-thumbnail

20-1. Basic

1. Intro

  • 리덕스는 상태관리를 하기 위한 라이브러리
  • 앱의 상태는 전부 하나의 저장소(store) 안에 있는 객체 트리에 저장
  • 상태 트리를 변경하는 유일한 방법은 어떤 행동이 일어날지에 해당하는 action
  • action에 따라 상태를 어떻게 변경할지를 명시하기 위한 함수를 reducer 함수라 함

2. 리덕스의 3가지 원칙

  • single source of truth
    • 스토어라는 하나뿐인 데이터 공간을 이용하기 때문에 신뢰할 수 있는 데이터
  • State is read-only
    • 액션을 전달하는 리듀서 함수를 이용해서만 상태를 변경하며 직접 state를 변경 할 수 없음
  • Changes are made with pure function
    • 변경은 오로지 순수 함수로만 가능(리듀서 이용)

3. Reducer 함수 작성

  • (state, action) => state 형태의 순수 함수 형태로 리듀서 함수 작성
  • switch 문 말고도 어떤 형태로든 팀 내 컨벤션에 맞게 작성
  • 주의 사항
    • state 객체는 변경해서 안되고 상태가 바뀌면 반드시 새로운 객체를 생성해서 반환
    • 일반적으로 spread 연산자 이용 가능
const { createStore } = Redux;

function counter(state = 0, action) {
        /* action은 스토어에 운반할 데이터를 말하며 주문서와 비슷한 개념이다. 
        {
            type : 'ACTION_NAME',                       //필수
            payload : { name : '홍길동', age : 20 }     //옵션
        }
        */
        switch(action.type) {
            case 'INCREMENT' : 
                return state + 1;
            case 'DECREMENT' : 
                return state - 1;
            default :
                return state;
        }

       }

       /* 리덕스 저장소 생성 - 앱의 상태를 보관
       통상 store라고 하며 store가 제공하는 api는 subscribe, dispatch, getState가 있다. */
       const store = createStore(counter);

       /* 스토어의 상태 변화를 구독할 함수를 콜백 형태로 전달한다. 
       리액트와 연동하는 react redux에서는 직접 subscribe를 호출하지는 않는다.
       */
      store.subscribe(() => console.log(store.getState()));

       /* dispatch는 리듀서 함수를 호출하며, state는 현재 상태를 리듀서가 넣어서 호출하게 된다. 
       dispatch를 이용해 action을 넣어 호출하게 되면 리듀서 함수의 두 번째 인자로 값을 전달하게 된다. 
       state를 변경하는 유일한 방법은 액션을 보내는 것 뿐이다. */
       store.dispatch({ type : 'INCREMENT' });
       store.dispatch({ type : 'INCREMENT' });
       store.dispatch({ type : 'DECREMENT' });

4. Provider

React에서 Provider는 상태 관리 라이브러리인 Redux나 MobX와 같은 라이브러리에서 주로 사용되는 용어입니다. 이러한 라이브러리는 전역 상태를 관리하며, 애플리케이션의 모든 컴포넌트에서 이 상태를 사용할 수 있도록 하는 기능을 제공합니다.

Provider는 이러한 전역 상태를 하위 컴포넌트에 전달하는 데 사용됩니다. Provider 컴포넌트는 하위 컴포넌트에 대한 컨텍스트를 생성하고, 이 컨텍스트를 통해 전역 상태를 하위 컴포넌트에 전달합니다.

예를 들어, Redux에서 Provider를 사용하여 전역 상태를 애플리케이션의 모든 컴포넌트에 전달할 수 있습니다. 다음은 Redux Provider를 사용하는 간단한 예시입니다.

import React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers';
import App from './App';

const store = createStore(rootReducer);

function Root() {
  return (
    <Provider store={store}>
      <App />
    </Provider>
  );
}

export default Root;

위 코드에서 Provider 컴포넌트는 Redux store를 store prop으로 전달하고, App 컴포넌트를 하위로 렌더링합니다. 이로써 App 컴포넌트와 하위 컴포넌트에서 Redux store를 사용할 수 있게 됩니다.

5. Read Redux

 const initioalState  = 0;
        const {createStore} = Redux;
        const { Provider, useSelector, useDispatch } = ReactRedux;

        /* state를 변경하기 위한 리듀서 함수 정의 */
        function reducer(state = initioalState, action){
            /* action은 dispatch를 호출하는 쪽에서 전달해주는 객체로
            행위의 종류(type)와 state 변경에 대한 내용(payload)를 담고 있다. */
            const {payload} = action;

            switch(action.type){
                case 'INCREMENT':
                    return state + payload.incrementValue;
                case 'DECREMENT':
                    return state - payload.decrementValue;
                default:
                    return state;
            }
        }

        /* 리듀서 함수를 이용해서 스토어 생성 */
        const store = createStore(reducer);

        function App(){

            /* useSelect hook을 이용하여 state를 이용할 수 있다.
            useSelector는 반드시 인자로 state를 매개변수로 하는 함수를 콜백으로 전달해야 한다. */
            const count = useSelector((state) => state);
            
            /* useDispatch hook을 사용하여 dispatch를 호출할 수 있다. */
            const dispatch = useDispatch();

            const increaseCount = () => {
                dispatch({
                    type : 'INCREMENT',
                    payload : {
                        incrementValue : 1
                    }
                });
            };


            const decreseCount = () => {
                dispatch({
                    type : 'DECREMENT',
                    payload : {
                        decrementValue : 1
                    }
                })
            }

            return (
                <>
                    <h1>Count : { count }</h1>
                    <button onClick={increaseCount}>1 증가</button>
                    <button onClick={decreseCount}>1 감소</button>
                </>
            )
        }

        /* Provider로 store를 props 형태로 전달하면 하위 컴포넌트는 store를 자동으로 구독하게 된다. 
        즉, state가 변경 되면 랜더링이 다시 일어난다는 의미. */
        ReactDOM.createRoot(document.getElementById('root')).render(
            <Provider store={store}>
                <App/>
            </Provider>);

6. useDispatch

React에서 dispatch는 Redux나 다른 상태 관리 라이브러리에서 사용되는 용어입니다. dispatch는 상태 변경을 알리는 역할을 합니다.

Redux에서 dispatch는 store 객체의 메서드 중 하나이며, action 객체를 인수로 받습니다. dispatch 메서드를 호출하면, action 객체가 Redux의 reducer 함수로 전달되어 상태를 변경합니다. 이 때, reducer 함수는 현재 상태와 전달받은 action 객체를 기반으로 새로운 상태를 계산합니다.

예를 들어, 다음은 Redux에서 dispatch를 사용하는 예시입니다.

import React from 'react';
import { useDispatch } from 'react-redux';
import { increment } from './actions';

function Counter() {
  const dispatch = useDispatch();

  const handleClick = () => {
    dispatch(increment());
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

export default Counter;

위 코드에서 useDispatch 훅을 사용하여 dispatch 함수를 가져와서, increment 액션 생성자 함수를 호출합니다. 이렇게 하면 increment 액션 객체가 생성되고, 이를 dispatch 함수에 전달하여 상태를 변경합니다.

7. Combine Reducer

/* 관리 대상 state가 복잡하거나 reducer 함수로 관리해야 할 성격이 다른 경우 combine reducer를 이용할 수 있다. */
        const { Provider, useSelector, useDispatch } = ReactRedux;
        const { combineReducers, createStore } = Redux;

        /* 관리 해야 할 상태들을 설정한다. */
        const countInitState = {
            currentCount : 0
        };

        const activationInitState = {
            isActivity : false
        };

        const userInitState = {
            name : '',
            email : '',
            phone : ''
        };

        /* 여러 리듀서 함수를 하나의 rootReducer로 묶을 때 combineReducers를 이용한다. 
        서로 관련 있는 상태 변경에 대한 로직을 각 함수로 분리하면서 관리가 수월해진다. */
        const rootReducer = combineReducers({
            countReducer : (state = countInitState, action) => {
                const { type, payload } = action;

                switch(type) {
                    case 'INCREMENT' :
                        return {
                            currentCount : state.currentCount + payload.incrementValue
                        };
                    case 'DECREMENT' :
                        return {
                            currentCount : state.currentCount - payload.decrementValue
                        };
                    default : 
                        return state;
                }
            },
            activationReducer : (state = activationInitState, action) => {
                const { type } = action;

                switch(type) {
                    case 'TOGGLE' : 
                        return {
                            isActivity : !state.isActivity
                        };
                    default : 
                        return state;
                }
            },
            userReducer : (state = userInitState, action) => {
                const { type, payload } = action;

                switch(type) {
                    case 'INPUT' : 
                        return {
                            ...state,
                            [payload.name] : payload.value
                        };
                    default : 
                        return state;
                }
            }
        });
        
        /* store 생성 시 reducer는 하나만 사용할 수 있기 때문에 combineReducers를 이용한 rootReducer를 전달한다. */
        const store = createStore(rootReducer,
        /* 크롬 확장 프로그램 redux devtool 설치 후 적용하기 위해 작성 */
        window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
        );

        function App() {

            /* combineReducers를 이용하는 경우 useSelector의 콜백함수의 매개변수로 rootReducer의 state가 전달 된다. 
            해당 state에서 reducer 설정한 key 값을 통해 사용하고자 하는 state 값만 가져올 수 있다. */
            const { currentCount } = useSelector(state => state.countReducer);
            const { isActivity } = useSelector(state => state.activationReducer);
            const { name, email, phone } = useSelector(state => state.userReducer);

            /* useDispatch hook을 이용하여 dispatch를 호출할 수 있다. */
            const dispatch = useDispatch();

            const increaseCount = () => {
                dispatch({
                    type : 'INCREMENT',
                    payload : {
                        incrementValue : 1
                    }
                });
            };

            const decreaseCount = () => {
                dispatch({
                    type : 'DECREMENT',
                    payload : {
                        decrementValue : 1
                    }
                });
            };

            const toggleActivation = () => {
                dispatch({type : 'TOGGLE'});
            };

            const onChangeHandler = (e) => {
                dispatch({
                    type : 'INPUT',
                    payload : {
                        name : e.target.name,
                        value : e.target.value
                    }
                });
            };

            return (
                <>
                    <h1>currentCount : { currentCount } </h1>
                    <button onClick={ increaseCount }>1 증가</button>
                    <button onClick={ decreaseCount }>1 감소</button>
                    <h1>isActivity : { isActivity.toString() }</h1>
                    <button onClick={ toggleActivation }>toggle activation</button>
                    <h1>input control</h1>
                    <label>name : </label>
                    <input type='text' name='name' value={ name } onChange={ onChangeHandler }/>
                    <label>email : </label>
                    <input type='text' name='email' value={ email } onChange={ onChangeHandler }/>
                    <label>phone : </label>
                    <input type='text' name='phone' value={ phone } onChange={ onChangeHandler }/>
                    <h3>name : { name }</h3>
                    <h3>email : { email }</h3>
                    <h3>phone : { phone }</h3>
                </>
            );
        }

        ReactDOM.createRoot(document.getElementById('root')).render(
            <Provider store={ store }>
                <App/>
            </Provider>
        );

7. Action Function

const { Provider, useSelector, useDispatch } = ReactRedux;
        const {createStore} = Redux;

        /* Ducks 패턴에 의하면 초기값, 액션, 리듀서를 관련 있는 state 별로 modules 폴더에 만들어 관리한다. */

        /* 초기값 */
        const initialState = 0;

        /* 액션 */
        /* 액션을 함수 형태로 재사용할 수 있도록 작성하여 dispatch 호출 시 인자로 전달할 값을 반환하는 함수를 만들어 둔다. */

        /* 액션 타입을 상수로 정의한다. 접두사는 다른 모듈과 액션 값이 겹치지 않게 하기 위함이다. */
        const INCREMENT = 'count/INCREMENT';
        const DECREMENT = 'count/DECREMENT';

        const increase = () => ({
            type : INCREMENT,
            payload : {
                incrementValue : 1
            }
        });
        const decrease = () => ({
            type : DECREMENT,
            payload : {
                decrementValue : 1
            }
        });

        /* 리듀서 */
        function reducer(state = initialState, action){
            const { payload } = action;

            switch(action.type){
                case INCREMENT :
                    return state + payload.incrementValue;
                case DECREMENT : 
                    return state - payload.decrementValue;
                default :
                    return state;
            }
        }

        /* ------------------------------------------------------------------------------------ */
        function App(){
            const count = useSelector(state => state);
            const dispatch = useDispatch();

            const increaseCount = () => {
                /* 사전에 정의한 액션 함수를 호출하여 반환 받은 액션 객체 값을 dispatch 호출 시 전달
                호출 시 직접 action 객체를 리터럴로 작성하는 것 보다 가독성, 재사용성이 증가한다. */
                dispatch(increase());
            }
            const decreaseCount = () => {
                dispatch(decrease());
            }

            return (
                <>
                    <h1>Count : {count}</h1>
                    <button onClick = {increaseCount}>1 증가</button>
                    <button onClick = {decreaseCount}>1 감소</button>
                </>
            );

        }

        /* 스토어 */
        const store = createStore(reducer);

        ReactDOM.createRoot(document.getElementById("root")).render(
            <Provider store={store}>
                <App/>
            </Provider>
        )

8. Redux Actions

const { Provider, useSelector, useDispatch } = ReactRedux;
        const {createStore} = Redux;

        /* 액션에 해당하는 내용을 매번 함수로 정의하기는 번거롭다.
        redux-actions 모듈에서 제공하는 createAction, createActions 기능을 이용하면 보다 쉽게 액션 함수를 생성할 수 있다. */
        const { createAction, createActions, handleActions } = ReduxActions;

        /* 초기값 */
        const initialState = 0;

        /* 액션 */
        const INCREMENT = 'count/INCREASE';
        const DECREMENT = 'count/DECREASE';

        /* createAction 사용 */
        /* 첫번째 인자 : 타입 / 두 번째 인자 : payload에 담고 싶은 값(return 값을 자동으로 payload 속성 값으로 담는다.) 
        콜백 함수의 매개변수는 dispatch 호출 시 전달 되는 값을 의미하며 이를 통해 payload에 담는 값을 설정할 수 있다.*/
        // const increase = createAction(INCREMENT, (amount = 1) => ({ incrementValue : amount }));
        // const decrease = createAction(DECREMENT, (amount = 1) => ({ decrementValue : amount}));

        /* createActions 사용 */
        /* 여러 개의 액션 함수를 한 번에 생성할 수 있다. 접두사가 있기 때문에 대괄호를 이용해서 키 식별자를 작성한다. 
        전달 하는 객체의 key 값이 action의 type 속성 값이 되고, value 값의 함수가 반환하는 값이 payload의 속성 값이 된다. 
        action type 속성 값에 따라 반환 되는 객체의 키값이 설정 된다. EX) actions.count.increment */
        const{ count : {increase, decrease }} = createActions({
            [INCREMENT] : (amount = 1) => ({ incrementValue : amount }),
            [DECREMENT] : (amount = 1) => ({ decrementValue : amount })
        })

        /* 리듀서 */
        /* 리듀서 함수도 handleActions 기능을 사용하여 간결하게 작성할 수 있다. 
        첫번째 인자로 state 를 전달 받고 두번째 인자로 action 을 전달 받는다. 
        중첩구조분해할당으로 payload 값을 꺼내 콜백 함수 안에서 사용 할 수 있다. 
        dispatch 호출 시 전달 된 action의 type 과 일치하는 함수를 동작시키게 된다. */
        const reducer = handleActions({
            [INCREMENT] : (state, action) => {
                return state + action.payload.incrementValue;
            },
            [DECREMENT] : (state, {payload : {decrementValue}}) => {
                return state - decrementValue
            }
        }, initialState);

        /* ------------------------------------------------------------------------------------ */

        function App(){
            const count = useSelector(state => state);
            const dispatch = useDispatch();

            const increaseCount = () => {
                /* 사전에 정의한 액션 함수를 호출하여 반환 받은 액션 객체 값을 dispatch 호출 시 전달
                호출 시 직접 action 객체를 리터럴로 작성하는 것 보다 가독성, 재사용성이 증가한다. */
                dispatch(increase());
            }
            const decreaseCount = () => {
                dispatch(decrease());
            }

            return (
                <>
                    <h1>Count : {count}</h1>
                    <button onClick = {increaseCount}>1 증가</button>
                    <button onClick = {decreaseCount}>1 감소</button>
                </>
            );

        }

        /* 스토어 */
        const store = createStore(reducer);

        ReactDOM.createRoot(document.getElementById("root")).render(
            <Provider store={store}>
                <App/>
            </Provider>
        )

20-2. Middleware

1. middleware

/* 리덕스 미들웨어

       dispatch(action) --- > middleware --- > reducer(state, action) --- > store 저장
       액션이 디스패치 된 다음 리듀서에서 액션을 받아 업데이트 하기 전에 추가적인 작업을 미들웨어를 통해 수행할 수 있다.
       
       특정 조건에 따라 액션 무시 및 추가 작업, 수정, 로깅, 트리거 액션, 함수 호출 등을 수행하도록 할 수 있다. 
       주로 사용하는 용도는 비동기 작업을 처리할 때이다. (redux-thunk, redux-saga 등이 비동기 관련 미들웨어 라이브러리)

       실제로 미들 웨어를 직접 만들어서 쓸 일은 거의 없지만, 간단한 미들웨어를 만들어 동작하는 순서를 확인해본다. 

       이러한 형태로 미들웨어를 작성하게 된다. 
       const middleware = store => next => action => {
           // 미들웨어 수행 내용
       }

       이러한 의미를 가진다. 
       function middleware(store) {
           return function (next) {
               return function (action) {
                   // 미들웨어 수행 내용
               }
           }
       }
       */

       /* 액션을 출력하는 로그 기능을 가지는 간단한 미들 웨어 */
       const consoleLoggingMiddleware = store => next => action => {

           console.log(action);            // 액션 객체를 출력하는 콘솔 로그 기능
           const result = next(action);    // 다음 미들웨어 또는 리듀서에게 액션을 전달 
           
           return result;  // 반환하는 result는 dispatch(action)의 결과물
       }

       /* ------------------------------------------------------------------------------------------------ */

       /* module */
       const { createActions, handleActions } = ReduxActions;

       /* 초기값 */
       const initialState = 0;

       /* 액션 */
       const INCREMENT = 'count/INCREASE';
       const DECREMENT = 'count/DECREASE';

       const { count : { increase, decrease } } = createActions({
           [INCREMENT] : (amount = 1) => ({ incrementValue : amount }),
           [DECREMENT] : (amount = 1) => ({ decrementValue : amount })
       });

       /* 리듀서 */
       const reducer = handleActions({
           [INCREMENT] : (state, { payload : { incrementValue } }) => {
               return state + incrementValue
           },
           [DECREMENT] : (state, { payload : { decrementValue } }) => {
               return state - decrementValue;
           }
       }, initialState);
       
       /* ---------------------------------------------------------------------------------------------- */

       /* UI */

       const { useSelector, useDispatch } = ReactRedux;

       function App() {

           const count = useSelector(state => state);
           const dispatch = useDispatch();

           const increaseCount = () => {
               dispatch(increase());
           }
           const decreaseCount = () => {
               dispatch(decrease());
           }

           return (
               <>
                   <h1> Count : { count } </h1>
                   <button onClick={ increaseCount }>1 증가</button>
                   <button onClick={ decreaseCount }>1 감소</button>
               </>
           );
       }

       /* ------------------------------------------------------------------------------- */

       /* 스토어 */
       const { Provider } = ReactRedux;
       const { createStore, applyMiddleware, compose } = Redux;

       /* 사용하려는 미들웨어를 두 번째 인자로 applyMiddleware 함수의 인자 형태로 전달한다. */
       //const store = createStore(reducer, applyMiddleware(consoleLoggingMiddleware));

       const store = createStore(
           reducer, 
           compose(
               applyMiddleware(consoleLoggingMiddleware),
               window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
           )
       );

       ReactDOM.createRoot(document.getElementById('root')).render(
           <Provider store={ store }>
               <App/>
           </Provider>
       );

2. logger

  /* 미리 만들어져 있는 미들웨어 사용해보기 (여러개의 미들 웨어 사용하기) 
        로깅 미들웨어로 가장 많이 사용 되는 미들웨어는 redux-logger 미들웨어이다. */

        /* react-logger 미들웨어 */
        const logger = reduxLogger.createLogger();

        /* 커스텀 미들웨어 - 액션을 출력하는 로그 기능을 가지는 간단한 미들 웨어 */
        const consoleLoggingMiddleware = store => next => action => {

            console.log(action);            // 액션 객체를 출력하는 콘솔 로그 기능
            const result = next(action);    // 다음 미들웨어 또는 리듀서에게 액션을 전달 
            
            return result;  // 반환하는 result는 dispatch(action)의 결과물
        }

        /* ------------------------------------------------------------------------------------------------ */

        /* module */
        const { createActions, handleActions } = ReduxActions;

        /* 초기값 */
        const initialState = 0;

        /* 액션 */
        const INCREMENT = 'count/INCREASE';
        const DECREMENT = 'count/DECREASE';

        const { count : { increase, decrease } } = createActions({
            [INCREMENT] : (amount = 1) => ({ incrementValue : amount }),
            [DECREMENT] : (amount = 1) => ({ decrementValue : amount })
        });

        /* 리듀서 */
        const reducer = handleActions({
            [INCREMENT] : (state, { payload : { incrementValue } }) => {
                return state + incrementValue
            },
            [DECREMENT] : (state, { payload : { decrementValue } }) => {
                return state - decrementValue;
            }
        }, initialState);
        
        /* ---------------------------------------------------------------------------------------------- */

        /* UI */

        const { useSelector, useDispatch } = ReactRedux;

        function App() {

            const count = useSelector(state => state);
            const dispatch = useDispatch();

            const increaseCount = () => {
                dispatch(increase());
            }
            const decreaseCount = () => {
                dispatch(decrease());
            }

            return (
                <>
                    <h1> Count : { count } </h1>
                    <button onClick={ increaseCount }>1 증가</button>
                    <button onClick={ decreaseCount }>1 감소</button>
                </>
            );
        }

        /* ------------------------------------------------------------------------------- */

        /* 스토어 */
        const { Provider } = ReactRedux;
        const { createStore, applyMiddleware, compose } = Redux;

        /* 사용하려는 미들웨어를 두 번째 인자로 applyMiddleware 함수의 인자 형태로 전달한다. */
        //const store = createStore(reducer, applyMiddleware(consoleLoggingMiddleware, logger));

        const store = createStore(
            reducer, 
            compose(
                applyMiddleware(consoleLoggingMiddleware, logger),
                window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
            )
        );

        ReactDOM.createRoot(document.getElementById('root')).render(
            <Provider store={ store }>
                <App/>
            </Provider>
        );

3. Fetch Problem

/* Module */
        const { createActions, handleActions } = ReduxActions;

        /* 초기값 */
        const initialState = [];

        /* 액션 */
        const FETCH_DATA = 'FETCH_DATA';

        /* 언더스코어로 연결 된 단어는 카멜케이스로 치환 되어 함수 이름으로 처리 된다. */
        const { fetchData } = createActions({
            [FETCH_DATA] : async () => {

                /* Promise를 이용한 비동기 방식인 fetch 함수를 실행하게 되면 async await 키워드를 이용해서 처리한다. */
                const response = await fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json());

                /* response 결과를 얻어오는데까지는 문제가 없다. */
                console.log('response : ', response);

                /* response를 반환할 때 비동기작업은 아직 실행중이고 결과가 만들어지지 않은 상태인데 값을 리턴하려
                하기 때문에 Promise 객체 자체를 반환해버린다. 
                즉, 리턴은 위의 비동기 흐름과 상관 없이 함수 자체를 종료시켜버리며 컨트롤 할 수 없다. */
                return [...response];
            }
        });

        /* 리듀서 */
        const reducer = handleActions({
            [FETCH_DATA] : (state, { payload }) => {
                /* payload 자체가 Promise 객체가 되어 스프레드 연산을 이용할 수 없다는 오류가 발생한다. */
                return [...payload];
            }
        }, initialState);

        /* ------------------------------------------------------------------------------ */

        /* UI */

        const { useSelector, useDispatch } = ReactRedux;

        function App() {

            const users = useSelector(state => state);
            const dispatch = useDispatch();

            const onClickHandler = () => {
                dispatch(fetchData());
                console.log("users : ", users);
            }

            return (
                <>
                    <h1>회원 목록</h1>
                    <button onClick={onClickHandler}>조회하기</button>
                </>
            );
        }

        /* ------------------------------------------------------------------------------- */

        /* 스토어 */
        const { createStore } = Redux;
        const { Provider } = ReactRedux;

        const store = createStore(reducer);

        ReactDOM.createRoot(document.getElementById('root')).render(
            <Provider store={store}>
                <App/>
            </Provider>
        );

4. Fetch resolve

/* Module */
        const { createActions, handleActions } = ReduxActions;

        /* 초기값 */
        const initialState = [];

        /* 액션 */
        const FETCH_DATA = 'FETCH_DATA';

        const { fetchData } = createActions({
            [FETCH_DATA] : () => {}
        });

        /* 미들웨어 */
        /* action을 매개변수로 하는 콜백 함수를 async로 하여 api 호출 후 next에 action 객체를 다시 생성해서 호출한다. */
        const fetchUser = store => next => async (action) => {

            console.log('action', action);

            const response = await fetch('https://jsonplaceholder.typicode.com/users').then((res) => res.json());

            console.log('response : ', response);

            next({ ...action, payload : response });
        }

        /* 리듀서 */
        const reducer = handleActions({
            [FETCH_DATA] : (state, { payload }) => {
                
                console.log('payload : ', payload);

                return [...payload];
            }
        }, initialState);

        /* ------------------------------------------------------------------------------ */

        /* UI */

        const { useSelector, useDispatch } = ReactRedux;

        function App() {

            const users = useSelector(state => state);
            const dispatch = useDispatch();

            const onClickHandler = () => {
                dispatch(fetchData());
            }

            return (
                <>
                    <h1>회원 목록</h1>
                    <button onClick={onClickHandler}>조회하기</button>
                    <ul>
                        {users.map(user => (<li key={user.id}>{user.name}</li>))}
                    </ul>
                </>
            );
        }

        /* ------------------------------------------------------------------------------- */

        /* 스토어 */
        const { createStore, applyMiddleware } = Redux;
        const { Provider } = ReactRedux;

        const store = createStore(reducer, applyMiddleware(fetchUser));

        ReactDOM.createRoot(document.getElementById('root')).render(
            <Provider store={store}>
                <App/>
            </Provider>
        );

5. 미들웨어(thunk)

const { createAction, createActions, handleActions } = ReduxActions;

      /* 초기값 */
      const initialState = [];

      /* 액션 */
      const FETCH_DATA = 'FETCH_DATA';

      /* 미들웨어 */
      /* action을 매개변수로 하는 콜백 함수를 async로 하여 api 호출 후 next에 action객체를 다시 생성해서 호출한다.
       * 동작 원리를 알기 위해 작성해 보았고 실제 react-thunk 로직과 거의 동일하다.
       * 하지만 브라우저에서 지원되지 않는 문법을 이용하여 작성되어서 현재 환경에서는 테스트 불가하고
       * CRA환경에서는 npm을 이용해 install 후 사용할 것이다.
       */
      const thunkMiddleware =
        ({ dispatch, getState }) =>
        (next) =>
        (action) => {
          /* dispatch 시 함수가 전달되면 전달된 콜백함수를 호출하며 dispatch와 getState를 전달한다. */
          if (typeof action === 'function') {
            return action(dispatch, getState);
          }

          /* 함수가 아닌 경우 원래 흐름대로 아무 작업을 하지 않고 다음 미들웨어 혹은 리듀서를 호출한다. */
          return next(action);
        };

      /* 콜백으로 사용할 비동기 함수(API 로직 작성) */
      const fetchUser = async (dispatch, getState) => {
        const response = await fetch('https://jsonplaceholder.typicode.com/users').then((res) => res.json());

        console.log('response : ', response);

        dispatch({ type: FETCH_DATA, payload: response });
      };

      /* 리듀서 */
      const reducer = handleActions(
        {
          [FETCH_DATA]: (state, { payload }) => {
            /* next()를 이용해 전달한 페이로드가 잘 전달되게 된다. */
            console.log('payload : ', payload);

            return payload;
          },
        },
        initialState
      );

      /* UI */
      const { useSelector, useDispatch } = ReactRedux;

      function App() {
        const users = useSelector((state) => state);
        const dispatch = useDispatch();

        console.log('users : ', users);

        const onClickHandler = () => {
          /* dispatch 호출 시 인자로 API 호출을 비동기로 처리하는 콜백 함수를 인자로 넘긴다. */
          dispatch(fetchUser);
        };

        return (
          <>
            <h1>회원 목록</h1>
            <button onClick={onClickHandler}>조회하기</button>
            <ul>
              {users.map((user) => (
                <li key={user.id}>{user.name}</li>
              ))}
            </ul>
          </>
        );
      }

      const { createStore, applyMiddleware } = Redux;
      const { Provider } = ReactRedux;

      const store = createStore(reducer, applyMiddleware(thunkMiddleware));

      ReactDOM.createRoot(document.getElementById('root')).render(
        <Provider store={store}>
          <App />
        </Provider>
      );

6. 미들웨어 진행 순서

<div id="root"></div>
    <script type="text/babel">  

        /* 리덕스 미들웨어 여러 개 사용 시 적용 우선순위 */

        const firstMiddleware = store => next => action => {
            console.log('first middleware');
            const result = next(action);
            return result;
        };

        const secondMiddleware = store => next => action => {
            console.log('second middleware');
            const result = next(action);
            return result;
        };

        const thirdMiddleware = store => next => action => {
            console.log('third middleware');
            const result = next(action);
            return result;
        };

        /* ---------------------------------------------------------------------------------- */
        const { createActions, handleActions } = ReduxActions;

        /* 초기값 */
        const initialState = 0;

        /* 액션 */
        const INCREMENT = 'count/INCREASE';
        const DECREMENT = 'count/DECREASE';

        const { count : { increase, decrease } } = createActions({
            [INCREMENT] : (amount = 1) => ({ incrementValue : amount }),
            [DECREMENT] : (amount = 1) => ({ decrementValue : amount })
        });

        /* 리듀서 */
        const reducer = handleActions({
            [INCREMENT] : (state, { payload : { incrementValue } }) => {
                return state + incrementValue
            },
            [DECREMENT] : (state, { payload : { decrementValue } }) => {
                return state - decrementValue;
            }
        }, initialState);
        
        /* ---------------------------------------------------------------------------------------------- */

        const { useSelector, useDispatch } = ReactRedux;

        function App() {

            const count = useSelector(state => state);
            const dispatch = useDispatch();

            const increaseCount = () => {
                dispatch(increase());
            }
            const decreaseCount = () => {
                dispatch(decrease());
            }

            return (
                <>
                    <h1> Count : { count } </h1>
                    <button onClick={ increaseCount }>1 증가</button>
                    <button onClick={ decreaseCount }>1 감소</button>
                </>
            );
        }

        /* --------------------------------------------------------------------------------------------- */

        /* 스토어 */
        const { Provider } = ReactRedux;
        const { createStore, applyMiddleware } = Redux;    

        /* 미들웨어를 등록한 순서대로 동작하게 된다. */
        const store = createStore(reducer, applyMiddleware(secondMiddleware, firstMiddleware, thirdMiddleware));

        ReactDOM.createRoot(document.getElementById('root')).render(
            <Provider store={ store }>
                <App/>
            </Provider>
        );

7. Redux Project Structure

https://gitlab.com/20221219-11/11_react/react-workspace.git

profile
DREAM STARTER

0개의 댓글