프로필

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

    카테고리

    포스트

    [Frontend/React] 22. 리덕스의 구조 - 액션, 미들웨어

    2020. 10. 6. 19:33

    꿈가게: To Do List - iOS

    꿈가게: To Do List - Android

    리덕스의 구조

    리덕스는 아래 그림과 같이 액션 / 미들웨어 / 리듀서 / 스토어 총 4가지 요소가 있습니다. 그리고 프론트엔드 애플리케이션을 만들 때는 이런 리덕스의 요소들과 뷰를 연결해서 사용을 합니다.

     

    먼저 뷰쪽에서 상태값을 변경하고 싶을 때, 액션을 발생시킵니다.

     

    액션을 발생 시키면 그 액션을 미들웨어가 처리를 합니다. 여기서 미들웨어에 여러가지 기능을 넣을 수 있습니다.

     

    미들웨어의 처리가 끝나면, 리듀서로 넘어갑니다. 리듀서에서는 해당 액션에 의해서 상태값이 어떻게 변경되는지 그 로직을 담고 있습니다. 그래서 리듀서의 출력은 새로운 상태값이 됩니다.

     

    그 새로운 상태값을 스토어에게 알려주면 스토어가 상태값을 저장합니다. 그리고 스토어에 등록되어있는 데이터에 관심이 있는 여러가지 옵저버들이 있습니다. 스토어는 액션 처리가 마무리 되었을 때 그 옵저버들에게 현재 상태값에 대한 정보를 알려줍니다.

     

    그러면 뷰가 그 이벤트를 받아서 화면을 갱신합니다.

     

    이렇게 직관적이고 예측 가능한 구조를 가지는게 리덕스의 장점입니다.

     

     


    액션

    액션은 type 속성값을 가지고있는 객체입니다. type 속성은 액션을 구분하기 위해서 사용을 합니다. 따라서 type 속성값은 유니크해야 합니다. 그래서 아래와 같이 prefix를 붙여서 많이 사용을 합니다.

     

    { type: 'todo/ADD', title: '...' }
    { type: 'todo/REMOVE', title: '...' }

     

    액션을 정의할때는 액션 타입과 액션 크리에이터 함수를 한 쌍으로 묶어서 작성을 합니다. 액션 타입을 상수로 만드는 이유는 나중에 리듀서에서도 사용을 해야하기 때문입니다.

     

    export const ADD = 'todo/ADD';
    export const REMOVE = 'todo/REMOVE';
    
    export function addTodo({ title, priority }) {
      return { type: ADD, title, priority }
    }
    export function removeTodo({ id }) {
      return { type: REMOVE, id }
    }

     


    미들웨어

    미들웨어는 아래와 같은 형태의 함수입니다.  아래와 같은 형태인 이유는, action => next(action) 영역에서 store 와 next 를 사용하기 위함입니다. 미들웨어를 작성할 때, 스토어가 필요한 경우가 많이 있습니다. 그리고 next 함수는 리덕스에서 넘겨주는 함수이며, 다음에 호출될 어떤 함수를 의미합니다.

     

    const myMiddleware = store => next => action => next(action);

     

    좀 더 이해하기 쉽게 미들웨어의 예를 들어보겟습니다. 두개의 미들웨어가 있고, 스토어를 생성할때 두개의 미들웨어를 등록해주고 있습니다. 그 다음 간단한 액션을 하나 실행하고 있습니다.

     

    // 미들웨어
    const middleware1 = store => next => action => {
      console.log('middleware1 start');
      const result = next(action);
      console.log('middleware1 end');
      return result;
    }
    const middleware2 = store => next => action => {
      console.log('middleware2 start');
      const result = next(action);
      console.log('middleware2 end');
      return result;
    }
    
    // 리듀서
    const myReducer = (state, action) => {
      console.log('myReducer');
      return state;
    }
    
    // 스토어
    const store = createStore(myReducer, applyMiddleware(middleware1, middleware2));
    
    // 액션 실행
    store.dispatch({ type: 'someAction' })

     

    앱을 실행해 보면 아래와 같은 절차대로 실행됩니다. 미들웨어의 next 함수는 다음 미들웨어의 함수를 호출합니다. 하지만 마지막 미들웨어인 경우에 next 함수는 리듀서를 호출합니다. 따라서 아래와 같이 실행됩니다.

     

    1. 처음에 상태값을 초기화 하기 위해서 리듀서가 먼저 실행 (미들웨어 없이 리듀서만 실행)
    
    2. 프로그램이 액션을 실행 (dispatch 호출)
    
    3. 미들웨어 1번 실행
    
    4. 미들웨어 1번의 next 호출
    
    5. 미들웨어 2번이 실행
    
    6. 미들웨어 2번의 next 호출
    
    7. 리듀서 실행
    
    8. 미들웨어 2번 종료
    
    9. 미들웨어 1번 종료

     

    이어서는 미들웨어를 활용하는 방법에 대해 알아보겠습니다.

     

    상태값을 출력하는 미들웨어

    이 미들웨어는 상태값을 가져오기 위해 스토어를 사용하고 있습니다. 우선 next를 호출하면 리듀서 함수를 호출하기 때문에 상태값이 변경이 될 것입니다. 이렇게 상태값이 변경이 되기 전에 미들웨어에서는 상태값을 출력을 합니다. 그 다음 next 함수로 리듀서 함수를를 호출 한 후에 다시한번 변경된 상태값을 출력합니다.

     

    // 미들웨어
    const printLog = store => next => action => {
      console.log(`prev state = ${JSON.stringify(store.getState())}`);
      const result = next(action);
      console.log(`next state = ${JSON.stringify(store.getState())}`);
      return result;
    }
    
    // 리듀서
    const myReducer = (state = { name: 'horong' }, action) => {
      console.log('myReducer');
      switch(action.type) {
        case 'someAction':
          return {...state, name:'horong123'}
        default:
          return state;
      }
      return state;
    }
    
    // 스토어
    const store = createStore(myReducer, applyMiddleware(printLog));
    
    // 액션 실행
    store.dispatch({ type: 'someAction' })

     

    작업의 시간을 지연시킬 수 있는 미들웨어

    이 미들웨어는 delay 라는 값이 있을 때, delay 값의 시간이 경과한 후에 next 함수를 호출하고, 이 작업을 도중에 취소할 수 있는 cancel 이라는 함수를 반환하여 작업 취소라는 부가적인 기능까지 제공하는 코드입니다.

     

    여기서 발견할 수 있는 부분은 미들웨어가 반환한 함수가 dispatch 함수가 반환한 값과 동일하다는 것이 매우 특이한 점입니다.

     

    // 미들웨어
    const delayAction = store => next => action => {
      const delay = action.meta?.delay;
      if (!delay) {
        return next(action);
      }
      const timeoutId = setTimeout(() => next(action), delay);
      return function cancel() {
        clearTimeout(timeoutId);
      }
    }
    
    // 리듀서
    const myReducer = (state = { name: 'horong' }, action) => {
      console.log('myReducer');
      switch(action.type) {
        case 'someAction':
          return {...state, name:'horong123'}
        default:
          return state;
      }
      return state;
    }
    
    // 스토어
    const store = createStore(myReducer, applyMiddleware(delayAction));
    
    // 액션 실행
    const cancel = store.dispatch({ type: 'someAction', meta: { delay: 3000 } });
    
    // 작업 취소
    cancel();

     

    여기서 action 이라는 객체 안에는 필수적으로 type 이라는 속성이  정의되어 있기 때문에 항상 값이 존재하는 객체입니다. 하지만 meta 라는 값은 정의되지 않을 수도 있습니다. 이러한 경우에 mata.delay 라는 속성을 참조하면 에러가 발생할 수 있습니다.

     

    이러한 문제를 해결하려면 아래와 같은 방법으로 가능합니다.

     

    const delay = action.meta && action.meta.delay;

     

    하지만 비교적 최근에 추가된 optional chaining 이라는 자바스크립트 기능을 사용하여 아래와 같이 표현할 수 있습니다.

     

    const delay = action.meta?.delay;

     

    로컬스토리지에 저장을 해주는 미들웨어

    아래의 코드는 로컬 스토리지에 저장을 해주는 미들웨어입니다. 이것도 위와 마찬가지로 optional chaining 기능을 사용하여 action.meta 에 localStorageKey 값이 있으면 action 객체를 저장해줍니다.

     

    // 미들웨어
    const saveToLocalStorage = store => next => action => {
      if (action.meta?.localStorageKey) {
        localStorage.setItem(action.meta?.loaclStorageKey, JSON.stringify(action));
      }
      return next(action);
    }
    
    // 리듀서
    const myReducer = (state = { name: 'horong' }, action) => {
      console.log('myReducer');
      switch(action.type) {
        case 'someAction':
          return {...state, name:'horong123'}
        default:
          return state;
      }
      return state;
    }
    
    // 스토어
    const store = createStore(myReducer, applyMiddleware(saveToLocalStorage));
    
    // 액션 실행
    store.dispatch({ 
      type: 'someAction',
      title: 'asdf',
      meta: { localStorageKey: 'myKey' } 
    });

     

     

     

     


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

     

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

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

    www.inflearn.com