redux-saga를 사용해 코드를 작성하는 방법은 아래
직접 사용해보기
부분을 참고해주세요😃
redux-saga는 redux의 미들웨어로, 어플리케이션의 사이드 이펙트를 더 효과적으로 관리하고자 만들어졌다.
사이드 이펙트?
API 통신이나 유저 인터렉션 등의 비동기 작업
따라서 saga는 어플리케이션에서 오로지 사이드 이펙트에만 반응하도록 만들어진 별도 스레드와 같다고 할 수 있다. 즉, 사이드 이펙트를 더 쉽게 관리하고 효과적으로 실행, 테스트, 에러처리할 수 있도록 한다.
리덕스의 action을 모니터링하고 있다가, 특정 액션이 발생하면 이에 따라 특정한 작업을 할 수 있도록 해준다. 또한 리덕스의 상태값의 접근하고 action을 dispatch할 수도 있다.
redux에서는 action을 dispatch하면 바로 state가 변경된다.
따라서 비동기 처리가 불가능하여 별도의 라이브러리를 사용한다.
비동기 처리를 위한 라이브러리 중에는 redux-thunk, redux-saga 등이 있다.
redux-thunk는 함수를 디스패치 할 수 있도록 해주는 미들웨어이다.
redux-thunk가 오랜 기간 사용되어왔으나, 최근에는 redux-saga가 비동기의 다양한 상황을 처리하기 좋고 테스트나 디버깅이 쉽기 때문에 더 많이 사용되는 추세라고 한다.
조금 더 자세히 말하면...
redux-saga는 redux-thunk로 하지 못하는 다양한 작업들을 처리할 수 있는데, 예를 들면,
이와 같은 다양한 비동기 작업들을 처리할 수 있다.
redux-saga는 generator라는 문법을 사용한다.
이 문법을 사용하면 함수의 실행을 멈추거나 원하는 시점에 함수를 다시 이어서 실행할 수 있다.
무슨 소리지?? 예시를 보자!
function weirdFunction() {
return 1;
return 2;
return 3;
return 4;
return 5;
}
이런 함수가 있을 때, 함수는 호출하면 1만 반환한다.
하지만 generator함수를 사용하면 값을 순차적으로 여러번 반환 할 수 있다. 나아가 진행을 멈췄다가 이후에 이어서 반환하게 만들 수도 있다...!
function* generatorFunction() {
console.log('안녕하세요?');
yield 1;
console.log('제너레이터 함수');
yield 2;
console.log('function*');
yield 3;
return 4;
}
제너레이터 함수를 만들 때는 function*라는 키워드를 사용한다.
const generator = generatorFunction();
해당 함수를 호출하여 반환되는 generator 객체를 generator라는 변수에 할당해주었다.
이 함수를 호출했을 때 반환되는 객체, 위 예시에서 generator라는 변수에 할당된 객체를 generator
라고 하며 이 객체는
{ value, done }
의 속성을 갖고 있다.
이제 선언해두었던 generatorFunction은 generator가 되었다.
generator.next()
yield는 함수 내부에서 다음값, 동작을 제어 하는데, 뒤의 로직이나 값을 전달한 뒤 해당 함수에서 벗어나 실행을 잠시 멈춘다.
따라서 이 함수는 호출 시마다 yield한 값을 반환하고 코드의 흐름을 멈춘다.
next()메서드는 yield()메서드로 멈춘 함수실행을 이어서 다음 동작을 다시 처리하도록 해준다.
따라서 다시 generator.next()를 호출하면 이어서 호출한다
정리하면, generator 함수는 코드 진행중에 yield 키워드를 만나면 일단 멈춘다. 그리고 계속 진행하라는 의미를 담은 next()메서드가 호출되면 다음 yield키워드를 만날때까지 코드를 실행시킨다.
이처럼 리덕스 사가가 Generator 문법 기반이기 때문에 비동기적 처리에 적합하다.
위에서 살펴본 메서드를 사용하기 때문에 while(true)같은 코드 안에서도 비동기 동작을 잘 제어할 수 있다.
이제 기본적인 문법은 이해했다.
그럼 이 문법들이 어떻게 리덕스 사가에서 비동기 처리에 사용되는걸까?
리듀서에 정의된 특정한 액션을 기다리다가 액션이 발생하는 시점에서 yield에 등록된 함수나 로직을 동작시킨다.
이런 처리들은 리덕스 사가에 미리 정의된 여러 부수효과(effects) 함수들로 동작하게 된다.
특정한 액션을 기다리다가 발생했을 때 함수를 동작시키는 건 어떻게 할까? generator가 어떻게 액션을 모니터링 하는지는 다음 예시를 참고하자.
function* watchGenerator() {
console.log('모니터링 시작!');
while(true) {
const action = yield;
if (action.type === 'HELLO') {
console.log('안녕하세요?');
}
if (action.type === 'BYE') {
console.log('안녕히가세요.');
}
}
}
const watch = watchGenerator();
watch.next({ type: 'HELLO' });// 안녕하세요?
아직 감이 오지 않는다면, 아래
직접 사용해보기
에서 코드와 설명을 확인해보자!
리덕스 사가를 사용하기 위해서는 첫번째로,
import { createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import user from './user';
import rootSaga from './saga';
// 여러 상태값을 변경하는 리듀서들을 하나의 리듀서 함수로 함친다.
const rootReducer = combineReducers({ user });
// 사가 미들웨어를 생성해서 스토어에 연결해준다.
const sagaMiddleware = createSagaMiddleware();
// store 생성
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
// 사가 미들웨어에서 통합 사가 함수를 실행시킨다.
sagaMiddleware.run(rootSaga);
export default store;
등록된 스토어 상태값을 변경할 때 사가 함수들을 인식할 수 있도록 하기 위함이다.
rootSaga는 이제 별도로 작성해주면 된다.
주로 사용되는 문법을 정리했다.
이 문법들은 아래와 같이 모두 yield라는 키워드와 함께 사용할 수 있다.
const res = yield call(getMyInfo, action.payload)
yield all([testSaga1(), testSaga2()])
call(delay, 1000)
call과 put의 차이?
takeEvery(INCREASE_ASYNC, increaseSaga)
takeLatest(DECREASE_ASYNC, decreaseSaga)
전체 코드
//index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import createSagaMiddleware from "redux-saga";
import { configureStore } from "@reduxjs/toolkit";
import catsReducer from "./catState";
import catSaga from "./catSaga";
const saga = createSagaMiddleware();
const store = configureStore({
reducer: {
cats: catsReducer,
},
middleware: [saga],
});
saga.run(catSaga);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
하나하나 살펴보자.
configureStore로 리듀서와 미들웨어를 전달해 스토어를 생성한다.
const store = configureStore({
reducer: {
cats: catsReducer,
},
middleware: [saga],
});
reducer와 미들웨어로 사용할 redux-saga 객체는 다음단계에서 생성해줄 것이다.
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
redux
const saga = createSagaMiddleware();
생성한 sagaMiddleWare 인 saga에 내장되어 있는 run 메서드를 사용해 catSaga를 실행한다.
catSaga는 별도로 정의해둔 generator 함수로, 다음단계에서 확인해보자.
saga.run(catSaga);
전체 코드
import { createSlice } from "@reduxjs/toolkit";
export const catSlice = createSlice({
name: "cats",
initialState: {
cats: [],
isLoading: false,
},
reducers: {
getCatsFetch: (state) => {
state.isLoading = true;
},
getCatsSuccess: (state, action) => {
state.cats = action.payload;
state.isLoading = false;
},
getCatsFailure: (state) => {
state.isLoading = false;
},
},
});
export const { getCatsFetch, getCatsSuccess, getCatsFailure } =
catSlice.actions;
export default catSlice.reducer;
이 부분에 대해서는 다음 포스팅에 정리해두었다
👉 포스팅 [리덕스 툴킷 사용하기] 보러가기
전체코드
import { call, put, takeEvery } from "redux-saga/effects";
import { getCatsSuccess } from "./catState";
function* workGetCatsFetch() {
const cats = yield call(() => fetch("https://api.thecatapi.com/v1/breeds"));
const formattedCats = yield cats.json();
const formattedCatsShortened = formattedCats.slice(0, 10);
yield put(getCatsSuccess(formattedCatsShortened));
}
function* catSaga() {
yield takeEvery("cats/getCatsFetch", workGetCatsFetch);
//cats라는 name의 slice에서 getCatsFetch리듀서
}
export default catSaga;
하나하나 살펴보자.
이 함수는 api를 호출해 json 문자열로 결과를 변환한뒤, 성공하면 getCatsSuccess라는 액션을 디스패치한다.
function* workGetCatsFetch() {
const cats = yield call(() => fetch("https://api.thecatapi.com/v1/breeds"));
const formattedCats = yield cats.json();
const formattedCatsShortened = formattedCats.slice(0, 10);
yield put(getCatsSuccess(formattedCatsShortened));
}
이 때 formattedCatsShortened는 fetch한 데이터를 json객체로 변환한 뒤 10개만 가져온 것이다. 이 데이터를 getCatsSuccess에 인자로 전달해주었다.
위에서 Redux의 createSlice로 선언한 slice의 리듀서 중 하나인 getCatsSuccess의 내용은 다음과 같았다.
getCatsSuccess: (state, action) => {
state.cats = action.payload;
state.isLoading = false;
},
즉, 데이터 fetch가 성공했을 경우 action.payload로 전달받은 formattedCatsSortened라는 10개의 데이터를 state의 cats에 할당해준 것이다.
참고로 state.cats는
export const catSlice = createSlice({
name: "cats",
initialState: {
cats: [],
isLoading: false,
},
이 부분에서 slice의 initialState 내부에 선언해주었었다.
getCatsFetch액션이 실행되면 workGetCatsFetch함수를 호출해 원하는 데이터를 fetch 해올 수 있도록 한다.
function* catSaga() {
yield takeEvery("cats/getCatsFetch", workGetCatsFetch); //cats라는 name의 slice에서 getCatsFetch리듀서
}
그럼 이 getCatsFetch는 어디에서 디스패치 되는 것일까?
전체코드
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import "./App.css";
import { getCatsFetch } from "./catState";
function App() {
const cats = useSelector((state) => state.cats.cats);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCatsFetch());
}, [dispatch]);
console.log(cats);
return <div className="App">dd</div>;
}
export default App;
하나하나 살펴보자.
페이지가 처음 렌더링 될 때 원하는 데이터를 불러와야 하므로,
useEffect를 사용해 getCatsFetch()를 호출한다.
useEffect(() => {
dispatch(getCatsFetch());
}, [dispatch]);
그러면 위에서 작성한 catSaga generator 함수가 이것을 감지하고 workGetCatsFetch를 호출한다.
function* catSaga() {
yield takeEvery("cats/getCatsFetch", workGetCatsFetch); //cats라는 name의 slice에서 getCatsFetch리듀서
}
즉, 기존 리덕스의 로직에 비동기 로직을 추가했다고 보면 된다.
정리하면,