ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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 });
      }
    }

     

     

     

     

     

     

    댓글

Designed by Tistory.