> Frontend/React

TIL-2024.07.18 - TIL - Redux - 003. redux-saga + redux-saga/effects

Janku 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 });
  }
}