본문 바로가기
Programming/Javascript

Redux-Saga 를 활용한 비동기 요청 예제

by Wilkyway 2021. 6. 29.
반응형

1. 프로젝트 생성

yarn create react-app .

2. 라이브러리 설치

yarn add redux react-redux redux-actions axios redux-saga redux-devtools-extension

3. 프로젝트 구성

프로젝트 구성은 아래와 같이 할 계획입니다. /src 하위부분만 살펴보면 되겠습니다.

/src

ㄴ components

      * Leader.jsx

ㄴ containers

      * LeaderContainer.jsx

ㄴ lib

      * api.js (서버에 요청하는 비동기 함수 정의부분)

ㄴ modules

      * index.js (redux와 saga를 통합하는 부분)

      * leader.js ( action 타입, action 생성함수, redux, saga 정의하는 부분)

* App.js (컴포넌트 로딩하는 부분)

* Index.js (스토어 생성, Middleware 적용)

 

 

 

 

 

 

 

 

(1) src/lib/api.js

우선 서버에 요청을 담당하는 함수를 먼저 정의합니다. JSONPlaceholder라는 곳에서 제공되는 가짜 API를 사용합니다.

import axios from "axios";

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

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

(2) src/modules/leader.js

Action Type과 Action Generator, Action Handler(Reducer), Saga를 정의합니다.

import { createAction, handleActions } from "redux-actions";
import { call, put, takeLatest } from "redux-saga/effects";
import * as api from "../lib/api";

const GET_POST = "leader/GET_POST"; // post 를 요청하는 액션
const GET_POST_SUCCESS = "leader/GET_POST_SUCCESS"; // post 요청 성공 시 데이터(post)를 전달하는 액션
const GET_POST_FAILURE = "leader/GET_POST_FAILURE"; // post 요청 실패 시 처리하는 액션

const GET_USERS = "leader/GET_USERS"; // users 를 요청하는 액션
const GET_USERS_SUCCESS = "leader/GET_USERS_SUCCESS"; // users 요청 성공 시 데이터(users) 를 전달하는 액션
const GET_USERS_FAILURE = "leader/GET_USERS_FAILURE"; // users 요청 실패 시 처리하는 액션

export const getPost = createAction(GET_POST, (id) => id); // payload가 id인 액션객체 생성
export const getUsers = createAction(GET_USERS); // payload가 없는 액션객체 생성

// state 의 초기값 설정
const initialState = {
  post: null,
  users: null,
};

//** Redux **//
// post/users 요청 성공 시 Data 처리하는 redux만 정의
// post/users 요청 실패 시 처리할 데이터 없음
// post/users 요청 액션은 Saga에서 수행
const leader = handleActions(
  {
    [GET_POST_SUCCESS]: (state, action) => ({
      ...state,
      post: action.payload,
    }),
    [GET_USERS_SUCCESS]: (state, action) => ({
      ...state,
      users: action.payload,
    }),
  },
  initialState
);

export default leader;

//** Saga **//
// GET_POST/GET_USERS 액션에 대한 서버요청을 위한 Saga
export function* leaderSaga() {
  yield takeLatest(GET_POST, getPostSaga);
  yield takeLatest(GET_USERS, getUsersSaga);
}

// GET_POST 액션 dispatch 시 실행될 함수. 비동기로 서버의 post 데이터 요청
function* getPostSaga(action) {
  try {
    const post = yield call(api.getPost, action.payload);
    // post 데이터요청하는 비동기함수인 getPost을 요청하며, 인자로 action.payload인 id를 넘김
    yield put({
      type: GET_POST_SUCCESS, // 요청 성공 액션을 dispatch함.
      payload: post.data, // 액션 dispatch할 때 payload로 post.data(포스트 내용)을 전달함
    });
  } catch (e) {
    yield put({
      type: GET_POST_FAILURE, // 요청 실패 액션을 dispatch함
      payload: e, // 액션 dispatch할 때 payload로 에러를 전달함
      error: true, // 액션 객체의 항목에 error: true를 넘겨줌
    });
  }
}

