일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- nvm
- redux saga
- 비동기 처리
- 세션스토리지
- scrollTo 안됨
- ESLint
- 실행 컨텍스트
- CSR
- restore scroll position
- Architecture
- Typescript
- 환경 레코드
- NextJS
- Next.js
- Apollo Client
- 무한 스크롤
- 식별자 결정
- useRef
- SSR
- 변수 섀도잉
- Spa
- Apollo Server
- Babel
- Webpack
- SAGA 패턴
- task-definition
- 타입스크립트
- 스코프체이닝
- 마이크로 프론트엔드
- graphql
- Today
- Total
minguri brain is busy
Saga패턴 vs Redux-Saga / 간단한 예제로 살펴보기 본문
이 글에선 SAGA 패턴과 Redux-Saga에 대한 이해를 해봤다.
Saga 패턴은 마이크로 서비스의 등장과 함께 주목받기 시작한 디자인패턴이다. Saga 패턴에 대해 알아보던 중 프론트에서 적용 가능한 Redux-Saga로 간단한 데모 프로젝트를 올려보고 싶었고 두 가지의 개념에 대한 정리를 해봤다.
SAGA 패턴이란?
각 서비스에는 자체 데이터베이스가 있다. 그러나 일부 비즈니스 트랜젝션은 여러 서비스에 걸쳐 있으므로 서비스에 걸쳐 트랜잭션을 구현하는 매커니즘이 필요하다.
🤓 예시
고객에게 신용 한도가 있는 전자 상거래 상점을 구축한다고 가정했을 때 애플리케이션은 새 주문이 고객의 신용 한도를 초과하지 않도록 해야한다. Orders
와 Customers
는 서로 다른 서비스가 소유한 서로 다른 데이터베이스에 있으므로 애플리케이션은 단순히 ACID 트랜젝션
을 사용할 수 없다.
이럴 때 서비스를 포괄하는 트랙잭션을 구현하는 사가패턴을!
🤓 ACID 트랜젝션?
ACID(원자성, 일관성, 고립성, 지속성)는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어이다.
Redux-Saga란?
Redux-Saga란 사이드 이펙트를 핸들링하기 위한 미들웨어 라이브러리이다.
🤓 사이드 이펙트?
여기서 사이드 이펙트
는 데이터를 불러오는 비동기 처리나 브라우저 캐시를 말한다. 이것에 접근하는 행위들을 쉽게 관리하고 효과적인 실행, 손쉬운 테스트 그리고 에러 핸들링을 쉽게 해주도록 설계된 패턴이다.
🤓 미들웨어?
미들웨어는 전통적으로는 OS와 소프트웨어 중간에서 조정과 중개 역할을 하는 중간 소프트웨어라고 말한다. Redux에서 동작하는 Redux-Saga, Redux Thunk는 어떠한 두 요소 중간에서 동작하는 소프트웨어라고 생각하면 그 의미가 대략 같다.
리덕스의 동작 순서는 액션이 dispatch가 된 후 리듀서를 호출하는데 기존에 있던 state를 dispatch한 액션을 바꾼다. React는 Redux액션을 수행하면 Redux-Saga에서 dispatch하여 Redux의 액션을 가로챈다.
🤓 왜 Redux-Saga인가
선언적 효과
: Redux-Saga의 모든 작업은 일반 Javascript 개체를 생성하므로 비지니스 로직의 테스트가 쉽다.비동기 제어 흐름
: 동기화 스타일과 친숙한 제어 흐름 구성으로 비동기 흐름을 간단히 설명한다.동시성 관리
: 작업 간 동시성을 관리하기 위한 기본 및 연산자 제공한다. 여러 백그라운드 작업을 병렬로 분기하고 실행 중인 작업을 취소할 수 있다.사이드 이펙트 핸들러
: 모든 사이드 이펙트는 Saga로 옮겨지고, UI 구성 요소는 비즈니스 로직을 수행하지 않고 발생한 일을 알리기 위한 작업만 dispatch한다.
Redux-Saga 데모 프로젝트 만들기
Redux-Saga 공식 초심자 튜토리얼
https://redux-saga.js.org/docs/introduction/BeginnerTutorial/
uzihoon.com(데모 실습 코드 참조)
https://uzihoon.com/post/181be130-63a7-11ea-a51b-d348fee141c4
작업한 전체 코드는 Github에서 확인할 수 있다.
- React
- Redux
- Redux-Saga
- Typescript
👩💻프로젝트 생성
create-react-app으로 React 기본환경 구성$ npx create-react-app saga-demo-app --template typescript
npm 라이브러리 설치$ npm i redux react-redux @types/react-redux typesafe-actions redux-saga
redux
: 상태 관리 도구react-redux
:React용 공식 Redux UI 바인딩 라이브러리@types/react-redux
: react-redux에서 Typescript 사용typesafe-actions
: Typescript로 Redux 활용 시, Redux 아키텍쳐의 다변적인 성격과 복잡성 줄이기 위해 설계된 Typesafe 유틸리티redux-saga
: Redux 사이드 이펙트 매니징 라이브러리
폴더 구조 생성$ mkdir components store hooks
components
: view 컴포넌트 폴더store
: Redux, Redux-Saga 폴더hooks
: Redux store에 데이터를 가져오는 등의 역할
👩💻코드 작성
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import reportWebVitals from './reportWebVitals';
import Root from './Root';
ReactDOM.render(
<Root />,
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();
src/components/App.tsx
import React from 'react';
import Counter from './Counter';
function App() {
return <Counter />;
}
export default App;
src/Root.tsx
import React from "react";
import App from "./components/App";
function Root() {
return <App /> ;
}
export default Root;
src/components/Counter.tsx
import React from 'react';
import useCounter from '../hooks/useCounter';
import useCounterActions from '../hooks/useCounterActions';
function Counter() {
const counterActions = useCounterActions();
const count = useCounter().count;
return (
<div>
<h1>Counter</h1>
<div>{count}</div>
<button onClick={() => counterActions.onPlus({})}>PLUS</button>
<button onClick={() => counterActions.onMinus()}>MINUS</button>
<button onClick={() => counterActions.onPlusRandom()}>PLUS RANDOM</button>
<button onClick={() => counterActions.onPlusAfterOneSeconds()}>PLUS AFTER ONE SECONDS</button>
</div>
)
}
export default Counter;
👩💻 Redux와 Redux-Saga 연결
src/store/redux/coutner.ts
import { createAction, ActionType, createReducer } from "typesafe-actions";
export interface IPlus {
num?: number;
}
// Actions
export const PLUS = "counter/PLUS";
export const MINUS = "counter/MINUS";
export const PLUS_RANDOM = "counter/PLUS_RANDOM";
export const PLUS_AFTER_ONE_SECONDS = "counter/PLUS_AFTER_ONE_SECONDS";
export const plus = createAction(PLUS)<IPlus>();
export const minus = createAction(MINUS)();
export const plusRandom = createAction(PLUS_RANDOM)();
export const plusAfterOneSeconds = createAction(PLUS_AFTER_ONE_SECONDS)();
// Types
export const actions = { plus, minus, plusRandom, plusAfterOneSeconds };
type CounterAction = ActionType<typeof actions>;
type CounterState = {
count: number;
};
const initialState: CounterState = {
count: 0
};
// Reducer
const status = createReducer<CounterState, CounterAction>(initialState, {
[PLUS]: (state, action) => {
const { num } = action.payload;
const { count } = state;
const add = num || 1;
const _count = count + add;
return { count: _count };
},
[MINUS]: (state, action) => {
const { count } = state;
const _count = Math.max(count - 1, 0);
return { count: _count };
}
});
export default status;
src/store/redux/index.ts
reducer들을 묶어준다.
import counter from './counter';
import { combineReducers } from 'redux';
const rootReducer = combineReducers({ counter });
export default rootReducer;
export type r = ReturnType<typeof rootReducer>;
src/store/saga/counter.ts
import { put, delay } from 'redux-saga/effects';
import * as CounterActions from '../redux/counter';
function getRandomInt(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min+1)) + min;
}
/**
* plusRandom
* Counter Store에서 counter 값을 가져와 random 값을 더한 후 저장
*/
export function* plusRandom() {
try {
const num = getRandomInt(1, 20);
console.log(num);
yield put(CounterActions.plus({ num }));
} catch(error) {
console.error(error);
}
};
/**
* plusAfterOneSeconds
* 1초 후 plusRandom 액션 발행
*/
export function* plusAfterOneSeconds() {
try {
yield delay(1000);
yield put(CounterActions.plusRandom());
} catch(error) {
console.error(error);
}
}
src/store/saga/index.ts
Saga의 루트 파일로 Saga들을 묶어준다.
// Saga
import { takeEvery, all, fork } from "redux-saga/effects";
import * as CounterSaga from "./counter";
import * as CounterActions from "../redux/counter";
export default function* rootSaga() {
// Root Saga
yield all([fork(handleCounter)]);
}
function* handleCounter() {
yield takeEvery(
CounterActions.plusAfterOneSeconds,
CounterSaga.plusAfterOneSeconds
);
yield takeEvery(CounterActions.plusRandom, CounterSaga.plusRandom);
}
src/store/configure.tsx
import createSagaMiddleware from "@redux-saga/core";
import { applyMiddleware, compose, createStore } from "redux";
import rootReducer from "./redux";
import rootSaga from './saga';
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
}
}
const sagaMiddleware = createSagaMiddleware();
const isDev = process.env.NODE_ENV === 'development';
const devTools = isDev && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
const composeEnhancers = devTools || compose;
export default function configura() {
const store = createStore(
rootReducer,
{}, // pre-loaded state
composeEnhancers(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
return store;
}
src/Root.tsx
configure를 import해서 store 생성후 React와 연결한다.
import React from 'react';
import { Provider } from 'react-redux';
import App from './components/App';
import configure from './store/configure';
const store = configure();
function Root() {
return (
<Provider store={store}>
<App />
</Provider>
)
}
export default Root;
👩💻 로직구현
src/hooks/useCounterActions.tsx
import { useDispatch } from "react-redux";
import { useCallback } from "react";
import { actions, IPlus } from '../store/redux/counter';
export default function useCounterActions() {
const dispatch = useDispatch();
const onPlus = useCallback((param: IPlus) => dispatch(actions.plus(param)), [
dispatch
]);
const onMinus = useCallback(() => dispatch(actions.minus()), [dispatch]);
const onPlusRandom = useCallback(() => dispatch(actions.plusRandom()), [
dispatch
]);
const onPlusAfterOneSeconds = useCallback(
() => dispatch(actions.plusAfterOneSeconds()),
[dispatch]
);
return { onPlus, onMinus, onPlusRandom, onPlusAfterOneSeconds };
}
src/hooks/useCounter.ts
import { useSelector } from "react-redux";
import { RootState } from "../store/redux";
export default function useCounter() {
const counter = useSelector((state: RootState) => state.counter);
return counter;
}
npm start하면 버튼액션이 잘 동작하는 모습을 확인할 수 있다.
참고:
Pattern: Saga
https://microservices.io/patterns/data/saga.html
Redux-Saga란
https://uzihoon.com/post/181be130-63a7-11ea-a51b-d348fee141c4
벨로퍼트 10. redux-saga
https://react.vlpt.us/redux-middleware/10-redux-saga.html
Redux 미들웨어란?
https://tried.tistory.com/86
'FE > 개발' 카테고리의 다른 글
Apollo Server 4로 마이그레이션(sandbox 랜딩페이지 플러그인 설정, health check 적용) (0) | 2023.08.24 |
---|---|
웹페이지에 구조화된 데이터(JSON-LD) 추가하기 (0) | 2023.05.04 |
TypeScript 5.1 베타 발표 살펴보기 (0) | 2023.04.19 |
무한스크롤 페이지에서 스크롤 위치 복원 (0) | 2022.12.23 |
Next.js의 SSR, CSR 기본개념알아보기 (0) | 2022.08.04 |