리덕스 사가 이해하기

심재철
9 min readFeb 26, 2020

리덕스 사가는 Effects라고 불리는 함수들을 갖고 있습니다. 리덕스 사가 자체가 사이드이펙트(브라우저 캐시 접근, API호출등 함수 외부에 영향을 미치고 매번 결과가 달라질 수 있는 순수하지 않은 작업)를 처리하기 때문에 이런 이름이 붙지 않았을까? 추측해봅니다.

리덕스 사가의 Effects 종류

Fork()

매개변수로 전달된 함수를 비동기적으로 실행합니다. 비동기란 쉽게 말하면그 함수를 누군가에게 대신 실행하게 맡기고 나는 다른일을 하러 떠나는 것입니다.

Take()

매개변수로 전달된 액션이 올때까지 블락된 상태로 기다립니다.

Race()

effects들을 마치 경주하듯이 동시에 실행시켜놓고 먼저 완료되는애가 있으면 다른 effects들을 종료시켜버립니다.

Call()

Fork()가 함수를 비동기적으로 실행한다고 하면, Call()은 함수를 동기적으로 실행합니다. Call에 넘겨진 함수가 Promise를 리턴하면 그 Promise가 resolved 될 때까지 call()을 호출한 부분에서 실행이 멈춥니다.

Put()

액션을 dispatch 합니다. 보통 take로 액션을 캐치해서 api 호출을 call로 실행하고 성공/실패 여부에 따라 리덕스 스토어에 반영하기 위해서 호출하는 Effects입니다.

Select()

State에서 데이터를 꺼내오기 위한 함수입니다.

takeLatest

가장 마지막에(최신) 실행된 액션에 대해서만 핸들러를 실행합니다.

takeEvery

캐치된 모든 액션에 대해서 핸들러를 실행합니다.

리덕스 사가를 리덕스에 연결하기

const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, [], compose(
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);

리덕스 스토어를 만들때(createStore) 미들웨어로 sagaMiddleware를 연결해줘야합니다. sagaMiddleware에는 여러개의 saga가 합쳐진 rootSaga가 실행되고 있습니다.

사가 코드 예시 (유저 데이터 받아오기)

getDeparture, getFilght, getForecast 3개의 함수는 직렬적으로(순서대로) 실행됩니다. 서로가 의존하고 있습니다.

여기서는 비행편의 예시가 나옵니다. 대시보드에 비행편에 대한 정보를 보여주기 위해서는 먼저 고객의 정보가 필요합니다. 고객 정보를 활용하여 비행편을 조회하는 사가를 만들어봅시다.

rootSaga는 여러개의 local saga를 합친것입니다. 첫번째 loadUser 함수가 비동기적으로 실행됩니다.(fork)
유저의 데이터를 서버에서 받아와서 리덕스 스토어에 반영합니다.
const user = yield call(getUser);

getUser함수를 동기적으로 실행합니다. 이 함수가 user 데이터를 서버에서 받아올때까지 해당 라인에서 블럭됩니다.

yield put({type: 'FETCH_USER_SUCCESS', payload: user});

받아온 user 객체를 액션 객체에 넣어서 리덕스 스토어에 dispatch해줍시다.

  1. api 호출 성공시 : FETCH_USER_SUCCESS 액션 dispatch
  2. api 호출 실패시 : FETCH_USER_FAILURE 액션 dispatch

이제 user데이터를 서버에서 받아와서 우리의 리덕스 스토어에 넣는 작업을 성공적으로 마무리 했습니다. 이제 다음은 takeLatest부분을 살펴보죠.

두번째 라인 takeLatest 실행

해석을 해보면 다음과 같습니다.

LOAD_DASHBOARD라는 액션 여러개가 dispatch되어도 가장 최근에 실행된 액션에 대해서 loadDashboardSequenced 함수를 실행 시켜줘!

loadDashboardSequenced는 이렇게 생겼습니다.

이 함수가 실행되려면 앱 어디에선가 FETCH_USER_SUCCESS 액션이 dispatch 되어야 합니다.

유저의 데이터를 서버에서 받아오는 사가 입니다.

FETCH_USER_SUCCESS 액션은 아까전에 유저의 데이터를 서버에서 가져온 다음에 put으로 dispatch 해줬었죠.

한번더 정리해보자.

먼저 rootSaga가 실행되면,

  1. fork(loadUser);
    takeLatest(“LOAD_DASHBOARD”, loadDashboardSequenced);

서버에서 유저의 데이터를 가져옴과 동시에(loadUser),

takeLatest의 loadDashboardSequenced이 실행되고 그 안에선 FETCH_USER_SUCCESS 액션이 dispatch 되길 listen하고 있습니다.

2. 이때 loadUser안에서,

