Home 09-Redux를-활용한-비동기-요청
Post
Cancel

09-Redux를-활용한-비동기-요청

강의 링크


리듀서 함수는 다음과 같은 특징이 있다.

  • 순수 함수이다
  • 부수 효과가 없다.
  • 동기식이여야 한다.

즉, 인풋을 받아 아웃풋을 생성해내는 순수하고 부수 효과가 없는 동기식 함수이다. (예측 가능해야 하기 때문에 Redux는 동기식으로 처리한다. )

그런데 리덕스로 작업할 때 HTTP 요청과 같이 부수 효과가 수반되는 일부 작업을 전달할 때 어떻게 해야 할까.


1. Redux를 활용하여 HTTP 요청 처리하기

우선 리듀서는 순수 함수여야 하고, 부수 효과도 없고 동기식이어야 한다는 점을 유념해야 한다.

따라서 부수 효과를 생성하거나 http 요청을 보내는 것과 같이 비동기식인 코드는 리듀서 함수에 들어가서는 안 된다.


image-20220911210157012


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useEffect } from 'react';
import { useSelector } from 'react-redux';

import Cart from './components/Cart/Cart';
import Layout from './components/Layout/Layout';
import Products from './components/Shop/Products';

function App() {
  const showCart = useSelector((state) => state.ui.cartIsVisible);
  const cart = useSelector((state) => state.cart);

  useEffect(() => {
    fetch('https://react-http-6b4a6.firebaseio.com/cart.json', {
      method: 'PUT',
      body: JSON.stringify(cart),
    });
  }, [cart]);

  return (
    <Layout>
      {showCart && <Cart />}
      <Products />
    </Layout>
  );
}

export default App;
  • redux 스토어를 업데이트 할 때 마다, http 요청을 보냄으로 redux와 서버에 있는 데이터를 맞춘다.


1) useEffect를 활용하여 데이터 보내기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/App.js
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';

let isInitial = true;

function App() {
  const dispatch = useDispatch();
  const reduxState = useSelector((state) => state.reduxState);

  useEffect(() => {
    dispatch(action 객체 생성)
    
    // 비동기 작업 수행
    const sendHttp = async () => {
     //.. 생략
      
    // 처음 렌더링 될때 요청을 보내고 싶지 않기 때문에, 바로 return 한다. 
    if (isInitial) {
      isInitial = false;
      return;
    }

   // 만약 내부에서 dispath함수를 사용하면, dispatch 함수는 이제 종속 항목이다.(useDispatch에 의해 생성된)
   // Redux는 이것은 절대 변경되지 않는 함수임을 보장하기 때문에 이것을 의존성 배열에 안전하게 추가할 수 있다. 
  }, [reduxState, dispatch]);

  return (
    //.. 생략
  );
}

export default App;
  • useEffect를 활용하여 Redux의 State가 변경되면 비동기 작업을 수행한다.
    • 데이터를 추가, 수정, 삭제 => Redux에 반영
    • Redux가 변경되면 서버와 통신


2) action을 활용하여 데이터 보내기

redux state가 변경될 때, 비동기 함수를 dispatch한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/store/state-actions.js

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

import { uiActions } from './ui-slice';

const stateSlice = createSlice({
  name: 'reduxState',
 //.. 생략
});

