본문 바로가기
  • GDG on campus Ewha Tech Blog
3-1기 스터디/Front-End 토이 프로젝트

[10주차] 리덕스 미들웨어를 통한 비동기 작업 관리

by ssine 2022. 1. 30.



GDSC FE-Toy Project Study Plan

  • 'React를 다루는 기술' Ch17, 18 공부 및 실습 진행

 

[Week 10] 1/27

  • 발표자
    • Ch17. 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 - 하수민
    • Ch18. 리덕스 미들웨어를 통한 비동기 작업 관리 - 장효신

 


 
 

Ch18. 리덕스 미들웨어를 통한 비동기 작업 관리

1) 미들웨어란?

 

리덕스 미들웨어란?

리덕스에서 액션을 디스패치했을 때, 리듀서에서 이를 처리하기에 앞서

사전에 지정된 작업들을 실행함

미들웨어는 액션과 리듀서 사이의 중간자

 

👉 액션을 디스패치하고나서 어떤 작업들을 연쇄적으로 실행해야 할 때 사용할 수 있음 

 

미들웨어가 할 수 있는 일

  • API 요청에 대한 상태를 관리
    • 예를 들어 요청이 시작됐을 때는 로딩 중임을,
    • 요청이 성공하거나 실패했을 때는 로딩이 끝났음을 명시해야 함
  • 전달받은 액션을 콘솔에 기록하거나
  • 전달받은 액션을 기반으로 액션을 취소하거나
  • 다른 종류의 액션을 추가로 디스패치할 수도 있음

2) 실습

실습을 통해서 미들웨어를 사용한 비동기 작업을 관리해보겠다. 

다음 순서로 진행한다.

  • 작업 환경 준비
  • 미들웨어 직접 만들기
  • redux-logger 사용하기
  • 미들웨어를 사용한 비동기 작업 관리

작업 환경 준비

1. CRA(create react app)을 사용해서 리엑트 프로젝트를 생성한다. 

 

2. 리덕스를 사용해서 카운터를 구현할 것이므로

이에 필요한 라이브러리인 `redux`와 `react-redux`와 `redux-actions`을 설치한다. 

 

cf. 각 라이브러리에 대한 설명

  • redux: 
  • react-redux:
  • redux-actions: 

다음 명령어를 사용하면 된다.

$ npm add redux react-redux redux-actions

 

3. 리덕스를 위한 코드를 준비한다.

  • 먼저, counter 리덕스 모듈을 작성한다. 

 

카운터 작동 확인

미들웨어 만들기

  • 실제 프로젝트에서는 다른 개발자가 만들어 놓은 미들웨어를 사용하므로 미들웨어를 직접 만들어서 사용하는 일이 많지 않음
  • 하지만 여기에서는 미들웨어의 작동방식을 제대로 이해하기위해 직접 만들어봄
  • 원하는 미들웨어가 없을 때 직접 만들거나 기존 미들웨어를 커스터마이징해서 사용해볼 수 있음

 

액션이 디스패치될 때마다 액션 정보와 액션 디스패치되기 전후의 상태를 콘솔에 보여주는

로깅 미들웨어를 작성해보자


리덕스 미들웨어 구조

미들웨어는 결국 함수를 반환하는 함수

 

함수 파라미터에는 store, next, action 3가지가 있음

  • store: 리덕스 스토어 인스턴스
  • next: 함수 형태이며 store.dispatch와 비슷한 역할을 함
  • action: 디스패치된 액션
const loggerMiddleware = store => next => action => {
    // 미들웨어 기본 구조
}

export default loggerMiddleware;
  • 화살표 함수를 연달아서 사용함

일반 function 키워드로 풀어서 쓴다면 다음 구조임

const loggerMiddleware = function loggerMiddleware(store){
 return function(next){
  return function (action){
   // 미들웨어 기본 구조
  };
 };
};

 

 

loggerMiddleware 작성

const loggerMiddleware = store => next => action => {

    // 액션 타입으로 log를 그룹화함
    console.group(action && action.type);
    
    // 1. 스토어 상태 조회
    console.log('이전 상태', store.getState());

    console.log('액션', action);

    // 2. 다음 미들웨어나 리듀서에게 액션을 전달함
    next(action); 

    // 업데이트된 상태
    console.log('다음 상태', store.getState());

    // 그룹 끝
};
    console.groupEnd();

export default loggerMiddleware;

 

다음, 리덕스 미들웨어를 스토어에 적용한다.

실행 결과