put({type: ‘FETCH_USER_SUCCESS’, payload: user}); 가 실행되면 Listen하고 있던 loadDashboardSequenced가 실행 됩니다. 그리고 그 함수 안에서는 payload로 전달한 user 데이터를 가지고 3번의 api 호출을 통해 대시보드에 필요한 데이터를 가져와서 put을 통해 리덕스 스토어에 최종 반영합니다. 리덕스 스토어의 상태가 바뀌면 그 상태를 구독하고 있던 리액트 컴포넌트들도 같이 바뀌겠죠.

yield는 함수 실행을 중간에 멈추게 만들 수 있습니다. next로 다시 재개할수 있는데 그때 yield에서 리턴된 값을 변수에 담을 수 있습니다.(const user)

사가를 병렬적으로 실행 시킬 수 있나요?

getForecastgetFlight 두개의 함수가 서로 의존하지 않아서 동시에 실행되도 상관없다면 어떻게 코드를 짜는게 좋을지 알아봅시다.

직렬적으로 대시보드 정보를 가져오는 loadDashboardSequenced 대신에 병렬적으로 가져오는 loadDashboardNonSequenced 함수를 살펴보겠습니다.

user데이터로 departure를 얻자마자 바로 또 다른 api를 호출하고 있습니다. 이렇게 되면 총 3개의 api가 모두 종료될때까지 UI는 업데이트 되지 않겠죠?? 유저는 얼마나 답답할까요. departure 정보를 구했으니까 일단 UI를 업데이트 하고 그다음에 나머지 2개의 api를 호출해도 충분하지 않을까요? 이건 조금 있다 설명해드리겠습니다.

루트 사가에 위 제네레이터 함수를 등록해줍시다.

yield 배열안에 함수 여러개를 실행시키면, 그 함수들은 병렬적으로 실행되고 모두 종료 될때까지 해당 라인에서 블럭됩니다.

위와 같이 루트 사가를 구성해버리면 yield배열 안에 있는 모든 함수가 종료될때까지 다른 일을 못합니다. 즉, 리액트가 화면을 렌더링 하지 못한다는 말입니다. 좀 더 빠르게 화면을 업데이트 할 수 있는 방법을 생각해야 합니다.

그러기 위해선 먼저 받은 데이터를 활용해서 바로 그 다음 비동기 요청에 활용할게 아니라 먼저 받은 데이터는 먼저 리덕스 스토어에 반영해서 리액트가 화면을 업데이트 할 수 있게 만들고 나서, 그 다음 비동기 요청들을 하는편이 좋겠죠. 무슨 말인지 예시를 통해서 다시 살펴봅시다.

병렬적이고 블락킹 되지 않는 사가

getForecast와 getFlight는 getDeparture에 의존합니다.

루트 사가에는 여러개의 로컬 사가가 들어있습니다. 각 로컬 사가는 서로에게 영향을 미치지 않고 독립적으로 동작할 수 있습니다.

한번 해봅시다.

getDeparture에 의존하는 getForecast와 getFlight 사가를 작성해봅시다.

isolatedFilght와 isolatedForecast 모두 독립적으로 작동하는 로컬 사가입니다. 각 로컬 사가는 departure에 의존하기 때문에 departure 사가가 먼저 실행되어야 이 두개의 사가가 실행되게 됩니다.

두 사가는 서로 같은 액션에 의해서 깨어나지만 리덕스 스토어에는 서로 다른 값을 dispatch하고 있다는 점을 주의 깊게 봐주세요.

다시말해서, 두 사가 중에서 어떤게 먼저 실행되든 상관없다는 말입니다. 그말은 두 사가가 서로 독립적이란 얘기겠죠.

자 이제, 대시보드를 병렬적이고 non-blocking하게 업데이트하는 사가를 작성해봅시다.

여기서 주의 깊게 봐야 할 부분은 다음 부분입니다.

yield put({type: 'FETCH_DEPARTURE3_SUCCESS', departure});

자세히 보면 뭔가 이상합니다.

yield put({type: 'FETCH_DEPARTURE3_SUCCESS', payload: departure});

departure를 payload의 value로 지정하지 않고 바로 전달하고 있습니다.

사실 아래 두가지 케이스는 같은 케이스 입니다.

yield put({type: 'FETCH_DEPARTURE3_SUCCESS', departure});
yield put({type: 'FETCH_DEPARTURE3_SUCCESS', departure: departure});

FETCH_DEPARTURE3_SUCCESS 액션이 dispatch되면 이 액션의 payload인 departure가 아래 함수의 리턴값으로 전달됩니다.

const departure = yield take(‘FETCH_DEPARTURE3_SUCCESS’);

궁금하신부분은 댓글로 남겨주세요!

테스팅에 관련한 부분도 있는데 이부분은 추후에 정리하도록 하겠습니다.

출처

--

--