안녕하세요!

FE 개발자 유진주입니다.

Web/React

[처음 만난 리덕스] 5. Reducer

ypearl 2023. 12. 25. 04:58

Reducer

: Action이 발생하면, Action을 실제로 처리하는 역할을 하는 함수

= 입력에 어떤 처리를 해서, 원하는 결과로 축소시키는 과정

 

[역할] Redux State에 변화를 주는 역할

 

state와 action을 받아 "새로운 state"를 만들어 리턴하는 Reducer

 

 

function appReducer(state = initialState, action) {
	switch(action.type) {
    	case 'PUSH_ITEM':
			return [...state, action.item];
        case 'POP_ITEM':
        	const newState = [...state]; // (state 변화가 아닌) 새로운 state를 생성!
            newState.pop();
            return newState;
        default:
        	return state;
   }
}

 

 

Rules of Reducer

  1. Only calculate the new state value based on the state and action arguments.
    : 새로운 state는 파라미터로 받은 현재 state와 Action 객체를 기반으로 생성해야 한다.
    (외부의 다른 값을 참조해서 state 변화주는 것 X)
    → pure function을 위한 규칙
  2. Not allowed to modify the existing state.
    Instead, make immutable updates. 
    : 현재 state를 조작할 수 없으며, 새로운 state를 만들어 immutalbe 업데이트를 해야 한다.

  3. Not do any asynchronous logic or other "side effects".
    : 비동기 로직이나 사이드 이펙트는 허용하지 않는다.
    예) Reducer 내에서 서버와 통신해서 데이터 받아오는 등의 동작(X)
    *side effect: reducer 외부에서 보여질 수 있는 상태의 변경 또는 동작
                       (콘솔 로그 출력, 파일 저장 등 순수함수의 return 값과 무관한 것들)

 

=> Predictable (예측 가능) 위해!

 

 

Immutable Update (불변적 업데이트)

: 현재 state를 변경하지 않고 새로운 state를 만들어 업데이트하는 방식

 

좌: X / 우: O

 

 

- Redux Toolkit을 사용하면 복잡한 객체에 대해서도 손쉽게 immutable update를 처리할 수 있다.

 

 

 

combineReducers()

: 여러 개의 리듀서들을 하나로 모으는 역할

 

 

=> Root Reducer

: 이렇게 모은 리듀서들이 하나로 합쳐진 것

 

 

import { combineReducers } from 'redux';

import postReducer from './reducers/postReducer';
import commentReducer from './reducers/commentReducer';

const rootReducer = combineReducers({  // postReducer와 commentReducer를 합침
	post: postReducer,  // 객체 형태: 객체의 키 = RootState의 키
    comment: commentReducer
});

export default rootReducer;

 

 

 

