[18장] 리덕스 미들웨어(redux-thunk)

2022. 4. 5. 12:24책/리액트를 다루는 기술

액션 디스패치시 리듀서에서 액션을 처리하기에 앞서 원하는 작업을 실행해주는 중간자. 사실 미들웨어를 직접 만들어 쓸 일은 별로 없고, Redux Thunk, Redux Saga 등을 사용하면 된다.

 

1. 미들웨어 기본 구조

미들웨어의 기본적인 구조를 파악해보기 위해 간단한 미들웨어를 만들어보자.

뭔가 대단한게 있을 것 처럼 소개는 됐지만 미들웨어는 그냥 함수를 반환하는 함수다.

1) next

다만 next라는 새로운 함수가 등장한다는 것 뿐인데, 기능적으로는 store.dispatch와 유사하나 차이점은 next(action)을 호출하면 그 다음 미들웨어에게 액션을 넘겨준다는 것 뿐이다(만약 그 다음 미들웨어가 없다면 최종적으론 리듀서에게 액션을 넘겨준다)

만약 미들웨어에서 next를 사용하지 않으면 액션이 리듀서에 전달되지 않는다(즉, 액션을 무시함)

 

이제 위에서 작성하다만 미들웨어를 다 채워보자. 간단하게 버튼을 누르면 이전/이후 상태를 보여주는 미들웨어이다. 사실 redux-logger라는 라이브러리가 제공하는 기능중 일부를 구현해본건데 뭐 암튼

 

2. 비동기 작업 처리 미들웨어

이제 본격적으로 미들웨어를 사용해보자. 특히, 리액트에서 비동기 작업 처리를 도와주는 대표적인 미들웨어 2가지를 알아보자.

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

물론 이 외에도 redux-promise-middleware, redux-pender, redux-observable 등 종류 자체는 많긴 하다. 뭐가됐든 내가 편하자고 쓰는거니까 불편하면 사용하지 않아도 좋다.

 

3. redux-thunk

리덕스를 쓰는 프로젝트에서 비동기 작업 처리시 사용하는 가장 기본적인 미들웨어. 리덕스 창시자인 댄 아브라모프가 만들었다. 기본적으로 Thunk란 '기존의 서브루틴에 추가적인 연산을 삽입할 때 사용되는 서브루틴' 이라고 정의된다.

 

그러나 리덕스 공식홈페이지에 가보면 For Redux specifically, "thunks" are a pattern of writing functions with logic inside that can interact with a Redux store's dispatch and getState methods. 라고 적혀있는데, 쉽게말해 스토어의 dispatch와 getState 메서드를 끄집어다 쓰는 함수 작성패턴이라고 설명해놨다.

 

1) Thunk 액션 생성 함수

그래도 아직까진 무슨소리인지 감이 잘 안잡히는데 예시를 보자. 

const sampleThunk = () => (dispatch, getState) => {
	// store에서 dispatch와 getState를 끄집어낸다
	// 이를 통해 현재 상태를 참조할 수 있고
	// 새 액션을 디스패치할 수 있다
};

위에서 미들웨어의 기본 생김새는 const 미들웨어 = store => next => action { ... } 라고 했으면서 위 모양은 전혀 그렇지 않다. 

 

2) Thunk 미들웨어 적용

※ applyMiddleware( ...Middlewares )

사실 Thunk 라이브러리의 핵심이며, 얘를 이해해야 이후 나오는 흐름을 이해할 수 있다.

 

3) 웹 요청 해보기

여기서부턴 코드양이 많아져서 이미지로 첨부하지 않았음. 웹 요청의 단계는

  • A라는 서버에 '요청'을 하고 →
  • '응답' 결과를 분류해서 →
  • 각 결과에 맞는 디스패치 로직 작성 →
  • 사용자 화면에 렌더링하는 단계를 거치게 만들었음 

[1] '요청'을 위한 api 작성

API를 호출하는 함수를 따로 작성해서 나중에 보기 쉽게 만들었다. 그 외 큰 이유는 없음.

// 파일 경로 : lib/api.js
import axios from "axios";

export const getPost = id => (
    axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
);

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

[2] 디스패치 로직 작성

디스패치 로직이라고 했는데 결국 '응답'의 결과(대기/성공/실패)에 맞는 액션 타입을 정의하고, 각 액션을 발생시켜 이를 처리해줄 리듀서 함수를 작성했다는 뜻.

// 파일경로 : modules/sample.js

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 = 'sample/GET_USERS_SUCCESS';
const GET_USERS_FAILURE = 'sample/GET_USERS_FAILURE';

const initialState = {
    loading: {
        GET_POST: false,
        GET_USERS: false,
    },
    post: null,
    users: null,
};

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; // 나중에 컴포넌트단에서 에러 조회 가능하도록 해줌
    }
};

