TIL-2024.07.18 - TIL - Redux - 003. redux-saga + redux-saga/effects
목표
- 기본적인 Redux Saga 사용법
- 추가 : Redux Saga / Effects
사용법
1. 프로젝트 및 tsconfig.json 설정
> 필요 패키지 설치
npm install @reduxjs/toolkit react-redux redux-saga axios @types/react-redux typescript
> tsconfig.json 설정
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es2015", "es2017"],
"allowJs": true,
"jsx": "react",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"]
}
2. Redux Slice 설정
> createSlice 을 활용해 Star Wars 데이터를 관리할 slice 설정
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
import {PeopleState, PeopleDTO} from "../../../types/people.type";
const initialState: PeopleState = {
person: null,
isLoading: false,
isError: null
}
const peopleSlice = createSlice({
name: 'people',
initialState: initialState,
reducers: {
fetchPeopleRequest(state, action: PayloadAction<number>) {
state.isLoading = true;
state.isError = null;
},
fetchPeopleRequestSuccess(state: PeopleState, action: PayloadAction<PeopleDTO>) {
state.isLoading = false;
state.isError = false;
state.person = action.payload;
},
fetchPeopleRequestFailed(state, action: PayloadAction<null>) {
state.isLoading = false;
state.isError = true;
state.person = null;
}
}
})
export const peopleActions = peopleSlice.actions;
export default peopleSlice.reducer;
3. Redux Saga 설정
> Redux Saga 설정하고, API 호출 처리하는 Saga 생성
import {call, put, takeLatest} from 'redux-saga/effects';
import {peopleActions} from "./people.slice";
import axios, {AxiosRequestConfig, AxiosResponse} from "axios";
// header 설정
const config: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json'
}
}
const API_BASE_URL = process.env.REACT_APP_STAR_WARS_URL;
// saga-watcher
function* peopleSaga() {
// takeEvery 이펙트를 사용해 마지막에 dispatch 된 fetchPeopleRequest 인 경우, fetchPeopleList generator 살행
yield takeLatest(peopleActions.fetchPeopleRequest, fetchPeopleList)
}
// function
function* fetchPeopleList({payload}: any) {
try {
// axios.get 메서드를 호출해 API 호출 수행
const response: AxiosResponse<any> = yield call(
axios.get,
`${API_BASE_URL}/people/${payload}`,
config
)
const {status, data} = response
// 성공 시, fetchPeopleRequestSuccess action을 dispatch하여 데이터를 store 에 저장
if (+status === 200) {
yield put(peopleActions.fetchPeopleRequestSuccess(data))
}
} catch (e) {
// 실패 시, fetchPeopleRequestFailed 호출
console.log('error:: ', e)
yield put(peopleActions.fetchPeopleRequestFailed)
}
}
export default peopleSaga;
4. Component 에서 state & component 사용
> component 에서 redux 상태를 읽고, saga 를 사용해 데이터를 가져오는 action 을 dispatch
import './people.style.css';
import React, {useEffect, useState} from "react";
import {useDispatch, useSelector} from "react-redux";
import {peopleActions} from "../../redux/slices/people/people.slice";
import {RootState} from "../../redux/store";
const People = () => {
const dispatch = useDispatch();
const [personNo, setPersonNo] = useState('');
const person = useSelector((state: RootState) => state.people?.person);
useEffect(()=>{
// TODO:: dispatch when first mounted.
},[])
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.preventDefault();
const {value} = event.target;
setPersonNo(value);
}
// find 버튼을 클릭할 시, action creator 호출 (fetchPeopleRequest)
const handleOnClickFind = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
dispatch(peopleActions.fetchPeopleRequest(+personNo));
}
const handleOnClickSave = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
// dispatch(peopleActions.savePeopleRequest())
}
return (
<div className="people-wrapper">
<div className="people-input-wrapper">
<input type="string" value={personNo} onChange={handleOnChange}/>
<button onClick={handleOnClickFind}>find</button>
</div>
<div className="people-body-wrapper">
<div className="people-body-find">
{person && Object.entries(person)?.length > 0
?
<div>
<h3>name: {person.name}</h3>
<h5>gender: {person.gender}</h5>
<h5>birth_year: {person.birth_year}</h5>
<h5>height: {person.height}</h5>
<h5>mass: {person.mass}</h5>
</div>
: null
}
</div>
<div className="people-body-save"></div>
</div>
<div className="people-save-wrapper">
<button onClick={handleOnClickSave}>save</button>
</div>
</div>
)
}
export default People
5. Provider 연결
> redux store 연결
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import store from "./redux/store";
import {Provider} from "react-redux";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App/>
</Provider>
);
Effects (redux saga / effects)
- Effects 는 비동기 작업을 처리할 수 있음.
1. Call
> Call effect는 함수를 호출하고, 그 함수가 반환하는 Promise 가 해결될때까지 기다림
> 사용 이유: 주로 API 호출과 같은 비동기 작업을 처리할 때 사용
import { call, put, takeEvery } from 'redux-saga/effects';
import axios from 'axios';
// API 호출 함수
const fetchData = (apiEndpoint) => axios.get(apiEndpoint);
// 워커 사가: FETCH_DATA_REQUEST 액션을 처리
function* fetchDataSaga(action) {
try {
const response = yield call(fetchData, action.payload.apiEndpoint);
yield put({ type: 'FETCH_DATA_SUCCESS', payload: response.data });
} catch (error) {
yield put({ type: 'FETCH_DATA_FAILURE', payload: error.message });
}
}
// 감시자 사가: FETCH_DATA_REQUEST 액션을 감시
function* watchFetchData() {
yield takeEvery('FETCH_DATA_REQUEST', fetchDataSaga);
}
2. put
> put 는 특성 action 을 dipatch
> success | failed 후에, 상태를 업데이트하는 데 사용
// FETCH_DATA_SUCCESS 액션을 디스패치
yield put({ type: 'FETCH_DATA_SUCCESS', payload: response.data });
// FETCH_DATA_FAILURE 액션을 디스패치
yield put({ type: 'FETCH_DATA_FAILURE', payload: error.message });
3. takeEvery
> takeEvery 는 주어진 action type 에 대해 작업을 수행.
> 이 effect는 action 이 발생할 때마다 작업을 시작
// FETCH_DATA_REQUEST 액션을 감시
function* watchFetchData() {
yield takeEvery('FETCH_DATA_REQUEST', fetchDataSaga);
}
4. takeLatest
> takeLatest 는 주어진 action type 에 대해 가장 최근 작업만 수행.
> 이 effect는 다수의 action 이 발생 / 이전 작업이 아직 완료되지 않더라도 취소하고 새로운 작업을 시작
import { takeLatest } from 'redux-saga/effects';
// 감시자 사가: FETCH_DATA_REQUEST 액션을 감시
function* watchFetchData() {
yield takeLatest('FETCH_DATA_REQUEST', fetchDataSaga);
}
5. select
> select 는 현재 상태를 선택하는데 사용
> 주로 current state를 기반으로 로직을 수행
import { select, call, put } from 'redux-saga/effects';
// 현재 상태에서 데이터 선택
const getApiEndpoint = (state) => state.apiEndpoint;
function* fetchDataSaga() {
try {
const apiEndpoint = yield select(getApiEndpoint);
const response = yield call(fetchData, apiEndpoint);
yield put({ type: 'FETCH_DATA_SUCCESS', payload: response.data });
} catch (error) {
yield put({ type: 'FETCH_DATA_FAILURE', payload: error.message });
}
}
6. fork
> fork 는 비동기적으로 작업을 시작
> call 과 달리 비동기 작업을 백그라운드에서 실행
import { fork, put, takeEvery } from 'redux-saga/effects';
function* backgroundTask() {
while (true) {
yield put({ type: 'BACKGROUND_TASK_TICK' });
yield delay(1000); // 1초 대기
}
}
function* watchStartBackgroundTask() {
yield takeEvery('START_BACKGROUND_TASK', function* () {
yield fork(backgroundTask);
});
}
7.delay
> delay 는 주어진 시간 (ms) 동안 대기
> 주로 특정 시간 동안 기다렸다가 작업을 수행할 때 사용
import { delay } from 'redux-saga/effects';
function* fetchDataSaga() {
try {
yield delay(1000); // 1초 대기
const response = yield call(fetchData, 'https://api.example.com/data');
yield put({ type: 'FETCH_DATA_SUCCESS', payload: response.data });
} catch (error) {
yield put({ type: 'FETCH_DATA_FAILURE', payload: error.message });
}
}