[실습] TODO 애플리케이션에 MEMO 기능 추가하기

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>처음 만난 리덕스 - TODO</title>
</head>
<body>
    <h3>오늘 할 일</h3>
    <ul id="todo-list"></ul>

    <div>
        <input id="input-text"/>
        <button id="add-button">할 일 추가</button>
        <button id="remove-button">할 일 삭제</button>
        <button id="remove-all-button">모두 삭제</button>
        <button id="logging-state">State Logging</button>
    </div>

    <h3>메모</h3>
    <ul id="memo-list"></ul>

    <div>
        <input id="input-memo-text"/>
        <button id="add-memo-button">메모 추가</button>
        <button id="remove-memo-button">메모 삭제</button>
    </div>

    <script>
        // 액션 타입을 별도로 선언해 코드의 반복을 줄임
        // TODO 관련 ACTION Type
        var ACTION_TYPE_ADD_TODO = "ADD_TODO";
        var ACTION_TYPE_REMOVE_TODO = "REMOVE_TODO";
        var ACTION_TYPE_REMOVE_ALL = "REMOVE_ALL";
       
        // MEMO 관련 ACTION Type
        var ACTION_TYPE_ADD_MEMO = "ADD_MEMO";
        var ACTION_TYPE_REMOVE_MEMO = "REMOVE_MEMO";

        var todoInitialState = []; // 리턴값이 없으면 에러 발생 -> 초기값 무조건 필요!
        var memoInitialState = [];

        function todoReducer(state = todoInitialState, action){
            switch(action.type){
                case ACTION_TYPE_ADD_TODO:
                    return state.concat(action.text);
                case ACTION_TYPE_REMOVE_TODO:
                    return state.slice(0, -1); // slice 함수를 사용해 배열의 마지막 아이템 하나를 삭제
                case ACTION_TYPE_REMOVE_ALL:
                    return [];
                default:
                    return state;
            }
        }
     
        function memoReducer(state = memoInitialState, action){
            switch(action.type){
                case ACTION_TYPE_ADD_MEMO:
                    return state.concat(action.text);
                case ACTION_TYPE_REMOVE_MEMO:
                    return state.slice(0, -1);
                default:
                    return state;
            }
        }

        function loggerMiddleware({getState}){ // 구조분해할당: 파라미터 중 getState만 꺼내서 사용
            return (next) => (action) => {
                console.log("dispatch 예정 action", action);

                // Middleware chain에 있는 다음 dispatch 함수를 호출
                const returnValue = next(action);

                console.log("dispatch 이후 state", getState());

                return returnValue;
            };
        }

        var rootReducer = Redux.combineReducers({
            todo: todoReducer,
            memo: memoReducer,
        });

        var store = Redux.createStore(
            rootReducer,
            Redux.applyMiddleware(loggerMiddleware)
        );

        var todoListElem = document.getElementById("todo-list");
        var memoListElem = document.getElementById("memo-list");
        var inputElem = document.getElementById("input-text");
        var inputMemoElem = document.getElementById("input-memo-text");

        function render() {
            // 이전 TODO 목록 초기화 (아이템이 중복으로 쌓이지 않기 위함)
            todoListElem.innerHTML="";
            memoListElem.innerHTML="";

            // TODO 목록 렌더링
            store.getState().todo.forEach((todo) => { // 각 state 접근시, root reducer 만들 때 사용한 키 값 사용
                const todoListItemElem = document.createElement("li");
                todoListItemElem.textContent=todo;
                todoListElem.appendChild(todoListItemElem);
            });
           
            // MEMO 목록 렌더링
            store.getState().memo.forEach((todo) => {
                const memoListItemElem = document.createElement("li");
                memoListItemElem.textContent=todo;
                memoListElem.appendChild(memoListItemElem);
            });
        }

        render();
        store.subscribe(render); // Redux Store의 state가 변경될 때마다 render 함수 호출

        function addTodoActionCreator(text){
            return {
                type: ACTION_TYPE_ADD_TODO,
                text: text,
            };
        }

        function removeTodoActionCreator(){
            return {
                type: ACTION_TYPE_REMOVE_TODO,
            };
        }

        function removeAllActionCreator(){
            return {
                type: ACTION_TYPE_REMOVE_ALL,
            };
        }

        function addMemoActionCreator(text){
            return {
                type: ACTION_TYPE_ADD_MEMO,
                text: text,
            };
        }

        function removeMemoActionCreator(){
            return {
                type: ACTION_TYPE_REMOVE_MEMO,
            };
        }

        document
            .getElementById("add-button")
            .addEventListener("click", function(){
                // Action을 실제로 dispatch
                store.dispatch(addTodoActionCreator(inputElem.value));

                // Input 초기화
                inputElem.value = "";
            });
       
        document
            .getElementById("remove-button")
            .addEventListener("click", function(){
                store.dispatch(removeTodoActionCreator());
            });

        document
            .getElementById("remove-all-button")
            .addEventListener("click", function(){
                store.dispatch(removeAllActionCreator());
            });
        document
            .getElementById("logging-state")
            .addEventListener("click", function(){
                console.log("현재 state", store.getState());
            });

        document
            .getElementById("add-memo-button")
            .addEventListener("click", function(){
                store.dispatch(addMemoActionCreator(inputMemoElem.value));
                inputElem.value = "";
            });
       
        document
            .getElementById("remove-memo-button")
            .addEventListener("click", function(){
                store.dispatch(removeMemoActionCreator());
            });
    </script>
</body>
</html>

 

 

실행 결과