redux-logger 사용하기

  • 이번엔 오픈소스 미들웨어인 redux-logger를 사용해보자
  • 앞서 만든 loggerMiddleware보다 훨씬 더 잘 만들어진 라이브러리이며,
  • 브라우저 콘솔에 나타는 형식도 훨씬 깔끔하다. 

 

다음 명령어로 설치함

$ npm add redux-logger

 

index.js 수정

  • 액션 디스패치 시간도 나타남
  • 콘솔에 색상이 입혀짐

redux-logger


비동기 작업을 처리하는 미들웨어 사용

오픈 소스 미들웨어를 사용해서 비동기 작업을 효율적으로 관리해보겠다. 

여기에서는 redux-thunk와 redux-saga를 다룸

 

  • redux-thunk: 비동기 작업을 처리할 때 가장 많이 사용하는 미들웨어로
    • 객체가 아닌 함수 형태의 액션을 디스패치 할 수 있음
  • redux-saga : redux-thunk 다음으로 가장 많이 사용되는 비동기 작업 미들웨어 라이브러리로,
    • 특정 액션이 디스패치되었을 때 정해진 로직에 따라 다른 액션을 대스패치하는 규칙을 작성해서 비동기 작업을 처리함

redux-thunk

리덕스의 창시자인 댄 아브라모프가 만듦

리덕스 공식 메뉴얼에서도 이 미들웨어를 사용해서 비동기 작업 다룸

 

Thunk란?

  • 특정 작업을 나중에 할 수 있도록 미루기 위해 함수 형태로 감싼 것

 

예를 들어, 파라미터에 1을 더하는 함수를 만들고 싶다면 다음과 같이 작성할 수 있고

addOne 호출하면 바로 1 + 1이 연산됨

const addOne = x => x + 1;
addOne(1); // 2

 

 

그런데 이 작업을 나중에 하도록 미루고 싶다면?

const addOne = x => x + 1;
const addOneThunk = x => () => addOne(x);

const fn = addOneThunk(1);

setTimeout(()=>{
 const value = fn(); // fn이 실행되는 시점에 연산
 console.log(value);
}, 1000);

 

 

redux-thunk는 다음 명령어로 라이브러리 설치

$ yarn add redux-thunk

 

index.js에서 store를 만들 때 적용한다.

import ReduxThunk from 'redux-thunk';

const logger = createLogger();

const store = createStore(rootReducer, applyMiddleware(logger, ReduxThunk));

 

Thunk 생성 함수 만들기

increaseAsync와 decreaseAsync를 만들어서 카운터값을 비동기적으로 변경시켜보자

 

- redux-thunk는 액션 생성 함수에서 일반 액션 객체를 반환하는 대신에 함수를 반환함

 

modules/counter.js 수정

// 1초 뒤에 increase 혹은 decrease 함수를 디스패치함
export const increaseAsync = () => dispatch => {
    setTimeout(()=>{
        dispatch(increase());
    }, 1000); // ms 단위
};

export const decreaseAsync = () => dispatch => {
    setTimeout(()=>{
        dispatch(decrease());
    }, 1000); // ms 단위
}

const initialState = 0; // 상태는 꼭 객체일 필요가 없음

const counter = handleActions(
    {
    [INCREASE]: state => state + 1,
    [DECREASE]: state => state-1
    },
    initialState
);

export default counter;

 

CounterContainer 수정

 

increase => increaseAsync 

decrease => decreaseAsync로 수정

import { increaseAsync, decreaseAsync } from '../modules/counter';
import Counter from '../components/Counter';

const CounterContainer = ({ number, increaseAsync, decreaseAsync }) => {
  return (
    <Counter number={number} onIncrease={increaseAsync} onDecrease={decreaseAsync} />
  );
};



export default connect(
  state => ({
    number: state.counter
  }),
  {
    increaseAsync,
    decreaseAsync
  }
)(CounterContainer);

 

실행화면

 

  • redux-thunk는 처음 디스패치되는 액션은 함수 형태이고, 두 번째 액션은 객체 형태임

 

웹 요청 비동기 작업 처리하기

thunk의 속성을 활용하여 웹 요청 비동기 작업을 처리하는 방법에 대해 알아보겠다.

JSONPlaceholder에서 제공되는 가짜 API를 사용할 것

 

사용할 API

# 포스트 읽기(:id는 1~100 사이 숫자)
GET https://jsonplaceholder.typicode.com/posts/:id

 

API를 호출할 때는 Promise 기반인 웹 클라이언트인 axios를 사용함

$ yarn add axios

 

lib/api.js

