본문 바로가기
  • 삽질하는 자의 블로그
Redux, Redux-toolkit

7. Redux-ToolKit [ 비동기, 사이드 이펙트 있는 함수 다루기 e.g. fetch함수] with useEffect **

by 이게뭐당가 2022. 12. 17.

1. Slice 의 reducers 안에 들어가는 함수는 반드시 "순수함수" 여야한다.

    "순수함수"는 동일한 input 에는 반드시 동일한 output 이 나오는 함수를 말한다.

    대표적으로

        const xxFn = ()=>{
            return 3 + 4
        }

    "비순수 함수"는 사이드이펙트 가 있는 함수이다. 동일한 input 으로 다른 output 이 나오는 함수.

    대표적으로
        
        const xxFn = (xx) =>{
            console.log(xx)
        }

   " xx 의 정의가 바뀌면, 다른 값이 log 로 나온다."

 

2. 대표적으로 안되는 함수 " fetch"

: http 요청을 위한 fetch 는 비동기 함수이자, 사이드이펙트가 있는 가장 많이 쓰이는 함수이다.

    import {createSlice} from "@reduxjs/toolkit";

    cosnt xxSlice = createSlice({
       	name : "",
        initialState: "...",
        reducers:{
           addItmes(state,action){
            			...
            	fetch( ... )     // ==>> [ X ] [이런게 안된다!]  비동기 함수는 사용 할 수 없다. ***   
        	}
    })

 

3.  그렇다면, 비동기, 사이드이펙트가 있는 함수는 어떻게 리덕스로 처리할까?

 

[리듀스 함수안에 일반적인 순수함수 일 경우]

  1) 컴포넌트에서 버튼

  2) 버튼이 action을 dispatch

  3) action은 자신의 논리로 즉각적으로 state 를 변경

 

[리듀스 함수안에 일반적인 비순수함수 일 경우]

  1) 컴포넌트에서 버튼

  2)  일반 함수를 호출 ( 일반 함수 안에는 비동기 함수와 그 결과를 가지고, State 를 변경할 dispatch 가 들어가있다  )

  3)  일반 함수버튼에 의해 실행되고, 그 안에서, 비동기 함수를 실행하고,

         그 결과만을 action 논리에 집어넣어 state 를 변경

      >> 결과적으로,  reducer 함수 안에는, 비동기 함수는 없고, 간단한 State 변경을 하는 정도

   

  ** 그 일반함수를 잘 만드는 것이 중요
  ** 그 일반함수를 어디에 둘지 도 중요

  ** 그 일반함수를 ( 평소 하듯 ) 그저 컴포넌트 안에서 fetch 할 수도 있고 ( 컴포넌트 Fat )

  ** 그 일반함수를 리덕스에서 만든다면, Thunk 액션 생성자라 한다. ( Store Fat )

 

4. 복잡하게 생각 할 것 없다.

 

      : 그냥 리듀서 함수를 통해, fetch 를 하여  State 를 변경하는 것이 아니고

          fetch 한 값을 reducer 함수에 넣어 State 를 변경할 뿐이다.

 

 

 

 

 

리덕스에서 fetch 사용하여 DB PUT 요청으로 값 넣기

[ 방법1 : 컴포넌트에서 직접 fetch 하기 WITH, useEffect ]

 

   [ 의도 ]

     1) Slice 안에는, action 의 논리에 의해, 모든 item이 담겨있는 배열의 state 가 존재한다.

     2) 자동으로 혹은, 버튼을 눌렀을때,  DB에 item 을 넣고싶다.

 

   [ 프로세스 ] 

     1) 버튼을 누른다.

     2) dispatch 에 의해 state 가 변경된다.

     3) state 가 변경되면, useEffect 에 의해, DB 에 fetch 가 되도록, state 를 종속성으로 만든 useEffect 를 사용한다.

 

      **  Redux 를 사용할때는 PUT 이 더 좋을 수 있다. (이유는 코드에서 설명)

 

let initial = true  	// ** 처음 실행 했을때, 실행이 되지 않기 위한 initial [첫 실행 막기]

