본문 바로가기

Redux

[Redux] 🌤 Redux-saga를 사용해 간단한 날씨 앱 만들기

본 글은 도서 '리액트를 다루는 기술, redux-saga을 참고하여 작성된 글입니다.

이전 글에서 redux-thunk를 통해 간단한 날씨 앱을 만들어 보았습니다. 이어서 이번 글에서는 redux-thunk 다음으로 가장 많이 사용되는 redux-saga를 사용하여 이전 글과 동일한 기능의 프로젝트를 만들어 보려 합니다.

 

🧰 redux-saga

redux-saga는 위에서 언급한 바와 같이 redux-thunk 다음으로 가장 많이 사용되는 리덕스 미들웨어입니다. redux-thunk를 다시 생각해보면, 액션 객체 대신, 함수를 디스패치할 수 있게 도와주는 도구라고 했습니다.

 

오늘 사용할 redux-saga는 액션을 모니터링하고 있다가 특정 액션이 발생하면 이에 따라 특정 작업을 수행하는 방식을 사용합니다. 이는 특정 자바스크립트 코드를 수행하거나, 특정 액션을 디스패치한다던지, 현재 상태 또한 불러올 수 있습니다. 

 

이런 동작을 redux-saga에서는 Generator라는 문법을 통해 구현합니다. 

👀  Generator 문법을 사용해보자.

가령, 함수에서 여러번에 걸쳐 반환을 하는 것은 불가능합니다. 아래와 같은 함수는 첫 번째로 오는 1을 반환하고 함수가 종료됩니다.

function fn1() {
  return 1;
  return 2;
  return 3;
}

 

하지만, 이를 가능하게 해주는 문법이 바로 Generator 입니다. 제네레이터 함수는 next()를 통해 함수를 실행하고, yield를 만나게 되면 값을 반환 후 yield가 위치하는 곳에서 코드의 흐름이 멈추게 됩니다. 

function* generatorFn1() {
  console.log("Start");
  yield 1;
  console.log("ing...");
  yield 2;
  console.log("Finish!");
  return 3;
}

const generator = generatorFn1();

generator.next();   //Start
generator.next();   //ing...
generator.next();   //Finish!

 

또한, 제네레이터 함수는 next()를 호출할 때 인자를 받아 함수에서 사용할 수 있습니다.

function* monitorGenerator() {
  console.log("Start!");
  const actionType = yield;
  if (actionType.type === "ADD") {
    console.log("추가되었습니다.");
  }
  if (actionType.type === "DELETE") {
    console.log("삭제되었습니다.");
  }
}

const monitor = monitorGenerator();

monitor.next();
monitor.next({ type: "ADD" });

redux-saga는 이러한 방식을 통해 특정 액션을 인자로 받아 특정 작업을 수행할 수 있게 됩니다.

 

🙂 한번 사용해볼까?, 웹 요청 처리하기

이제 직접 사용해볼까 하는데, 이전글에서 만들었던 'redux-thunk를 사용한 간단한 날씨 앱'을 수정하면서 만들어 볼 예정이니, 이전 글에서 사용했던 프로젝트를 이어서 작성하시거나, 아래 링크에서 다운로드 혹은 클론 하여 사용해주세요!

 

 

youthfulhps/noteapp-react-redux-thunk

리덕스 성크를 공부하기 위한 간단한 노트앱입니다. Contribute to youthfulhps/noteapp-react-redux-thunk development by creating an account on GitHub.

github.com

또한, 오늘 만들어볼 프로젝트의 완성 코드는 아래 링크에 있으니 참고 바랍니다.

 

youthfulhps/noteapp-react-redux-saga

리덕스 사가를 공부하기 위한 간단한 노트앱입니다. Contribute to youthfulhps/noteapp-react-redux-saga development by creating an account on GitHub.

github.com

터미널에서 다운로드 혹은 클론하신 파일 디렉터리로 이동해 아래와 같이 입력하면 프로젝트 환경에서 필요한 모듈이 설치가 됩니다.

~$ yarn

잘 설치되어 실행되는 지 확인도 해보세요!

~$ yarn start

 

모두 잘 되셨다면, 이제 redux-saga를 통해 간단한 날씨 앱을 만들어 보도록 하겠습니다.

 

✔ redux-saga 모듈 설치 및 적용

우선, 오늘의 주인공인 redux-saga를 설치해주도록 하겠습니다.

~$ yarn add redux-saga

리덕스 사가에서는, 특정 액션을 모니터링하고, 해당 액션이 주어지면 제네레이터 함수를 실행시켜 비동기 작업을 처리 후 액션을 디스 패치하는 과정을 기억하시면 좋습니다.

 

✔ index.js 파일 수정

redux-thunk 프로젝트에서는 redux-thunk가 리덕스 미들웨어로 적용되어 있는데, 이번엔 redux-saga를 적용하기 위해 아래와 같이 수정해보도록 하겠습니다.

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { Provider } from "react-redux";

import { createStore, applyMiddleware } from "redux";
import rootReducer from "./store/modules";

import { createLogger } from "redux-logger";
// import reduxThunk from "redux-thunk"; //삭제
import createSagaMiddleware from "redux-saga"; //추가

const reduxLogger = createLogger();
// const devTools =
//   window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();

const sagaMiddleware = createSagaMiddleware(); //추가

const store = createStore(
  rootReducer,
  applyMiddleware(reduxLogger, sagaMiddleware) //수정
); //수정
console.log(store.getState() + "스토어");

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

