[React.js] #10 비동기 API 연동하기(Redux) + 예제
이번 포스팅에서는 React에서 비동기 API를 사용하는 방법에 대해서 알아보겠습니다.
상태 관리에 대해서는 앞에서 배운 Redux를 활용하여 진행하고 비동기 처리를 위해 Redux 미들웨어인 Thunk를 사용합니다. 미들웨어 없이 비동기 처리를 시도해보았으나 안 되는 것 같아서 미들웨어를 사용하게 되었습니다.
비동기 API 예제를 만들어보자!
우선, 만들고자 하는 기능은 아래 이미지와 같습니다.
동작 과정
1. 버튼에 클릭하면 API를 발생시킵니다.
2. 비동기로 동작하기 떄문에 API 요청에 대한 결과가 오기 전에는 "Loading" 문구를 화면에 랜더링 해줍니다.
3. 비동기 요청이 완료된 경우 데이터를 화면에 랜더링 해줍니다.
4. 에러가 발생한 경우에는 "error"를 화면에 랜더링 해줍니다.
디렉토리 구조는 아래와 같이 만들어 사용하였습니다.
컴포넌트는 Button과 Content 그리고 컴포넌트 관리를 위한 Container입니다. API 요청 관리를 위해 /lib/api.js파일을 생성하였고 비동기 처리 함수 관리를 위해 /lib/asyncPromiseThunk.js 파일을 생성하였습니다.
store에는 redux에서 사용할 액션을 정의하고 dispatch시 동작할 reducer를 정의해줍니다.
우선, 프로젝트에서 HTTP 요청을 사용하기 위해 패키지 설치를 진행합니다. 이번 프로젝트에서는 "axios"를 통해 http 요청을 진행하고 async/await 문법을 통해 비동기에 대한 관리를 진행하도록 하겠습니다.
추가로 사용할 "redux" 그리고 "react-redux"를 함께 설치해줍니다.
yarn add axios redux react-redux
먼저 컴포넌트에 관련된 코드를 하나씩 살펴봅니다.
# App.js
import React, { Component } from 'react';
import ApiContainer from 'containers/ApiContainer'
class App extends Component {
render() {
return (
<div>
<ApiContainer />
</div>
);
}
}
export default App;
App.js에서는 특별한 부분은 없습니다. 실제 로직관리를 담당할 컨테이너를 만들어주고 컨테이너에서는 데이터 출력을 위해 Presenter를 담당할 컴포넌트들을 관리(?)하여 줍니다.
# src/components/containers/ApiContainer
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as counter from 'store/actions/counter';
import * as api from 'store/actions/api';
import Button from 'components/common/Button';
import Content from 'components/Contents';
class ApiContainer extends Component {
getDataApi = async (e) => {
const { api, counter, dispatchApiUpdate, dispatchCounterUpdate } = this.props
dispatchCounterUpdate();
if(api.status) {
dispatchApiUpdate(counter);
}
}
componentDidMount() {
const { api, counter, dispatchApiUpdate, dispatchCounterUpdate } = this.props
if(api.data) return;
dispatchCounterUpdate();
dispatchApiUpdate(counter);
}
render() {
let content, button = null;
if(this.props.api.status) {
content = <Content api={this.props.api} counter={this.props.counter}/>
button = <Button onClickHandler= {this.getDataApi} api={this.props.api}/>
} else if(this.props.api.error) {
content = <div>error...</div>
} else {
content = <div>loading...</div>
}
return (
<div>
{button}
{content}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
counter: state.counter.number,
api: state.api
}
}
const mapDispatchToProps = (dispatch) => {
return {
dispatchApiUpdate: (id) => (dispatch(api.getDataSample(id))),
dispatchCounterUpdate: () => (dispatch(counter.increase()))
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ApiContainer);
Container입니다. Class 선언 부분에는 초기 데이터 로딩과 이벤트 핸들링을 위한 함수 그리고 데이터 존재 유/무에 따라 어떻게 컴포넌트를 디스플레이할 것 인지에 대한 로직을 담당하고 있습니다.
아래 부분에서는 connect 함수는 리덕스 사용을 위해 state와 dispatch 함수를 컴포넌트의 prop로 전달하는 역할을 합니다. connect 함수는 HOC입니다. Hook에서는 이런 HOC 패턴이 가져오는 복잡성을 낮추기 위해 개발되었고 Hook의 도입으로 인해 사용의 필요성이 거의 없어진 상태입니다.
** Hook에 대해 궁금하신 경우 이전 포스팅을 확인해주세요.
다음은 버튼과 컨텐츠 컴포넌트입니다.
# src/components/common/Button.js
import React from 'react';
const Button = ({onClickHandler, api}) => {
return (
<div>
<button onClick={onClickHandler}>api call!!</button>
</div>
)
}
export default Button;
# src/components/Content.js
const Content = ({api, counter}) => {
return (
<div>
<div>{api.data.id}</div>
<div>{api.data.title}</div>
<div>{api.data.body}</div>
</div>
)
}
export default Content
다음은 Redux와 관련된 코드 부분입니다.
# sotre/action/api.js
import * as api from 'lib/api'
import { createPromiseThunk } from 'lib/asyncPromiseThunk';
// 액션 생성
const API_CALL = 'API_CALL'
const API_LOAD = 'API_LOAD'
const API_FAILURE = 'API_FALURE'
export const getDataSample = createPromiseThunk('API', api.getData)
// 초기 state
const initialState = {
data: null,
status: false,
error: null,
}
// reducer 생성
export default function apiReducer(state=initialState, action) {
switch(action.type) {
case API_CALL:
return {
...state,
data: null,
status: false,
error: null,
}
case API_UPDATE:
return {
...state,
data: {
title: action.data.title,
id: action.data.id,
body: action.data.body
},
status: true,
error: false
}
case API_FAILURE:
return {
data: null,
status: false,
error: true,
}
default:
return state
}
}
총 세 개의 state를 가지고 있습니다. 실제 데이터를 가지고 있을 data 객체 그리고 현재 API가 어떤 상태인지 판별해줄 status 객체 애러가 발생한 경우 애러 여부 판별을 위한 error 객체입니다.
액션은 이에 따라 "API 요청", "API 로드", "API 실패" 총 3가지의 액션으로 구분해주었습니다. 액션의 type에 따라 리듀서에서는 state에 대한 처리를 진행하고 immutable 하게 새로운 객체를 return 합니다.
# store/actions/counter.js
export const INCREMENT = 'counter/INCREMENT';
export const DECREMENT = 'counter/DECREMENT';
const initialState = {
number: 1,
}
export const increase = () => ({ type: INCREMENT })
export const decrease = () => ({ type: DECREMENT })
export default function counter(state = initialState, action) {
switch(action.type) {
case INCREMENT:
return {
number: state.number+1,
}
case DECREMENT:
return state.number-1
default:
return state
}
}
버튼 이벤트가 발생할 때 요청할 API의 path parameter를 결정하기 위해 counter 값을 관리해주는 부분입니다.
해당 부분은 비동기 처리가 이루어지지 않습니다. 이전 포스팅에서 진행했던 예제처럼 단순히 counter 값을 증가시키고 감소시키는 역할을 합니다.
다음은 실제 비동기 처리를 위한 부분입니다.
## store/actions/api.js
// 함수 호출부분
export const getDataSample = createPromiseThunk('API', api.getData)
## lib/api.js
import axios from 'axios';
export const getData = (id) => (axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`))
## lib/asyncPromiseThunk.js
export const createPromiseThunk = (type, promiseCreator) => {
const [UPDATE, LOAD, FAILURE] = [`${type}_CALL`, `${type}_LOAD`, `${type}_ERROR` ]
return param => async dispatch => {
dispatch({type: CALL})
try {
const res = await promiseCreator(param)
dispatch({type: LOAD, data: res.data})
} catch(e) {
dispatch({type: FAILURE, error: e})
}
}
}
해당 함수는 첫 번째 인자로 액션에 대한 타입을 받고 두 번째 인자로 비동기 처리를 위한 promise 생성자 함수를 전달받습니다. 리덕스 미들웨어를 통해 전달받은 dispatch 함수를 통해 상황에 맞는 액션을 dispatch 합니다.
마지막으로 미들웨어 등록 부분입니다.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk'
import counter from 'store/actions/counter.js';
import api from 'store/actions/api'
const reducers = combineReducers({
counter,
api,
})
let store = createStore(reducers, applyMiddleware(thunk));
export default store;
위에서 생성한 두 가지의 reducer를 하나의 reducer로 통합해주는 작업을 진행하고 redux store에 reducer와 middleware를 등록하여 줍니다.
미들웨어를 사용하지 않으면 어떻게 될까?
미들웨어를 사용하지 않고 액션 생성자를 다음과 같이 수정하였습니다.
export const increase = async () => ({ type: INCREMENT })
단순히 async/await 문법을 사용하기 위해 async 함수로 선언하였습니다.
리덕스에서 기본적으로 액션 객체를 생성합니다. 하지만 async 함수를 통해 뭔가 비동기적인 작업이 이루어지게 되면 액션 객체에 대한 조작이 이루어질 수 있으니 다음과 같은 오류가 발생하는 것 같습니다.
특별한 설명 없이 예제를 만들어보며 비동기 API를 처리해줄 수 있는 방법에 대하여 공부해보았습니다.
비동기 처리를 위해서 사용하는 middleware나 HTTP 사용을 위한 axios 등 react와 redux를 사용함에 있어 연관되는 개념들이 많아 아직 많이 헷갈리지만 하나하나 알아가는 게 재미는 있는 것 같습니다.
추후에 이런 개념들에 대해 좀 더 자세하게 정리해두어야 할 것 같습니다.
탕빠이!