const Header = () => {
    const itemState = useSelector((state) => state.item.items); 

    const itemDispatch = useDispatch(); 

    const submitHandler = (e) => {
        e.preventDefault()

        itemDispatch(itemState.addItems());		// dispatch 에 의해, itemState 가 변경
            ...
    };

    useEffect (()=>{  
        if(initial){		// [첫 실행 막기] initial 이 true 라면, false 로 바꾸고 뒤에 코드는 실행시키지 않도록 한다.
          initial = false  
          return
        }
        fetch( ... ,{
            method: "PUT",      // 이때 PUT 을 활용하면 좋다. (PUT은 새 값을 기존의 값에 "오버라이드" 한다. "추가가 아니고 오버라이드")
            ...                 // "전체 item 의 info 를 담고있는, state 이기 때문에, POST를 한다? 마치 팩토리얼처럼 더해질 것이다."
        })                      //   [X], [X,X,Y] [X,X,Y,X,X,Y,Z,] ... 처럼 저장될것이다...
    },[itemState])				// 종속성에 itemState

 

Redux 의 장점은 "전역으로 같은 State가 업데이트 된다는 것이다"

이것을 이용해, 굳이 fetch를 해당 컴포넌트 안에서 하지 말고, App.js 같은 곳에서 해도 된다.

 

import { useSelector } form "react-redux"

let initial = true  	// ** 처음 실행 했을때, 실행이 되지 않기 위한 initial [첫 실행 막기]

function App() {
    const itemState = useSelector((state) => state.item.items); // 1. 다른 컴포넌트에 의해, item이 변경되면

    useEffect (()=>{
       if(initial){		// [첫 실행 막기] initial 이 true 라면, false 로 바꾸고 뒤에 코드는 실행시키지 않도록 한다. 
          initial = false  
          return
        }
        const sendData = async () =>{          // 3. 평범한 useEffect 에서 비동기 함수 실행시키기 방법
            const response = await fetch( ... ,{
                method: "PUT", 
                ...
            })
                ...
        } 
        sendData()  		// 3. 평범한 useEffect 에서 비동기 함수 실행시키기 방법
    },[itemState])			// 2. useEffect 의 종속성에 의해 실행된다.

    return (
        <Fragment>
            <Header />
            <Auth />
            <Counter />
        </Fragment>
    );
}

export default App;

 

[첫 실행 막기] 에 대하여.

왜 initial 변수를 썼을까?

1. 컴포넌트에서 useEffect 를 사용한 fetch(비동기 함수)의 문제점

    : PUT 을 사용하여, 값을 페칭하면, "초기값이 아무것도 없으므로, 만약 앱을 시작한다면 [기존에 있던] DB 가 비워지는 불상사가 생기고 말 것이다."

    ==> 앱을 처음 시작해서, "초기값이 초기화가 되지 않았다면, 아직 값을 DB 로 PUT 하지 않게 한다"

[추가. 컴포넌트가 RE-RENDER 될때에는, "컴포넌트 밖에 있는 것이랑은 아무련 관련이 없다는 것을 알자"]

2. 해결 : 초기에는 DB 로 넘기지 않기
    : useEffect 는 초기 [앱이 시작될때] 한 번, 그리고, 종속성이 변경될 때마다 실행된다.
    : 문제는 초기에 실행되게 하면 [있던 카트 DB 가 초기화 될 수 있다는 위험] 을 가지고 있다.
        (예를들면, 장바구니에 담아놓고, 다시 껐다 키면, 장바구니가 초기화되어버리는 상황 심지어 DB까지 지워지니...)

    : 그러므로, useEffect 를 사용하지만, "앱이 시작될때는 적용되지 않고", "오로지 종속성에만 영향을 받아야 한다."

    <원리>
        1. 컴포넌트 함수 밖에 initial 변수를 정의한다.
        2. 이 값은, re-render 되도 전혀 영향을 받지 않는다.(초기화가 이루어지지지 않음. 밖 이므로)
        3. 이 값을 이용하여, initial = true (앱 초기) 라면 useEffect에서 함수를 실행할때, 되돌려버리고, initial = false 로 변경한다.
        4. 이렇게되면, 아무일도 없이 initial = false 만 만들고, 초기 phase 가 끝날 것이다.
        5. "초기 phase 는 끝났으니", 이제 useEffect 는 "종속성에 의해서만 함수를 실행할 것이다."

 

 

 

 

**[ 방법2 : 리덕스를 이용하여 fetch 하기 WITH, useEffect ]

 

어? 리듀스 함수 안에서는 fetch 같은 비동기 함수를 사용 할 수 없을텐데!

걱정 마시라, 일반함수를 사용할 것이다.

대신 store 파일 (리듀서함수 아님)에 넣어서 사용 할 것이고,

또, dispatch 를 그 일반함수에 넣어, State 변경( 대부분 Modal, Notification 같은 UI)까지 한번에 이룰 것이다.

 

 

 

[ 방법1. 컴포넌트에서 하는 방법 ]

 

1) 버튼을 누르면, State 가 변경된다.

2) State가 변경되는 의존성에 의해, fetch 가 실행된다.

 

[ 방법2. 리덕스를 이용하여 fetch 하는 방법 ] : Thunk 액션 생성자를 활용한 방법

 

1) 버튼을 누르면 State 가 변경된다.

2) State가 변경되는 의존성에 의해, "정의한 일반함수" 가 실행된다.

3) 정의한 일반 함수"비동기 함수" 를 반환한다. dispatch 도 가능하다.

4) "비동기 함수는 fetch 를 실행한다."

5) 일반함수는 보통 "Store" 안에 존재한다.

 

Thunk 는, "액션 생성자"이다.

