전체 소스코드는 여기에서 확인해볼 수 있습니다.
개요
Redux-Saga의 Github에서는 아래와 같이 설명하고 있다.
Redux-Saga는, 애플리케이션 부작용(예: 데이터 가져오기와 같은 비동기 작업 및 브라우저 캐시 액세스와 같은 순수하지 않은 작업)을 보다 쉽게 관리하고, 보다 효율적으로 실행하고, 테스트하기 쉽고, 오류를 더 잘 처리하도록 만드는 것을 목표로 하는 라이브러리입니다.
그래서 언제 쓰나?
사실 Redux-Saga는 도입한다고 드라마틱하게 뭔가가 바뀌지는 않는다. 다만, 사용자 환경에 있어서 조금 더 좋은 환경을 제공해줄 뿐이지. 그 더 좋은 환경은 뭘까?
- API 호출 등의 비동기 작업에서 사용자 부주의로 인한 세부적 컨트롤이 가능하다.
- Async / Await으로 처리하는 것이 아니라, 동일 API를 여러번 호출한 경우 마지감 Response만 받아올 수 있도록 제어할 수 있다고 한다.
- 비동기 작업에서 기존 요청을 취소할수 있다.
- 웹소캣 이용 시 Channel이라는 기능을 이용하여 효율적 코드 관리가 가능하다.
진행할 Redux-Saga 테스트는, 이전 프로젝트를 그대로 가져와 수정하는 형식으로 진행할 것이다.
들어가기 전에...
Redux-Saga를 이용하기 전에 Generator문법을 보고 갈 필요가 있는데, 이를 이해하지 못하면 Redux-Saga를 배우는 것에 난해를 겪을 수 있다고 해서 추가로 정리했다.
여기까지 완료되었다면, 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가 변경되면서 값이 적용되는것을 확인할 수 있다.
버그 발생했던 부분 수정
위에는 수정된 사항이지만, 이전에 작업했던 내용은 컴포넌트가 무한 리렌더링되는 버그가 있었다.
도저히 무슨 문제인지 몰랐는데, 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