serviceWorker.unregister();

✔ src/store/modules/weather.js 수정

redux-thunk로 작성되어 있는 코드를 redux-saga로 수정해 주겠습니다. 아래와 같이 코드를 작성해 주세요.

import { handleActions, createAction } from "redux-actions";

import axios from "axios";
import { call, put, takeEvery } from "redux-saga/effects"; //추가

const GET_WEATHER_PENDING = "GET_WEATHER_PENDING";
const GET_WEATHER_SUCCESS = "GET_WEATHER_SUCCESS";
const GET_WEATHER_FAILURE = "GET_WEATHER_FAILURE";

function getAPI() {
  return axios.get(
    "http://api.openweathermap.org/data/2.5/weather?q=Seoul&units=metric&APPID=e75a0a68adc50371c5898d8d43931062"
  );
}

export const getWeather = createAction(GET_WEATHER_PENDING); //추가

function* getWeatherSaga(action) {
  //추가
  try {
    const response = yield call(getAPI, action.payload);    //(1)
    yield put({ type: GET_WEATHER_SUCCESS, payload: response.data });  //(2)
  } catch (e) {
    yield put({ type: GET_WEATHER_FAILURE, payload: e });
  }
}

export function* weatherSaga() {
  yield takeEvery("GET_WEATHER_PENDING", getWeatherSaga);   //(3)
}

/*  삭제
export const getWeather = () => async (dispatch) => {
  dispatch({ type: GET_WEATHER_PENDING });

  try {
    const response = await getAPI();
    dispatch({
      type: GET_WEATHER_SUCCESS,
      payload: response.data,
    });
  } catch (err) {
    dispatch({
      type: GET_WEATHER_FAILURE,
      payload: err,
    });
    throw err;
  }
};
*/

const initialState = {
  loading: false,
  error: false,
  data: {
    area: "",
    temp: 0,
    weather: "",
  },
};

export default handleActions(
  {
    [GET_WEATHER_PENDING]: (state, action) => {
      return {
        ...state,
        loading: true,
        error: false,
      };
    },
    [GET_WEATHER_SUCCESS]: (state, action) => {
      const area = action.payload.name;
      const temp = action.payload.main.temp;
      const weather = action.payload.weather[0].main;

      return {
        ...state,
        loading: false,
        data: {
          area: area,
          temp: temp,
          weather: weather,
        },
      };
    },
    [GET_WEATHER_FAILURE]: (state, action) => {
      return {
        ...state,
        loading: false,
        error: true,
      };
    },
  },
  initialState
);

 

첫번째로 추가한 코드는 'redux-saga/effects'라는 것인데, 리덕스 사가에서 사용되는 유용한 기능들이 담긴 유틸 함수들을 사용할 수 있습니다. 

 

주석 (1) 에서 사용된 call은 특정함수를 호출하고, 결과가 반환될 때까지 기다려줍니다. 현재는 첫 번째 파라미터로 호출하고자 하는 함수를 넣어주었는데, 호출하고자 하는 함수에 파라미터가 필요하다면, call의 두 번째 파라미터로 넣어주시면 됩니다.

 

주석 (2) 에서 사용된 put은 새로운 액션을 디스 패치해주는 아주 중요한 역할을 합니다. 위 코드와 같이 상황에 맞게 다른 액션을 디스 패치해줄 수 있게 도와줍니다.

 

주석 (3) 에서 사용된 takeEvery는 특정 액션에 대하여 디스 패치되는 모든 액션을 처리합니다. 또한, 위 코드에서 사용되지는 않았지만, takeLatest는 특정 액션에 대하여 가장 마지막에 디스 패치되는 액션만을 처리합니다. 이를 통해 특정 액션이 처리되고 있는 과정에서 새로운 액션이 디스 패치된다면, 기존에 진행하고 있었던 액션을 무시하고 가장 최근에 발생한 액션을 처리합니다. 

 

✔ src/store/modules/index.js 수정

import { combineReducers } from "redux";
import note from "./note";
import weather, { weatherSaga } from "./weather";  //수정
import { all } from "redux-saga/effects";   //추가

export default combineReducers({
  note,
  weather,
});

export function* rootSaga() {   //추가
  yield all([weatherSaga()]);   //(1)
}

 

주석 (1)에서 사용된 all은 배열안에 있는 사가를 동시에 실행시켜 줍니다. 지금은 weatherSaga 하나만을 생성했지만, 여러 개의 사가가 필요하다면, 배열 안에 추가하시면 됩니다.

 

✔ index.js 수정

마지막으로 index.js에서 rootSaga를 가져와 주겠습니다.

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { Provider } from "react-redux";

import { createStore, applyMiddleware } from "redux";
import rootReducer, { rootSaga } from "./store/modules"; //수정

import { createLogger } from "redux-logger";

import createSagaMiddleware from "redux-saga";

const reduxLogger = createLogger();
// const devTools =
//   window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  rootReducer,
  applyMiddleware(reduxLogger, sagaMiddleware)
);

sagaMiddleware.run(rootSaga); //추가
console.log(store.getState() + "스토어");

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

serviceWorker.unregister();

✔ App 실행

아래와 같이 잘 작동되시길 바랍니다!

 

1. 날씨 불러오기 버튼 클릭 전

2. 날씨 불러오기 버튼 클릭 후 (로딩 중)

(데이터가 보이는 곳에 로딩 스핀이 돌고 있는 모습입니다.)

3. 날씨 불어오기 버튼 클릭 후 (성공)