til - redux, redux-saga, createAction

백승일·2020년 11월 18일
0

TodayILearn

목록 보기
6/9

이번 시간에는 내가 만들었던 어때시네마에서 사용한 기술인 redux에 대해서 정리하려 한다. 그럼 먼저 redux가 뭔지 부터 살펴보도록 한다.

redux가 뭔데

redux란?

이걸 알아 보기 위해 일단 redux의 홈페이지로 갔다. 어떤 라이브러리든 메인 홈페이지에 가면 이게 뭔지 정의해두기 마련이지.

A Predictable State Container for JS Apps

아..예측 가능한 상태 컨테이너라..일단 주변에서 들은 것으로는 상태를 저장하는 역할을 한다고 알고 있으니 상태 컨테이너라는 말은 이해를 하겠는데 예측 가능하다? 는 뭐지

Redux helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test

아 리덕스는 다양한 환경에서 일관되게 작동하니까 어디서 사용하던 예측 가능하다라는 의미 인것 같다.

그니까 결국 redux팀이 설명하는 리덕스는 어느 환경에서도 일관되게 동작하는 상태 보관 상자라는 거라는 것을 알았다. 그럼 이제 이 상자를 어떻게 조립하는지, 또 상자에 넣을 상태를 어떻게 만지는 지 , 어떻게 넣을지 알아보자.

redux라는 상자는 말이야

이 부분에 대한 설명도 결국 오피셜한 docs를 읽어보도록 하자.

여기 예시에서 써있는 것 처럼
1. redux는 "단일 스토어"에 저장된다.
2. 상태를 변경하는 유일한 방법은 action을 만들고 스토어에 전달하는 것이다.
3. 상태의 업데이트 방식을 지정하려면 pure reducer함수를 작성해야한다.

//redux에서 store를 만드는 함수를 가져오고
import { createStore } from 'redux';

//내부 동작을 지정하는 reducer함수를 선언한다. 이때 첫 번째 매개변수는 기본값설정을 해준다. 아니면 함수 밖에 선언해 놓던가 
function counterReducer(state = { value: 0 }, action) {
  //switch문에 따라 action에 맞게 store가 변경되는 부분
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + 1 }
    case 'counter/decremented':
      return { value: state.value - 1 }
    default:
      return state
  }
};
// 내부 동작 설정을 마치고 store를 생성하는 부분
let store = createStore(counterReducer)
// 상태 변경은 이렇게
store.dispatch({ type: 'counter/incremented' })

출처 : https://redux.js.org/introduction/getting-started

redux라는 상자 곧 store의 생성은 대충 이렇다. 여기서 조금 더 살펴볼 부분은 store를 생성하는 createStore함수이다. 공식문서의 이 함수 사용법을 좀 보면 함수의 첫 인수는 reducer함수, 두번 재는 preloadedState라 해서 초기값, 마지막에 다른 미들웨어들을 사용할수 있게 해주는 enhancer 부분이 있다.

const store = createStore(
    reducer(history),
    {},
    composeWithDevTools(
      applyMiddleware(routerMiddleware(history), SagaMiddleware),
    ),
  )
// 어때시네마의 store생성부분

여기서 잠깐, 어차피 상자면 상자이지 다른 기능이 뭐 필요하다고 미들웨어가 존재하나? 라 생각할 수 있다. 여기서 조금 생각해야 할 부분이 redux의 특징 3번에 pure reducer함수다. 요기 이다.

pure하다. 어디서 많이 들었지..순수...순수함수다. 결국 외부 상태에 영향받는 부수효과없고 같은 값을 입력하면 항상 같은 값을 뱉어야하는 그런 함수였다. 때문에 reducer도 입력에 따라 다른 값이 나올 수 있는 http request를 사용하면 안대겠지.

그때 사용하는 것이이제 redux-thunk나 redux-saga가 되시겠다.

호오...redux-thunk

이 정리글을 쓰기 위해 검색 하던 중 재밋는 글을 발견했다.

이 글을 정리해보는게 도움이 될 것 같아 정리해본다.

redux-thunk의 thunk는 내가 redux-thunk를 처음 봤을 때 머리에 울리는 소리란다. ㅋㅋㅋ 이름 참 잘지었다.

다시 진지하게 thunk는 함수가 함수를 반환하는 이름. 고차함수를 부르는 이름이라 한다.

function higherFuq(){
	return function thunk(){};
}

redux를 사용하기 위해서는 action이 필요하다. 이 action은 평범한 객체고 항상 반복해서 작성하는 것이 귀찮기 때문에 action-creator라는 개념을 가지고 있다.

const action = {type:"",use:""};
const actionCreator = ()=>({type:"",use:""})

액션 생성자 함수를 통해서 똑같은 액션을 반복해서 치지 않고 함수 호출로 생성할 수 있다. 이때 이 액션 생성자가 하는 행동이 그저 액션의 반환이라는 점이 좀 역할이 빈약하다 해야하나. 뭐 그렇게 보이기도 한다. redux-thunk의 핀트는 이 부분인 것 같다. action생성자 함수가 순수 함수인 reducer대신에 이들이 못하는 무언가를 해줬으면..

function logOutUser() {
  return function(dispatch, getState) {
    return axios.post('/logout').then(function() {
      // pretend we declared an action creator
      // called 'userLoggedOut', and now we can dispatch it
      dispatch(액션생성자함수());
    });
  };
}