// GET_USERS 액션 dispatch 시 실행될 함수. 비동기로 서버의 users 데이터 요청
function* getUsersSaga() {
  try {
    const users = yield call(api.getUsers);
    // users 데이터를 요청하는 비동기함수인 getUsers를 요청하며, 전달인자는 없음.
    yield put({
      type: GET_USERS_SUCCESS,
      payload: users.data,
    });
  } catch (e) {
    yield put({
      type: GET_USERS_FAILURE,
      payload: e,
      error: true,
    });
  }
}

(3) src/modules/index.js

reducer, saga 를 통합해줍니다.

import { combineReducers } from "redux";
import leader, { leaderSaga } from "./leader";
import { all } from "redux-saga/effects";

const rootReducer = combineReducers({
  leader,
});

export default rootReducer;

export function* rootSaga() {
  yield all([leaderSaga()]);
}

(4) src/index.js (스토어 생성)

Saga를 불러오고, Saga를 포함한 미들웨어를 적용하여 스토어를 생성하고, Saga를 실행합니다.

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

import createSagaMiddleware from "@redux-saga/core";
import { applyMiddleware, createStore } from "redux";
import rootReducer, { rootSaga } from "./modules";
import { composeWithDevTools } from "redux-devtools-extension";
import { Provider } from "react-redux";

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

(5) src/containers/LeaderContainer.jsx

이제 데이터를 받아올 통로는 만들어졌으므로, 데이터를 실제 요청하고, 받은 데이터는 하위로 보내주는 데이터 담당 Container를 작성합니다. 

import React from "react";
import { connect } from "react-redux";
import Leader from "../components/Leader";
import { getPost, getUsers } from "../modules/leader";
import { useEffect } from "react";

const LeaderContainer = ({ getPost, getUsers, post, users }) => {
  useEffect(() => {
    getPost(1); // 액션 생성함수 실행('leader/GET_POST' 액션타입, payload: id=1)
    getUsers(); // 액션 생성함수 실행
  }, [getPost, getUsers]); // getPost ,getUsers에 변경이 있을 시 마다 호출
  return <Leader post={post} users={users} />;
};

export default connect(
  ({ leader }) => ({
    // props로 전달해 줄 post/users에, state의 leader에서 각각의 값 추출하여 전달
    post: leader.post,
    users: leader.users,
  }),
  {
    getPost, // props로, dispatch 함수 중 Post요청 액션생성함수 전달
    getUsers, // props로, dispatch 함수 중 Users요청 액션생성함수 전달
  }
)(LeaderContainer);

 

(6) src/components/Leader.jsx

최종적으로 데이터를 받아서 표현할 컴포넌트를 구현합니다.

import React from "react";

const Leader = ({ post, users }) => {
  return (
    <div>
      <section>
        <h1>포스트</h1>
        {post && (
          <div>
            <h3>{post.title}</h3>
            <h3>{post.body}</h3>
          </div>
        )}
      </section>
      <hr />
      <section>
        <h1>사용자 목록</h1>
        {users && (
          <ul>
            {users.map((user) => {
              return (
                <li key={user.id}>
                  {user.username} ({user.email})
                </li>
              );
            })}
          </ul>
        )}
      </section>
    </div>
  );
};

export default Leader;

주의할 점은 {post && ...} 또는 {users && ...} 가 없을 경우 최초 로딩 시 아직 Data 전달이 안된 시점에는 에러를 띄우므로 데이터가 있을 경우에만 출력하도록 해야합니다.

 

(7) src/App.js

앱에서 불러와 출력합니다.

import "./App.css";
import LeaderContainer from "./containers/LeaderContainer";

function App() {
  return (
    <div className="App">
      <LeaderContainer />
    </div>
  );
}

export default App;

 

<결과>

 

 

~끝~

반응형

댓글