프로필

프로필 사진
Popomon
Frontend Developer
(2020/12 ~)

    카테고리

    포스트

    [Frontend/React] 23. 리덕스의 구조 - 리듀서, 스토어

    2020. 10. 6. 22:37

    꿈가게: To Do List - iOS

    꿈가게: To Do List - Android

    리듀서

    리듀서는 액션이 발생하였을 때, 새로운 상태값을 만드는 함수입니다. 리덕스의 상태값을 수정하는 유일한 방법은 액션 객체와 함께 dispatch 함수를 호출하는 것입니다. 다른 어떤 방법으로도 상태값을 수정하면 안됩니다. 상태값은 dispatch 함수가 호출된 순서대로 리덕스 내부에서 변경되기 때문에 상태값이 변화되는 과정을 쉽게 이해할 수 있습니다. 그리고 상태값은 불변객체로 관리해야 합니다.

     

    아래의 리듀서 샘플 코드를 보면서 설명을 드리겠습니다. 리덕스가 처음에 실행이 될 때, state에 undefined를 넣어서 리듀서를 호출합니다. 이 때, 초기 상태값을 설정하면 됩니다. 여기서는 기본 초기값으로 INITIAL_STATE를 넣어줍니다. 그리고 두번째 매개변수는 액션 객체가 넘어옵니다. 액션 객체의 type에 따라서 해당하는 액션에 대한 처리를 해주면 됩니다.

     

    여기서는 REMOVE_ALL, REMOVE 두개의 액션이 있습니다. 이 때, 불변객체로 관리하기 위해서 전개연산자를 사용하여 todos를 새로 만들어주고 있습니다. 

     

    // 리듀서 함수
    function reducer(state = INITIAL_STATE, action) {
      switch (action.type) {
        case REMOVE_ALL:
          return {
            ..state,
            todos: [],
          };
        case REMOVE:
          return {
            ..state,
            todos: state.todos.filter(todo => todo.id !== action.id),
          };
        default:
          return state;
      }
    }
    
    // 초기 상태값
    const INITIAL_STATE = { todos: [] }

     


    불변 객체를 신경쓰지 않도록 도와주는 immer 패키지

    그런데 수정하려는 속성이 깊은 곳에 있다면, 전개연산자를 매번 사용하는 것이 번거롭습니다. 이를 위하여 편리한 라이브러리가 존재합니다. 여러 라이브러리중에 immer 라는 패키지를 추천합니다. 사용법은 아래와 같습니다.

     

    produce 라는 것을 가져와서 아래와같이 호출을 해줍니다. 바꿀 값을 첫번째 인자에 넣고, 두번째 인자로 상태를 변경하는 로직이 들어간 함수를 작성을 합니다. 아래와 같이 적용을 하면 newPerson 객체는 person 객체에서 age 값만 32로 변경된 새로운 객체가 됩니다.

     

    import produce from 'immer';
    
    const person = { name: 'horong', age: 22 };
    const newPerson = produce(person, draft => {
      draft.age = 32
    })

     

    아래와 같이 조금 더 복잡한 코드로 적용을 해 보겠습니다. 아래와 같이 friends 배열의 첫번째 요소에서 age 속성 하나만 변경이 되었습니다. 이러한 경우에 총 4개의 조건으로 객체의 변경 여부를 출력해 보겠습니다.

     

    import React from 'react';
    import produce from 'immer';
    
    const prevState = {
      user: {
        name: 'horong',
        friends: [
          {name: 'jane', age: 23},
          {name: 'jake', age: 24},
        ],
      },
      products: [],
    }
    
    const nextState = produce(prevState, draft => {
      draft.user.friends[0].age = 32;
    });

     

    첫 번째로 이전 상태와 이후 상태를 비교하고 있습니다. produce 함수를 통해서 상태를 변경하였기 때문에 false를 반환하게 됩니다.

    두 번째는 user.friends[0] 객체의 age 값이 변경되었기 때문에 user.friends[0] 객체도 변경되었습니다. 따라서 false를 반환합니다.

    세 번째는 user.friends[1] 객체의 변경여부를 나타냅니다. 0번 객체만 변경하였기 때문에 1번과는 무관합니다. 따라서 true를 반환합니다.

    네 번째는 products 배열의 변경여부를 나타냅니다. product 객체는 user 객체의 변경과는 무관하기 때문에 true를 반환합니다.

     

    // false - 변경됨
    console.log('prevState === nextState', prevState === nextState);
    
    // false - 변경됨
    console.log(
      'prevState.user.friends[0] === nextState.user.friends[0]',
      prevState.user.friends[0] === nextState.user.friends[0]
    );
    
    // true - 변경되지 않음
    console.log(
      'prevState.user.friends[1] === nextState.user.friends[1]',
      prevState.user.friends[1] === nextState.user.friends[1]
    );
    
    // true - 변경되지 않음
    console.log(
      'prevState.products === nextState.products',
      prevState.products === nextState.products
    );

     

    이제 이 immer 라는 패키지를 사용해서 리듀서를 작성해 보겟습니다. immer 패키지를 사용 시, 하단의 코드처럼 리듀서 내부의 로직 전체를 produce 로 감싸서 정의하면 됩니다. 이로써, 불변객체로 관리 하기 위해서 전개 연산자를 사용할 필요 없이 훨씬 간결하게 표현이 가능합니다.

     

    // 리듀서 함수
    function reducer(state = INITIAL_STATE, action) {
      return produce(state, draft => {
        switch (action.type) {
          case ADD:
            draft.todos.push(action.todo);
            break;
          case REMOVE_ALL:
            draft.todos = [];
            break;
          case REMOVE:
            draft.todos = draft.todos.filter(todo => todo.id !== action.id);
            break;
          default:
            break;
        }
      });
    }
    
    // 초기 상태값
    const INITIAL_STATE = { todos: [] }

     


    리덕스 코드 작성시 주의사항

    리덕스 코드 작성시 주의할점이 몇가지 있습니다. 예제 코드를 가지고 설명을 해보겠습니다.

     

    객체를 참조하지 말고, 객체의 고유 아이디를 참조하도록 한다

    지금 SET_SELECTED_PEOPLE 이라는 액션에서, peopleList 에 있는 아이템을 하나 가져와서 selectedPeople 에 저장을 했습니다. 그리고 밑에있는 액션에서도 마찬가지로 배열에서 객체를 하나 가져와서 name 값을 수정했습니다.

     

    이렇게 EDIT_PEOPLE_NAME 액션에서와 같이, name 값을 수정하는 경우에는 people 객체가 새로 만들어집니다. 이 때, 문제가 될 수 있는 것은 SET_SELECTED_PEOPLE 액션에서 집어 넣었던 selectedPeople 에서 참조하고 있던 객체의 name 값을 두번 째 액션인  EDIT_PEOPLE_NAME에서 수정을 했을 때, people 객체가 새로 만들어졌지만, selectedPeople은 이전의 people 을 참조하고 있기 때문에 name 값이 변경되지 않습니다.

     

    따라서 EDIT_PEOPLE_NAME 액션으로 아무리 수정해도 selectedPeople 을 따로 변경해주지 않는 이상 최신 데이터로 반영되지 않는다는 것입니다.

     

    function reducer(state = INITIAL_STATE, action) {
      return produce(state, draft => {
        switch (action.type) {
          case SET_SELECTED_PEOPLE:
            draft.selectedPeople = draft.peopleList.find(
              item => item.id === action.id,
            );
            break;
          case EDIT_PEOPLE_NAME:
            const people = draft.peopleList.find(
              item => item.id === action.id,
            );
            people.name = action.name;
            break;
        }
      })
    }

     

    이러한 문제를 해결하려면, 객체를 가리킬 때에 객체의 레퍼런스를 참조해서 넣어주는 것이 아니라 객체의 고유한 아이디값(변하지 않는 값)을 저장해 두는 것이 좋습니다. 이렇게 하면 값이 변경되어도 아이디 값 만큼은 변경되지 않기 때문에, 값의 최신화 문제가 사라지게 됩니다.

     

    function reducer(state = INITIAL_STATE, action) {
      return produce(state, draft => {
        switch (action.type) {
          case SET_SELECTED_PEOPLE:
            draft.selectedPeople = action.id;
            break;
          case EDIT_PEOPLE_NAME:
            const people = draft.peopleList.find(
              item => item.id === action.id,
            );
            people.name = action.name;
            break;
        }
      })
    }

     

    리듀서 함수는 순수함수로 작성한다

    순수함수는 입력이 같을 때, 같은 출력을 주어야 하기 때문에 random을 사용하면 입력이 같으면 출력이 다를 수 있습니다. 따라서 random 값도 리듀서 영억에서는 사용해서는 안됩니다. 꼭 필요하다면 액션 객체를 만들 때, random 값을 생성해서 리듀서로 전달해야 합니다.

     

    또한 순수 함수가 되려면 부수 효과가 없어야 합니다. 부수효과라는 것은 외부 상태를 변경하는 것을 말합니다. 서버 API를 호출하는 callApi 함수와 같은 것도 부수효과 이기 때문에 이 부분을 없애야합니다. 그래서 서버 API는 리듀서 영역에서 호출하면 안됩니다.

     

    function reducer(state = INITIAL_STATE, action) {
      return produce(state, draft => {
        switch (action.type) {
          case SAY_HELLO:
            const random = Math.floor(Math.random() * 10 + 1);
            draft.msg = `안녕하세요, ${action.name}님의 행운의 숫자는 ${random}입니다.`;
            break;
          case INCREMENT:
            callApi({ url: '/sendActionLog', data: action });
            draft.value += 1;
            break;
        }
      });
    }

     

    따라서 아래와 같이 변경되어야 합니다. action 함수에서 random 값이 넘어왔고, API 호출 함수는 제거되었습니다.

     

    function reducer(state = INITIAL_STATE, action) {
      return produce(state, draft => {
        switch (action.type) {
          case SAY_HELLO:
            draft.msg = `안녕하세요, ${action.name}님의 행운의 숫자는 ${action.random}입니다.`;
            break;
          case INCREMENT:
            draft.value += 1;
            break;
        }
      });
    }

     


    리듀서 생성함수를 만들어서 사용하기

    리덕스를 사용하는 많은 분들이 아래와 같이 createReducer 함수를 만들어서 많이들 사용합니다.

     

    아래의 코드 중에서 조금 특이한 문법을 발견하실 수 있습니다. [ADD]: ()=>{} 과 같은 코드입니다. ADD: ()=>{} 와 같이 사용을 하면, 함수를 호출하는 경우에 ADD 의 값을 키 값으로 불러오는 것이 아니라 "ADD" 라는 값을 키로 함수를 불러옵니다. 따라서 ADD 안에 들어있는 값을 키로 사용하고 싶은 경우에는 아래와 같이 대괄호를 붙여서 표현합니다.

     

    액션 파일에서 ADD, REMOVE 와 같은 타입 값을 가져와서 키 값으로 정의한 후, 리듀서에서 처리할 로직을 작성을 해주면 됩니다.

     

    const reducer = createReducer(INITIAL_STATE, {
      [ADD]: (state, action) => state.todos.push(action.todo),
      [REMOVE_ALL]: state => (state.todos = []),
      [REMOVE]: (state, action) => state.todos.filter(todo => todo.id !== action.id),
    });

     

    createReducer 함수를 사용할 때 이러한 방법으로 작성을 한다면 createReducer 함수를 정의하는 코드는 훨씬 더 간결해집니다.

     

    import produce from 'immer';
    
    // createReducer 정의하기
    function createReducer(initialState, handlerMap) {
      return function(state = initialState, action) {
        return produce(state, draft => {
          const handler = handlerMap[action.type];
          if (handler) {
            handler(draft, action);
          }
        })
      }
    }
    
    // createReducer 사용하기
    const reducer = createReducer(INITIAL_STATE, {
      [ADD]: (state, action) => state.todos.push(action.todo),
      [REMOVE_ALL]: state => (state.todos = []),
      [REMOVE]: (state, action) => state.todos.filter(todo => todo.id !== action.id),
    });

     


    리덕스에서 스토어 생성하기

    리덕스에서 스토어를 만들 때는 createStore 함수를 이용합니다. 이 함수의 매개변수로 reducer를 넣어주면 스토어가 생성이 됩니다. 스토어는 상태값을 저장하는 역할도 있고, 그리고 액션처리가 끝났다는 것을 외부(뷰)에 알려주는 역할도 합니다. 그 액션처리가 끝났다는 메시지를 받기 위해서는 스토어의 subscribe 함수를 호출해서 아래와 같이 함수를 입력하면 됩니다.

     

    const store = createStore(reducer);
    
    let prevState;
    store.subscribe(() => {
      const state = store.getState();
      if (state === prevState) {
        console.log('상태값 같음');
      } else {
        console.log('상태값 변경됨');
      }
      prevState = state;
    });

     

     

     

     


    출처 : 실전 리액트 프로그리맹 - 인프런 (추천합니다 !!)

     

    실전 리액트 프로그래밍 - 인프런

    [실전 리액트 프로그래밍] 책의 저자 직강! 리액트의 기초부터 실전 활용법까지 익힐 수 있습니다. 초급 프레임워크 및 라이브러리 웹 개발 Front-End React Redux 웹 개발 온라인 강의 리액트(React)를 �

    www.inflearn.com