export const getUsers = () => async dispatch => {
    dispatch({ type: GET_USERS });

    try {
        const response = await api.getUsers();
        // 요청 성공
        dispatch({
            type: GET_USERS_SUCCESS, 
            payload: response.data
        });
    } catch (e) {
        dispatch({
            type: GET_USERS_FAILURE,
            payload: e,
            error: true
        })
        throw e;
    }
};

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 },
        }),
        [GET_USERS]: state => ({
            ...state, 
            loading: { ...state.loading, GET_USERS: true } 
        }),
        [GET_USERS_SUCCESS]: (state, action) => ({
            ...state,
            loading: { ...state.loading, GET_USERS: false },
            users: action.payload
        }),
        [GET_USERS_FAILURE]: (state, action) => ({
            ...state,
            loading: { ...state.loading, GET_USERS: false },
        })
    }, initialState
);

export default sample;
// 파일경로 : modules/index.js

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

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

export default RootReducer;

 

[3] 렌더링

// 파일경로 : components/Sample.js

function Sample({ loadingPost, loadingUsers, post, users }) {
    return(
        <div>
            <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>
        </div>
    )
}

export default Sample;
// 파일경로 : containers/SampleContainer.js

import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Sample from '../components/Sample';
import { getPost, getUsers } from '../modules/sample';

function SampleContainer() {
    const post = useSelector(state => state.sample.post);
    const users = useSelector(state => state.sample.users);
    const loadingPost = useSelector(state => state.sample.loading.GET_POST);
    const loadingUsers = useSelector(state => state.sample.loading.GET_USERS);
    
    const dispatch = useDispatch();

    useEffect(() => {
        dispatch(getPost(1));
        dispatch(getUsers(1));
    }, [getPost, getUsers]);

    return(
        <Sample post={post} users={users} loadingPost={loadingPost} loadingUsers={loadingUsers} />
    );
}

export default SampleContainer;

 

3.1 리팩토링

위 코드들이 너무 많아서 어디서부터 손봐야하나 고민이 되는데, 여하튼 먼저 modules/sample.js 리듀서를 보자. 

getUsers와 getPost의 성공/실패를 다루는 코드가 유사하게 생겨서 중복이 발생하고, 각 요청에 따른 로딩상태도 관리해줘야 하므로 개선이 필요할거같다. 

 

1) 로딩 관리하기(로딩 리듀서)

기존엔 sample 리듀서 내부에서 요청별 액션 디스패치마다 로딩 상태를 변경해줘야 했는데, 이걸 따로 빼서 로딩 상태만 관리하는 리듀서를 만들어보자. 

리듀서를 다 작성했으면 RootReducer로 가서 하나로 합쳐주는것도 잊지 말자

2) 성공/실패 관리하기(유틸함수)

여기는 단순히 코드가 중복되는 부분들을 하나로 합쳐서 빼내자는 의도이므로 리듀서를 통짜로 새로이 만들어줄 필요는 없고, 유틸 함수로 lib폴더에서 적당히 합쳐주기만 하면 된다.

코드 중간에 params는 12번째 게시글을 가져온다고 치면 getPost(12)라고 할텐데, getPost함수안에 전달된 파라미터를 뜻함

3) 수정된 코드 반영하기

이제 위에서 수정한 코드들을 sample 리듀서에 반영해주기만 하면 끝난다.

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

const GET_POST = 'sample/GET_POST';
const GET_POST_SUCCESS = 'sample/GET_POST_SUCCESS';

const GET_USERS = 'sample/GET_USERS';
const GET_USERS_SUCCESS = 'sample/GET_USERS_SUCCESS';

const initialState = {
    post: null,
    users: null,
};

export const getPost = createRequestThunk(GET_POST, api.getPost);
export const getUsers = createRequestThunk(GET_USERS, api.getUsers);

const sample = handleActions(
    {
        [GET_POST_SUCCESS]: (state, action) => ({
            ...state,
            loading: { ...state.loading, GET_POST: false },
            post: action.payload
        }),
        [GET_USERS_SUCCESS]: (state, action) => ({
            ...state,
            loading: { ...state.loading, GET_USERS: false },
            users: action.payload
        }),
    }, initialState
);

export default sample;

 

 

' > 리액트를 다루는 기술' 카테고리의 다른 글

[19장] 코드 스플리팅  (0) 2022.04.12
[18장] 리덕스 미들웨어(redux-saga)  (0) 2022.04.12
[17장] 리덕스 활용(3)  (0) 2022.04.02
[17장] 리덕스 활용(2)  (0) 2022.04.02
[17장] 리덕스 활용(1)  (0) 2022.04.02