-
TIL-2024.07.18 - TIL - Redux - 003. redux-saga + redux-saga/effects> Frontend/React 2024. 7. 18. 09:20
목표
- 기본적인 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 }); } }
'> Frontend > React' 카테고리의 다른 글
TIL-2024.08.04 - React - 폰트 적용하는 방법 (0) 2024.08.04 TIL-2024.07.17 - TIL - Redux - 002. ReduxSaga 101 (0) 2024.07.17 TIL-2024.07.16 - TIL - Redux - 001. Redux 101 (0) 2024.07.17 TIL-2024.07.11 - React - redux 기초 (0) 2024.07.11 TIL-2024.05.11 - Alias 설정 (0) 2024.05.11