전체 소스코드는 여기에서 확인해볼 수 있습니다.
개요
Redux-Saga의 Github에서는 아래와 같이 설명하고 있다.
Redux-Saga는, 애플리케이션 부작용(예: 데이터 가져오기와 같은 비동기 작업 및 브라우저 캐시 액세스와 같은 순수하지 않은 작업)을 보다 쉽게 관리하고, 보다 효율적으로 실행하고, 테스트하기 쉽고, 오류를 더 잘 처리하도록 만드는 것을 목표로 하는 라이브러리입니다.
그래서 언제 쓰나?
사실 Redux-Saga는 도입한다고 드라마틱하게 뭔가가 바뀌지는 않는다. 다만, 사용자 환경에 있어서 조금 더 좋은 환경을 제공해줄 뿐이지. 그 더 좋은 환경은 뭘까?
- API 호출 등의 비동기 작업에서 사용자 부주의로 인한 세부적 컨트롤이 가능하다.
- Async / Await으로 처리하는 것이 아니라, 동일 API를 여러번 호출한 경우 마지감 Response만 받아올 수 있도록 제어할 수 있다고 한다.
- 비동기 작업에서 기존 요청을 취소할수 있다.
- 웹소캣 이용 시 Channel이라는 기능을 이용하여 효율적 코드 관리가 가능하다.
진행할 Redux-Saga 테스트는, 이전 프로젝트를 그대로 가져와 수정하는 형식으로 진행할 것이다.
들어가기 전에...
Redux-Saga를 이용하기 전에 Generator문법을 보고 갈 필요가 있는데, 이를 이해하지 못하면 Redux-Saga를 배우는 것에 난해를 겪을 수 있다고 해서 추가로 정리했다.
Generator
Redux-Saga를 공부하면서 Generator이라는 문법을 알게 되었는데, 이에 대해 확인해 보려고 한다. 개요 A generator is a process that can be paused and resumed and can yield multiple values. (Generator는, 일시 정지 및 다시
conative.tistory.com
여기까지 완료되었다면, saga를 적용해보자.
설치
$ npm i redux-saga
Reducer 수정
src/reducers/user-info.tsx
import { delay, put, takeEvery, takeLatest } from 'redux-saga/effects';
// reducer 정의
export const setUserName = (userName: string): SataType => ({
type: "SETUSERNAME" as const,
userName
});
export const increaseAsync = (userName: string) => ({ type: 'SETUSERNAME_ASYNC', userName });
// set main reducer
type UserInfo = ReturnType<typeof setUserName>
const init = {
userName: 'Conative'
};
type SataType = {
type: string,
userName: string
}
// Generator 제작 (Redux-Saga에서는, 이를 '사가'라고 칭한다.)
function* setUserNameSaga(action: SataType) {
console.log("SAGA 실행!", action);
yield delay(2000);
yield put(setUserName(action.userName)); // put매서드는, 특정 액션을 dispatch 한다.
}
export function* userSaga() {
yield takeEvery('SETUSERNAME_ASYNC', setUserNameSaga); // 모든 INCREASE_ASYNC 액션을 처리
// yield takeLatest('SETUSERNAME_ASYNC', setUserNameSaga); // 가장 마지막으로 디스패치된 DECREASE_ASYNC 액션만을 처리
}
const userInfo = (state = init, action: UserInfo) => {
switch (action.type) {
case "SETUSERNAME":
return {
...state,
userName: action.userName
};
default:
return state;
}
};
export default userInfo;
src/reducers/index.tsx
import { combineReducers } from "redux";
import userInfo, { userSaga } from "./user-info";
import { all } from 'redux-saga/effects'; // [추가] Import
// 저장된 모든 Reducer들을 한곳으로 합쳐준다.
const rootReducer = combineReducers({
userInfo
});
export function* rootSaga() {
yield all([userSaga()]); // [추가] all은, 배열안 모든 사가를 동시에 실행시켜준다.
}
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
Reducer 적용
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { applyMiddleware, legacy_createStore as createStore } from "redux";
import { Provider } from "react-redux";
// [추가]
import rootReducer, { rootSaga } from './reducers';
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware(); // 사가 미들웨어 제작
const store = createStore(rootReducer, // 스토어에 적용
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga); // 루트 사가 실행
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<Provider store={store}>
<React.StrictMode>
<App />
</React.StrictMode>
</Provider>
);
reportWebVitals();
컴포넌트 수정
src/components/Input-Name.tsx
import { ChangeEvent } from 'react';
import { useDispatch } from "react-redux";
import { increaseAsync } from '../reducers/user-info';
// 이름을 입력받는 컴포넌트
const InputName = () => {
// dispatch 정의
const dispatch = useDispatch();
const handleInputName = (e: ChangeEvent<HTMLInputElement>) => {
dispatch(increaseAsync(e.target.value)); // 적용!
}
return (
<div>
<input type="text" onChange={handleInputName} />
</div>
)
}
export default InputName;
텍스트에 글을 입력 시, 비동기적으로 State가 변경되면서 값이 적용되는것을 확인할 수 있다.
CodeSandbox
CodeSandbox is an online editor tailored for web applications.
codesandbox.io
버그 발생했던 부분 수정
위에는 수정된 사항이지만, 이전에 작업했던 내용은 컴포넌트가 무한 리렌더링되는 버그가 있었다.
도저히 무슨 문제인지 몰랐는데, Chat-GPT에게 물어보니 답을 알려주었다.
3번이 내게 해당되었었는데, setUserName 부분을 2번 호출하였기 때문에 문제가 일어났던 것이였다.
// 변경 전 user-info
import { delay, put, takeEvery, takeLatest } from 'redux-saga/effects';
// reducer 정의
export const setUserName = (userName: string): SataType => ({
type: "SETUSERNAME" as const,
userName
});
// set main reducer
type UserInfo = ReturnType<typeof setUserName>
const init = {
userName: 'Conative'
};
type SataType = {
type: string,
userName: string
}
// Generator 제작 (Redux-Saga에서는, 이를 '사가'라고 칭한다.)
function* setUserNameSaga(action: SataType) {
console.log("SAGA 실행!");
yield delay(2000);
yield put(setUserName(action.userName)); // put매서드는, 특정 액션을 dispatch 한다.
}
export function* userSaga() {
// 여기에서 같은 Action을 반복해서 호출하니, 무한 재렌더링이 되는것...
yield takeEvery('SETUSERNAME', setUserNameSaga); // 모든 INCREASE_ASYNC 액션을 처리
// yield takeLatest('SETUSERNAME', setUserNameSaga); // 가장 마지막으로 디스패치된 DECREASE_ASYNC 액션만을 처리
}
const userInfo = (state = init, action: UserInfo) => {
switch (action.type) {
case "SETUSERNAME":
return {
...state,
userName: action.userName
};
default:
return state;
}
};
export default userInfo;
// 변경 전 Input-Name
import { ChangeEvent } from 'react';
import { useDispatch } from "react-redux";
import { setUserName } from '../reducers/user-info';
// 이름을 입력받는 컴포넌트
const InputName = () => {
// dispatch 정의
const dispatch = useDispatch();
const handleInputName = (e: ChangeEvent<HTMLInputElement>) => {
dispatch(setUserName(e.target.value)); // 적용!
}
return (
<div>
<input type="text" onChange={handleInputName} />
</div>
)
}
export default InputName;
참고 자료
https://leego.tistory.com/entry/Redux-saga%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90
https://tech.trenbe.com/2022/05/25/Redux-Saga.html
https://react.vlpt.us/redux-middleware/10-redux-saga.html