API를 함수화한다.

  • API를 호출하는 함수를 따로 작성하면 가독성이 좋고 유지보수도 쉬워짐
  • export를 사용해서 내보냄
import axios from 'axios';


export const getPost = id =>
  axios.get(https://jsonplaceholder.typicode.com/posts/</span><span class="co49">${</span><span class="cd2 co33">id</span><span class="co33">}</span><span class="cd2 co31">);



export const getUsers = id =>
  axios.get(https://jsonplaceholder.typicode.com/users);

 

리듀서 작성

import {handleActions} from 'redux-actions';
import * as api from '.../lib/api';

// 액션 타입 선언
const GET_POST = 'sample/GET_POST';
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS';
const GET_POST_FAILURE = 'sample/GET_POST_FAILURE';

const GET_USERS = 'sample/GET_USERS';
const GET_USERS_SUCCESS = 'smaple/GET_USERS_SUCCESS';
const GET_USER_FAILURE = 'sample/GET_USERS_FAILURE';

// thunk 함수 생성
export const getPost = id => async dispatch => {
    dispatch({type: GET_POST}); // 요청 시작 알림
    try{
        const response = await api.getPost(id);
        dispatch({
            type: GET_POST_SUCCESS,
            payload: response.data
        }); // 요청 성공
    } catch(e){
        dispatch({
            type: GET_POST_FAILURE,
            payload: e,
            error: true
        }); // 에러 발생
        throw(e) // 나중에 컴포넌트에서 에러를 조회할 수 있게 해줌
    }
};

};

// 초기 상태 선언
const initialState = {
    // 요청의 로딩 중 상태 관리
    loading: {
        GET_POST: false,
        GET_USERS: false
    },
    post: null,
    users: null
};

const sample = handleActions({
    [GET_POST]: state => ({
        ...state,
        loading: {
            ...state.loading,
            GET_POST:true // 요청 시작
        }
    }),
    [GET_POST_SUCCESS]: (state, action) => ({
        ...state,
        loading: {
            ...state.loading,
            GET_POST:false // 요청 완료
        },
        post: action.payload
    }),
    [GET_POST_FAILURE]: (state, action) => ({
        ...state,
        loading: {
            ...state.loading,
            GET_POST:false // 요청 완료
        },
    }),
    initialState
);

export default sample;

 

 

index.js에 루트 리듀서에 포함시키기

import {combineReducers} from 'redux';
import counter from './counter';
import sample from './sample';

const rootReducer = combineReducers({
    counter,
    sample
});

export default rootReducer;

 

다음은, 위에서 불러온 데이터를 렌더링해 줄 프레젠테이셔널 컴포넌트 작성

 

이때, 유효성 검사를 꼭 해야한다

  • 조건부 렌더링 연산자인 &&를 작성하면
  • post가 유효할 때만 값을 보여줄 수 있음
  • 데이터가 없는 상태라면 post.title을 조회할 때 오류가 발생하니 꼭 유효성 검사를 해줘야 함
(...)
      <section>
          <h1>포스트</h1>
          {loadingPost && '로딩 중...'}
          {!loadingPost && post && (
              <div>
                  <h3>{post.title}</h3>
                  <h3>{post.body}</h3>
              </div>
          )}
      </section>
      <hr />
      <section>
          <h1>사용자 목록</h1>
          {loadingUsers && '로딩 중...'}
          {!loadingUsers && users && (
              <ul>
                  {users.map(user => (
                      <li key={user.id}>
                          {user.username} ({user.email})
                      </li>
                  ))}
              </ul>
          )}
      </section>
(...)

 

 

sample container 작성

  • useEffect로 데이터 로딩
    • 뎁스에 getPost와 getUsers를 줌
  • connect로 스토어에서 상태 조회한거랑 리듀서 내려보내 줌
import React, { useEffect } from "react";
import { connect } from "react-redux";
import Sample from "../components/Sample";
import { getPost, getUsers } from "../modules/sample";

const SampleContainer = ({
  getPost,
  getUsers,
  post,
  users,
  loadingPost,
  loadingUsers
}) => {
  useEffect(() => {
    getPost(1);
    getUsers(1);
  }, [getPost, getUsers]);
  return (
    <Sample
      post={post}
      users={users}
      loadingPost={loadingPost}
      loadingUsers={loadingUsers}
    />
  );
};

export default connect(
  ({ sample }) => ({
    post: sample.post,
    users: sample.users,
    loadingPost: sample.loading.GET_POST,
    loadingUsers: sample.loading.GET_USERS
  }),
  {
    getPost,
    getUsers
  }
)(SampleContainer);

 

실행 결과

 

댓글