// action creator
export const sendData = (slice) => {
  // 리덕스 툴킷을 통해 자동으로 action을 dispatch한다. 
  // action 객체를 생성하는 것이 아닌 함수를 dispatch할 수 있다. 
  // 함수를 dispatch하면 함수를 자동으로 실행하고 자동으로 dispatch를 인수로 제공한다. 
  // 실행된 함수에서 우리는 다시 디스패치 할 수 있다.
  return async (dispatch) => {
    dispatch(action 객체 생성)
      
    // 비동기 작업 수행
    const sendHttp = async () => {
    //.. 생략
    }
};

export const stateActions = stateSlice.actions;

export default cartSlice;


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/App.js
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { sendData } from './store/cart-slice';

let isInitial = true;

function App() {
  const dispatch = useDispatch();
  const reduxState = useSelector((state) => state.reduxState);

  useEffect(() => {
    const sendHttp = async () => {
  
    if (isInitial) {
      isInitial = false;
      return;
    }

	dispatch(sendData(reduxState))
  }, [reduxState, dispatch]);

  return (
    //.. 생략
  );
}

export default App;
  • 비동기 작업을 컴포넌트가 아닌 Redux Action을 dispatch하는 함수에서 관리한다. (컴포넌트의 코드가 훨씬 가벼워진다.)
  • useEffect()를 활용하여, Redux의 state가 변경되면 dispatch를 한다.
  • dispatch 하는 함수에 비동기 작업을 하는 함수를 실행한다.


3) 데이터 fetch 하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/store/state-actions.js

export const fetchData = () => {
  return async (dispatch) => {
    const fetchData = async () => {
      const response = await fetch(
        'https://react-http-6b4a6.firebaseio.com/cart.json'
      );
	
      //.. 생략
      const data = await response.json();
      return data;
    };

    try {
      const newData = await fetchData();
        
      // 새로 받은 데이터를 Redux에 payload로 전달하고 데이터를 변경한다.
      dispatch(Redux에 있는 stateData를 newData로 교체)
      );
    } catch (error) {
        //.. 생략
      );
    }
  };
};

  • 데이터를 fetch해서 Redux에 반영한다.
  • 이는 데이터의 값을 변경하는 Actions를 dispatch하여 진행한다.
    • dispatch할 때, fetch한 데이터를 전달하여 값을 변경한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/App.js
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { sendData } from './store/cart-slice';

let isInitial = true;

function App() {
  const dispatch = useDispatch();
  const reduxState = useSelector((state) => state.reduxState);

  // 처음 컴포넌트가 생성될 때, 데이터를 fetch 한다. 
  useEffect(() => {
    dispatch(fetchData());
  }, [dispatch]);

  return (
    //.. 생략
  );
}

export default App;


2. RTK 활용하기

공식 문서

1) RTK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import axios from "axios";
import {createAsyncThunk, createSlice} from "@reduxjs/toolkit"
import { useDispatch, useSelector } from 'react-redux';


// 비동기 action creator이다. 
export const fetchNumber = createAsyncThunk(
  "counter/fetchNumber",
  async {payload, thunkAPI} => {
    const data = await axios("api 링크");
    return data.number
  }
)

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