그래서 redux-thunk에서는 액션생성자 함수를 이용하여 http-request등의 행위를 하는 것이다.

그러면 saga는 뭐고

앞서 본 thunk는 액션생성자를 이용하여 비동기처리를 하는 redux 미들웨어 정도로 정리할 수 있겠다. 일단 처음보면 공식 docs부터 보도록 하자.

You might've used redux-thunk before to handle your data fetching. Contrary to redux thunk, you don't end up in callback hell, you can test your asynchronous flows easily and your actions stay pure.

처음부터 thunk를 저격하고 있다. (뿌이뿌이뿌이)

오피셜 docs에 의하면 redux-saga는 제너레이터함수를 사용하여 비동기 흐름을 쉽게 파악하고 사용하나고 한다. 자 여기서 평소에 잘 안쓰는 기능이 두둥등장했다. 바로 제너레이터 이다.

쉽게 말해서 제너레이터 함수 내부에 여러 개의 브레이크가 있고, 한번 실행되면 브레이크 지점까지 실행되었다가 멈추고, 다시 실행하면 멈춘 코드 위치를 기억했다가 다시 실행한다고 한다.

프로젝트에서 사용한 redux-saga코드를 보면서 이야기해보자.

import { createActions, handleActions, createAction } from 'redux-actions';
import { put, call, takeEvery } from 'redux-saga/effects';
import MoviesServie from '../../Services/moviesServices';

// saga구분을 위한 prefix
const prefix = 'movies';

// createActions 라이브러리를 이용한 action생성자
const { start, success, fail } = createActions(
  {
    SUCCESS: (movies) => ({ movies }),
  },
  'START',
  'FAIL',
  { prefix },
);
// 초기값
const initialState = {
  loading: false,
  movies: [],
  error: null,
};

//reducer 
const movies = handleActions(
  {
    START: () => ({
      loading: true,
      movies: [],
      error: null,
    }),
    SUCCESS: (state, action) => ({
      loading: false,
      movies: action.payload.movies,
      error: null,
    }),
    FAIL: (state, action) => ({
      loading: false,
      movies: [],
      error: action.payload.error,
    }),
  },
  initialState,
  { prefix },
);
// 이후에 combineReducers을 이용한 바인딩을 위해서 외부로
export default movies;

//saga 함수파트

// 사가 액션명
const START_GET_MOVIES_SAGA = `START_GET_MOVIES_SAGA`;
// 사가 액션 생성자 함수
export const startGetMoviesActionCreator = createAction(START_GET_MOVIES_SAGA);
// saga 함수 선언
function* startGetMoviesSaga() {
  try {
    yield put(start());
    // 비동기 통신의 경우 Service클래스를 생성하여 구분해두었다.
    const movies = yield call(MoviesServie.getMovies);
    yield put(success(movies));
  } catch (error) {
    yield put(fail(error));
  }
}
// saga 함수와 액션생성자 묶기
export function* moviesSaga() {
  yield takeEvery(START_GET_MOVIES_SAGA, startGetMoviesSaga);
}

확실히 thunk에서는 then을 사용하여 처리를 해주는 것이 좀 지저분해 보였으나, saga의 경우 제너레이터를 이용하여 구분을 할 수 있으니 코드가 깔끔해보이고, 생성자함수도 본연의 역할만 하고 있는 것을 볼 수 있다.

하지만 이렇게만 해둔 다고 saga를 사용할 수는 없다. saga를 만들었으면 reducer들을 묶고 또 store의 미들웨어에 등록해주어야 비로소 사용할 수 있다.

const reducer = (history) =>
  combineReducers({
    selectData,
    theaters,
    movies,
    bookingData,
    router: connectRouter(history),
  });
export default reducer;

위에서 본 사가 파일들의 reducer들을 하나로 묶어주고

export default function* rootSaga() {
  yield all([
    theaterSaga(),
    selectDataSaga(),
    moviesSaga(),
    bookingSaga(),
    checkDataSaga(),
  ]);
}

saga들도 하나로 바인딩 해준다.

import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
import reducer from './modules/reducer';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './middlewares/saga';

export const history = createBrowserHistory();
const SagaMiddleware = createSagaMiddleware();
const Store = () => {
  const store = createStore(
    //reducer바인드
    reducer(history),
    //초기값없고
    {},
    //미들웨어 등록
    composeWithDevTools(
      applyMiddleware(routerMiddleware(history), SagaMiddleware),
    ),
  );
  // 사가 미들웨어 실행
  SagaMiddleware.run(rootSaga);
  return store;
};	

이렇게 미들웨어 등록후 실행해주는 함수를 선언해주고

  
import React from 'react';
import ReactDOM from 'react-dom';
import './normalize.css';
import App from './App';
import {Provider}from "react-redux"
import Store from './Redux/create';

const store = Store()

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

render함수가 있는 곳에서 선언해주면 스토어의 생성시점도 예측 가능해진다.

결론

thunk나 saga 둘다 결국 reducer에서 비동기 처리를 하기 위해서 존재하는 미들웨어라는 점을 기억하면 어떤 미들웨어를 사용하던지 금방 이해할 수 있을 것이라 생각한다.

profile
뉴비 개발자

0개의 댓글