redux-saga 깃허브
역할
redux-saga는 애플리케이션에서 일어나는 사이드 이펙트(side effects) (데이터를 불러오는 비동기 처리나 브라우저 캐쉬에 접근하는 행위들)을 쉽게 관리하며 효과적인 실행, 손쉬운 테스트 그리고 에러 핸들링을 쉽게 해준다.
특징
- saga 패턴을 차용 (참고 블로그)
- 미들웨어로서 역할을 수행
(React는 Redux 액션을 수행하면 Redux-Saga에서 디스패치하여 Redux의 액션을 인터셉트합니다. Proxy와 유사한 개념입니다. 중간에 가로챈 액션의 역할을 수행 후 다시 액션을 발행하여 데이터를 저장하거나 다른 이벤트를 수행시킵니다.) - 코드 작성 시 제너레이터 함수 사용 – 자바스크립트 ES6+: 제너레이터 함수 (generator function)
이 글의 진행 순서는 실제 작업 순서와는 연관이 없습니다.
전체 코드는 https://github.com/ayaysir/saga-board-1/ 에서 볼 수 있습니다.
리덕스 사가 기초 요약 리덕스 액션 리액트 리덕스
npm 라이브러리 설치
npm install redux react-redux redux-actions redux-devtools-extension redux-saga
"react-redux": Redux 를 React 환경에 맞게 편하게 사용하게 해주는 역할입니다."redux": 상태 관리 도구입니다."redux-actions":createAction,handleAction등의 함수를 제공하며 액션 생성 자동화, 분기 코드 간소화 등의 작업을 도와주는 역할입니다."redux-devtools-extension": 브라우저 콘솔에서 redux에 의한 상태 변화를 추적하기 위한 개발자 도구를 연결하는 역할입니다."redux-saga": 상태 관리 중 부수 효과를 처리하기 위한 미들웨어
src/index.js
최상단 인덱스 파일입니다.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from "react-router-dom";
// 리덕스와 미들웨어 적용을 위해 필요한 모듈을 불러온다.
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import rootReducer, { rootSaga } from "./modules";
import { composeWithDevTools } from "redux-devtools-extension";
import createSagaMiddleware from "redux-saga";
const sagaMiddleware = createSagaMiddleware()
// 루트 리듀서를 전달받아 스토어를 생성한다. composeWithDevTools 함수는 Redux DevTools의 기능을
// 사용할 수 있게 한다. applyMiddleware 함수를 통해 redux-saga 미들웨어를 적용한다.
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware))
)
// 주의 //
sagaMiddleware.run(rootSaga) // 리덕스 사가 미들웨어 실행
ReactDOM.render(
`<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</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();
- Redux에서 사용할 저장소(
store)를 생성합니다. - redux-saga 미들웨어를 적용합니다.
- 리덕스 사가 미들웨어를 실행합니다.
App부분을<Provider store={store}></Provider>로 감싸store를 사용할 수 있게 합니다.
src/lib/api.js
axios를 사용해 외부 API 호출 함수를 작성합니다.
import axios from "axios"
// 상품 상세 조회 API 호출 함수
export const fetchItemApi = (itemId) => axios.get(`/items/${itemId}`)
// 상품 목록 조회 API 호출 함수
export const fetchItemListApi = () => axios.get("/items")
// 상품 삭제 API
export const removeItemApi = (itemId) => axios.delete(`/items/${itemId}`)
src/module/index.js
import { combineReducers } from "redux"
import { all } from "redux-saga/effects"
import item, { itemSaga } from "./item"
import loading from "./loading"
// 루트 리듀서와 다른 리듀서를 결합
// item 리듀서와 loading 리듀서를 추가
const rootReducer = combineReducers({
item,
loading
})
// 루트 사가
// all 함수는 여러 사가를 합친다.
export function* rootSaga() {
yield all([itemSaga()])
}
export default rootReducer
combineReducer를 통해 리듀서들을 결합시킵니다. (리듀서는 redux-actions의handleAction을 이용해 생성)root사가를 생성하고 개별 생성된 모든 사가 함수들을 합칩니다. 사가 함수의 정의는 아래에서 설명합니다.
src/module/loading.js
로딩 상태를 관리하는 부분입니다.
import { createAction, handleActions } from "redux-actions"
// action type (액션 상수 타입 정의)
const START_LOADING = "loading/START_LOADING"
const END_LOADING = "loading/END_LOADING"
// create action function
// 로딩 시작/끝 액션 함수를 만들고 외부에서 사용할 수 있도록 공개한다.
export const startLoading = createAction(START_LOADING, actionType => actionType)
export const endLoading = createAction(END_LOADING, actionType => actionType)
// init states - 모듈의 초기 상태 설정
const initialState = {}
// redux의 액션의 type에 따른 작업 - if문 또는 switch 문을 아래로 대체
// 해당 액션별 로딩 시작/끝 상태를 설정한다.
const loading = handleActions(
{
[START_LOADING]: (state, action) => ({
...state,
[action.payload]: true
}),
[END_LOADING]: (state, action) => ({
...state,
[action.payload]: false
})
},
initialState
)
export default loading
- 먼저 액션 상수를 정의합니다.
createAction함수를 이용하여 액션 함수를 생성합니다.startLoading변수는 함수 타입으로 한 개의 파라미터를 갖습니다. 나중에 이startLoading변수가 스토어에 저장 시 파라미터를 키값으로 하여true,false를 스토어에 저장하게 됩니다.handleAction을 이용하여 액션에 타입에 따른 작업 분기를 설정합니다.switch문 대신 객체 형태로 작성합니다.action.payload는createAction으로 만들어진 변수가 나중에 사가에서 실행될 때 파라미터의 값과 대응되는 부분입니다.
src/module/item.js
메인 컨텐츠에 대한 부분입니다.
import { createAction, handleActions } from "redux-actions"
import { takeLatest, call, put } from "redux-saga/effects"
import { fetchItemApi, fetchItemListApi } from "../lib/api"
import { startLoading, endLoading } from "./loading"
// 액션 타입 (상세)
const FETCH_SUCCESS = "item/FETCH_SUCCESS"
const FETCH_FAILURE = "item/FETCH_FAILURE"
// 상세 조회 액션 타입
export const FETCH_ITEM = "item/FETCH_ITEM"
// 액션 타입 (목록)
const FETCH_LIST_SUCCESS = "item/FETCH_LIST_SUCCESS"
const FETCH_LIST_FAILURE = "item/FETCH_LIST_FAILURE"
// 목록 조회 액션 타입
export const FETCH_ITEM_LIST = "item/FETCH_ITEM_LIST"
// 액션 생성 함수 (상세)
export const fetchSuccess = createAction(FETCH_SUCCESS, data => data)
export const fetchFailure = createAction(FETCH_FAILURE, e => e)
// 상세 조회 액션 생성 함수
export const fetchItem = createAction(FETCH_ITEM, itemId => itemId)
// 액션 생성 함수 (목록)
export const fetchListSuccess = createAction(FETCH_LIST_SUCCESS, data => data)
export const fetchListFailure = createAction(FETCH_LIST_FAILURE, e => e)
// 목록 조회 액션 생성 함수
export const fetchItemList = createAction(FETCH_ITEM_LIST)
// 상품 상세정보를 조회하는 태스크
function* fetchItemSaga(action) {
yield put(startLoading(FETCH_ITEM))
try {
const response = yield call(fetchItemApi, action.payload)
yield put(fetchSuccess(response.data))
} catch(e) {
yield put(fetchFailure(e))
}
yield put(endLoading(FETCH_ITEM))
}
// 상품 목록을 조회하는 태스크
function* fetchItemListSaga() {
yield put(startLoading(FETCH_ITEM_LIST))
try {
const response = yield call(fetchItemListApi)
yield put(fetchListSuccess(response.data))
} catch(e) {
yield put(fetchListFailure(e))
}
yield put(endLoading(FETCH_ITEM_LIST))
}
// 상품 saga 함수 작성
export function* itemSaga() {
// 상세조회 태스크
yield takeLatest(FETCH_ITEM, fetchItemSaga)
// 목록 조회 태스크 수행
yield takeLatest(FETCH_ITEM_LIST, fetchItemListSaga)
}
// 모듈의 초기 상태
const initialState = {
item: null, // 하나의 상품 정보
items: [], // 상품 목록
error: null // 응답에러 정보
}
// 리듀서 함수 정의
// 리듀서는 상태변화를 일으키는 함수이다.
const item = handleActions(
{ // 상세 조회 상태 변경
[FETCH_SUCCESS]: (state, action) => ({
...state,
item: action.payload
}),
[FETCH_FAILURE]: (state, action) => ({
...state,
error: action.payload
}),
// 목록 조회 상태 변경
[FETCH_LIST_SUCCESS]: (state, action) => ({
...state,
items: action.payload
}),
[FETCH_LIST_FAILURE]: (state, action) => ({
...state,
error: action.payload
})
},
initialState
)
export default item
- 제너레이터 함수 형식으로 사가 함수를 작성합니다.
- 사가란 특정 액션에 대한 리스너입니다.
- 리덕스 사가에서 이펙트를 다룰 수 있는 명령어가 몇 개 있습니다.
put– 이 함수를 통하여 새로운 액션을 디스패치(변경 내용이 저장소에 저장, 갱신됨) 할 수 있습니다.put(loading(FETCH_ITEM))을 함으로써loading액션 함수가 실행되며, 저장소에 저장이 됩니다.
takeEvery– 특정 액션 타입에 대하여 디스패치되는 모든 액션들을 모니터링하고 처리하는 역할입니다,takeLatest– 특정 액션 타입에 대하여 디스패치된 가장 마지막 액션만을 처리하는 함수입니다.- 특정 액션을 처리하고 있는 동안 동일한 타입의 새로운 액션이 디스패치되면 기존에 하던 작업을 무시하고 새로운 작업을 시작합니다.
call– 인자로 함수나 saga task를 받을 수 있습니다. 보통Promise등의 실행 (ajax 등) 에 쓰이며Promise가resolve될 때까지 다음 작업이pending됩니다.
src/components/ItemList.jsx (일부)
아이템을 표시하는 외부 영향이 없는 재사용 가능한 컴포넌트입니다.
import { Link } from "react-router-dom"
export default function ItemList({ items, isLoading }) {
return (
`<div align="center">
<h2 className="title">상품 목록</h2>
{isLoading && "로딩중..."}
{!isLoading && items && (
<생략>
</생략>
)}
</div>`
)
}
src/container/ItemListContainer.jsx
ItemList 컴포넌트를 감싸 상태 정보를 관리하는 고차 컨테이너(HOC) 함수입니다.
import ItemList from "../components/ItemList"
import { useDispatch, useSelector } from "react-redux"
import React, { useEffect } from "react"
import { fetchItemList, FETCH_ITEM_LIST } from "../modules/item"
const ItemListContainer = () => {
// useDispatch: 컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해주는 hook 이다.
// useSelector: connect 함수를 대신하여 스토어 상태를 조회
const dispatch = useDispatch()
const { items, isLoading } = useSelector(({ item, loading }) => ({
items: item.items,
isLoading: loading[FETCH_ITEM_LIST]
}))
// 브라우저 상에 컴포넌트가 나타날 때 상품 목록을 조회하는 함수를 실행
useEffect(() => {
dispatch(fetchItemList())
}, [dispatch])
return `<ItemList items={items} isLoading={isLoading} />`
}
export default ItemListContainer
dispatch(fetchItemList())가 실행되면"item/FETCH_ITEM_LIST"액션이 전송되며, item.js의itemSaga()의yield takeLatest(FETCH_ITEM_LIST, fetchItemListSaga)부분에 의해 목록 조회 태스크가 실행됩니다.- api 로딩에 성공해
"item/FETCH_LIST_SUCCESS"타입의 액션이 전송된다면 item.js의handleAction으로 생성된 분기 함수에서 액션을 찾아 상태 저장소의item키에 받아온 데이터를 추가합니다. return으로ItemList컴포넌트를 넘기고props로items,isLoading을 추가한 뒤ItemList컨테이너에서 첫 번째에 위치한 객체 형태의 파라미터를 읽어올 수 있습니다.
타입스크립트에서 작업하는 경우 handleAction 대신 다른 함수를 사용해야 합니다.





0개의 댓글