const counterSlice = createSlice({
  name: 'counter',
  initialState: initialCounterState,
    
  // sync
  reducers: {
    increase(state, action) { 
      state.counter = state.counter + action.payload;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
    
  // async
  extraRedcers: {
    [fetchNumber.pending]: (state, action ) => {
      state.loading = true;
    },
    [fetchNumber.fulfilled]: (state, action) => {
      state.loading = false;
      state.counter = action.payload;
    },
    [fetchNumber.rejected]: (state, action) => {
      state.loading = false;
      state.error = true;
    }
  }
});


// custom 훅 생성하여 componenet에서 사용
// const {loading, error, counter} = useFecthNumber(); 
export const useFecthNumber = () => {
  const {loading, error, counter} = useSelector(selectCounter);
  const dispatch = useDispatch();
    
  useEffect(()=> {
    dispatch(fetchNumber()) // async action thunk  
  })
    
  return {loading, error, counter}
}


// 성능 관리 (캐시: 메모이제이션 활용)
// 상태가 변하지 않으면 캐시된 데이터를 반환한다.
export const selectCounter = createSelector(
  // 입력 셀렉터
  (state) => state.counter,
    
  // 출력 셀렉터
  (counter) => counter
)
메서드설명
createAsyncThunk()비동기 처리할 때 사용한다.
액션 타입 문자 값과 Promise를 반환하는 함수이다.
pending, fulfilled, rejected action 타입을 dispatch하는 Thunk를 생성한다.
createSelector(입력 셀렉터, 출력 셀렉터)useSelector()를 사용하면, 특정 state를 가져올 수 있다.
useSelector를 사용하기 전, 상태에 처리작업을 하고 이를 메모이제이션 하는 함수이다.
컴포넌트가 상태를 처리하여, 컴포넌트가 리렌더링되는 것을 막는다.
입력 셀렉터는 모든 인자와 하께 호출된다.
입력 셀렉터 반환 값이 변경될 경우, 출력 셀렉터가 다시 실행된다.
출력 셀렉터의 반환 값은 캐시된다.


Tip: LocalStorage 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/utils/LocalStorag.js
const STORE_KEY = 'redux/store';

export const loadState = (key = STORE_KEY) => {
  const storeData = localStorage.getItem(key);
  return storeData ? JSON.parse(storeData) : undefined;
};

export const saveState = (storeState, key = STORE_KEY) => {
  const serializedState = JSON.stringify(storeState);
  localStorage.setItem(key, serializedState);
};

export const removeState = (key = STORE_KEY) => {
  localStorage.removeItem(key);
};


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/store.index.js

import { configureStore } from '@reduxjs/toolkit';
import { loadState, saveState } from 'utils';

import counterReducer from './counter';
import authReducer from './auth';


const store = configureStore({
  reducer: { counter: counterReducer, auth: authReducer },
  
  // 로컬스토리지에 데이터가 있으면 우선 기본 값으로 가져온다. 
  preloaded: loadState() ?? {},
});


// 구독을 통해 store가 변경되면 saveState를 실행한다. 
export const subscription = () => {
  return store.subscribe(throttle(() => saveState(store.getState()), 1000));
};

export const unsubscription = subscription();


export default store;


2) RTK 쿼리

데이터를 로딩하는 흔한 케이스를 간단하게 하는 도구이다.

공식 문서

어떤 컴포넌트든 같은 쿼리를 구독하면 항상 같은 데이터를 사용할 수 있도록 보장한다. 중복 요청을 자동 제거 하기 때문에 성능에도 좋다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const contactsApi = createApi({
  reducerPath: 'contactsApi',
  baseQuery: fetchBaseQuery({
    baseUrl: "베이스 URL",
  }),
  tagTypes: ['Contact'],
  endpoints: (builder) => ({
    // QUERY
    contacts: builder.query({
      query: () => '/contacts',
      providesTags: ['Contact'],
    }),

    // MUTATION
    addContact: builder.mutation({
      query: (newContact) => ({
        url: '/contacts',
        method: 'POST',
        body: newContact,
      }),
      invalidatesTags: ['Contact'],
    }),
    updateContact: builder.mutation({
      query: ({ id, ...willUpdateContact }) => ({
        url: `/contacts/${id}`,
        method: 'PUT',
        body: willUpdateContact,
      }),
      invalidatesTags: ['Contact'],
    }),
    deleteContact: builder.mutation({
      query: (id) => ({
        url: `/contacts/${id}`,
        method: 'DELETE',
      }),
      invalidatesTags: ['Contact'],
    }),
  }),
});

export const {
  useContactsQuery,
  useAddContactMutation,
  useUpdateContactMutation,
  useDeleteContactMutation,
} = contactsApi;
메서드설명
createApi()데이터를 패치하고 변환하는 설정을 포함해서 엔드포인트들에서 어떻게 데이터를 패치하는지 정의한다.
api를 생성하는 function이다.
Query (Read), Mutation(Create, Update, Delete) 을 반환한다.
fetchBaseQuery()baseUrl을 등록할 수 있다.
setupListeners()refetchOnMountrefetchOnReconnect기능을 위해 사용되는 유틸리티이다.
This post is licensed under CC BY 4.0 by the author.