: 액션생성자라 하면, "컴포넌트에서, dispatch 를 사용할때, 사용하는 정의된 action 을 호출" 하는 것이 [액션생성자의 생성]이라 할 수 있다 

    import { counterAction } from "../store/index";

    const Counter = () => {
        const dispatch = useDispatch();

        const toggleCounterHandler = () => {
            dispatch(counterAction.toggleCounter());        // counterAction.toggleCounter() => 액션 생성자

 

 

1. Thunk 액션 생성자의 생성

 

0. 일반함수를 만든다 

1. 일반함수의 return 함수 정의 (dispatch 사용하기 위해 dispatch를 파라미터에 넣음)

2. return 되는 함수 안에서, dispatch 하고~,

     fetch 할 비동기 함수 정의 하고~,

     fetch 비동기 함수 실행 하고~

     그에 따른 UI 조작 위한 dispatch  또 하고~

하는걸 export 한다.

< /store/cart.js >

import { createSlice } from "@reduxjs/toolkit";
import { xxActions } from "../../.."

const initialCounterState = { counter: 0, showCounter: true };

const counterSlice = createSlice({ 	// 슬라이스(리듀서함수) 시작 [상관 X] 
    name: "counter",
    initialState: initialCounterState,
        reducers: {
            increment(state) {
                state.counter++;
                    ...
            }
        }
})

// Thunk 액션 생성자 생성

export const sendCartData = ( inputData ) =>{       // 1. Slice(리듀서 함수) 밖에, "일반 자바스크립트 함수" 를 만든다.
    return async (dispatch)=>{       	// 2. "사용할 함수" 를 반환한다. [비동기함수로], 이유는 이 스코프 내에서 바로실행할거라서.
					//  disapath(state조작) 을 사용하기위해, 파라미터에는 dispatch
        dispatch( xxActions.xxx ())     // 3. dispatch도 가능하다. (알림 같은거 사용하기 위한), 파라미터에 있는 dispatch 덕분에

        const sendRequest = async () =>{            // 4. "fetch 를 할 중첩함수"를 정의
            const response = await fetch(...)

            if(!response.ok){
                throw new Error(...)
            }
            const responseData = await response.json()
                ...    
            dispatch( xxActions.xxx ())   // 대충 성공했다는 notification 띄울 action ...
            ...
             
        }   

        try{ 
        await sendRequest                // 5. "정의한 중첩함수 실행" [ 2.반환하는 함수가 async 인 이유]
            dispatch( xxActions.xxx ())    
        }catch(error){
            dispatch( xxActions.xxx ()) 
        }
    }
}

** dispatch 는 "파라미터로 쓴 것" 이므로 따로 import 하여 사용하는 것이 아니다.
** 이 함수 내의 dispatch 는 "현 State를 변형하기 위한 것이 아니라(할순 있지만, 해봐야 같은 함수 안에 있어서 최신 적용 안됨)"\
	혹시 fetch 로 인해 바뀌는 UI( Modal, Notification 등..)의 State 를 변경하기 위한 dispatch 이다.

 

2. useEffect 를 통하여, Thunk 액션 생성자를 사용

<App.js>

    import { useSelector, useDispatch } from "react-redux"
    import { sendCartData } from "../..."       // 1. Thunk 를 import

    let initial = true                          // 2. useEffect 사용하므로, 초기에는 역시나 안되도록 설정

    function App(){
    	const cart = useSelector(state => state.cart)
        useEffect(()=>{
            if(initial){
                initial = false           // initial 이 true 라면, false 로 바꾸고 뒤에 코드는 실행시키지 않도록 한다.
                return
            }
            dispatch(sendCartData(cartData))    // 3. "dispatch" 를 사용하여, 실행
            
        },[cart, dispatch])
    }

 

 

** [ 차이점 ]

[방법들의 차이점] : "컴포넌트 lean" VS "Store lean"

    1. "App.js" 나, "사용하는 컴포넌트"에서 "직접 fetch" 를 하는 방법.

        : 어렵지 않고, 이렇게 해도 된다.

    2.  fetch 하는 "Thunk(액션생성자)" 를, "store 의 논리가 있는 곳"에 만들어, 컴포넌트에서 import 하여 사용하는 바업
    
        : 조금 복잡하지만, "컴포넌트를 Lean 하게 만든다."

    [누굴 더 lean 하게 만드냐의 차이만이 있다.] 예컨대, 아웃소싱 같은것이다.

    보통은 Thunk 를 사용하는 편이 컴포넌트를 관리하기에  더 깔끔하지만, 컴포넌트 내부에서 1번을 할 수도 있다.

    ** 어디든, "Fat" 한 쪽을 조금더 "Lean" 하게 만드는 "코드 관리 차원에서 선택하면 좋겠다."

 

8편 예고. 

Redux-ToolKit [ 비동기, 사이드 이펙트 있는 함수 다루기2 e.g. fetch함수] with useEffect *

리덕스에서 fetch 사용하여 DB 의 값 GET 요청하고 STATE 에 넣기

댓글