Redux 프로젝트 - 상품 목록 추가 / 삭제하기 (3)

코딩하는 남자·2022년 5월 9일
0

React 정리

목록 보기
5/8
post-thumbnail
post-custom-banner

본 포스트는 Udemy 의 리액트 강좌를 정리한 글입니다.

강의 링크

상품 목록 Firebase 와 연동하기

📌 추가한 기능

  1. 상품 목록 삭제 시 장바구니에서도 동일 품목 삭제하기
  2. ReduxFirebase 의 상품 목록 동기화하기
  3. 페이지 로드할 때 Firebase 에서 저장된 상품 목록 받아오기

🧩 1. 상품 목록 삭제 시 장바구니에서도 동일 품목 삭제하기

상품이 품절되서 품목에서 제외하면 장바구니에서도 삭제되어야한다.
이 기능은 cart-reducer 에 관련 함수를 추가해서 간단하게 구현할 수 있었다.


cart-slice.js

✏️ 코드 설명

상품 목록을 삭제할 때 위의 함수도 같이 실행한다.
payload로 ID를 넘기면 장바구니에 상품이 있는지 ( find() ) 확인하고 해당 상품을 삭제한다. ( filter() )

📌 완성된 모습


🧩 2. ReduxFirebase 의 상품 목록 동기화하기

앱이 꺼져도 상품 목록이 저장 되도록 Firebase 에도 상품 목록을 업데이트 해준다.

📌 구현 순서

  1. 상품 목록의 데이터를 Firebase 에 보내는 sendProductData() 함수를 생성한다.
  2. App.js 파일에서 useEffect() 함수로 Redux 의 상품 목록이 변경될 때마다 위의 함수가 실행되도록 설정한다.

📌 useDispatch() 함수를 리액트 함수 밖에서 사용하는 방법

한가지 문제점이 있다.

useDispatch() 함수는 React 함수 내에서만 사용할 수 있다.
그러나 sendProductData() 함수는 다른 파일에 정의되었기 때문에 보통처럼 useDispatch() 를 사용할 수 없다.

이를 위한 해결책으로 두가지를 생각해 볼 수 있다.

  1. sendProductData() 함수를 그냥 App.js 의 리액트 함수 내에 생성한다. 하지만 코드가 더러워지는 것을 피할 수 없다.
  2. Thunk 를 활용한다.

📌 Thunk 란?

Thunk 는 '다른 작업이 완료될 때까지 작업을 지연시키는 함수' 라고 정의된다.
자바스크립트에서는 함수의 리턴값으로 함수를 내보내서 비동기 함수를 처리하는 방식(Thunk) 를 사용할 수 있다.

Thunk 의 간단한 예

출처 - https://reactgo.com/thunks-javascript

Redux 에서 Thunk 를 사용하면 Dispatch() 함수가 실행되기 전에 우리가 정의한 함수를 먼저 실행시킬 수 있다.

Reducer 가 아닌 함수를 Dispatch 하는 것으로 확인되면 Redux 는 그 함수를 자동으로 실행하고 매개변수로 Dispatch 를 넣어준다.

아래는 그 코드이다.

store/product-actions.js

export const sendProductData = (product) => {

  // Redux가 이 함수를 자동으로 실행한다.
  return async (dispatch) => {
    dispatch(
      uiActions.showNotification({
        status: 'pending',
        title: 'Sending',
        message: 'Sending updated products data!',
      })
    );

    // firebase에 상품 목록을 업데이트하는 함수
    const sendRequest = async () => {
      const response = await fetch(
        'https://order-app-4744c-default-rtdb.firebaseio.com/product.json',
        {
          method: 'PUT',
          body: JSON.stringify({
            items: product.items,
            total: product.total,
          }),
        }
      );
      if (!response.ok) {
        throw new Error('Sending updated products data failed.');
      }
    };

    // try 문으로 위의 sendRequest() 함수를 실행하고 오류가 난다면 catch 문으로 잡아낸다.
    try {
      sendRequest();
      dispatch(
        uiActions.showNotification({
          status: 'success',
          title: 'Success!',
          message: 'Sent updated products data successfully!',
        })
      );
    } catch (error) {
      dispatch(
        uiActions.showNotification({
          status: 'error',
          title: 'Error!',
          message: 'Sending updated products data failed!',
        })
      );
    }
  };
};

App.js

let isInitial = true

 useEffect(() => {
   if (isInitial){ // // 페이지를 처음 로드할 때는 실행 안되게 설정
     isInitial = false
     return
   }
   // dispatch가 반환하는 함수의 매개변수로 dispatch를 넣어준다.
      dispatch(sendProductData(product)); 
  }, [product, dispatch]);

useEffect() 함수는 처음 페이지가 로드될 때에도 실행되는 함수이다.
하지만 처음엔 데이터를 Firebase 에 보낼 이유가 없으므로 isInitial 변수를 따로 만들어서 문제를 해결했다.


📌 완성된 모습


🧩 3. 페이지 로드할 때 Firebase 에서 저장된 상품 목록 받아오기

앱이 실행될 때 Firebase 에서 기존의 상품 목록 정보를 받아온다.
구현하는 방식은 위와 비슷하다.

📌 구현 순서

  1. Firebase 에서 상품 목록에 대한 정보를 받아오는 fetchProductData() 함수를 생성한다. (위와 같은 방식으로)
  2. App.js 파일에서 useEffect() 함수로 앱이 처음 실행될 때 위의 함수가 실행되도록 설정한다.

📌 주의할 점

Firebase 에서 데이터를 받아오면 product 의 상태가 변경되어서 위의 2번에서 만든 useEffect() 가 의도치 않게 실행된다.
이 문제를 해결하기 위해 위에서 만들었던 isInitial 변수와 비슷하게 product 리덕스에 changed 라는 상태를 만들어서 1회 방어용으로 사용한다.

📌 코드

fetch() 함수로 받아온 데이터를 product 에 반영하게 위해 replaceProducts() 라는 reducer 함수를 새로 만들었다. (제품 목록의 수를 체크하는 total 변수를 리덕스에 추가했다.)


product-slice.js

 reducers: {
    replaceProducts(state, action) {
      state.items = action.payload.items;
      state.total = action.payload.total;
    },

product-action.js

export const fetchProductData = () => {
  
  // Redux가 이 함수를 자동으로 실행하고 매개변수로 dispatch를 넣어준다.
  return async (dispatch) => {

    // firebase에서 상품 목록을 받아오는 함수
    const fetchData = async () => {
      const response = await fetch(
        'https://order-app-4744c-default-rtdb.firebaseio.com/product.json'
      );
      if (!response.ok) {
        throw new Error('Could not fetch cart data!');
      }
      const data = await response.json();

      return data;
    };

    // try 문으로 위의 fetchProductData() 함수를 실행하고 오류가 난다면 catch 문으로 잡아낸다.
    try {
      const productData = await fetchData();
      dispatch(
        productActions.replaceProducts({
          items: productData.items || [],
          total: productData.total,
        })
      );
    } catch (error) {
      dispatch(
        uiActions.showNotification({
          status: 'error',
          title: 'Error!',
          message: 'Fetching products data failed!',
        })
      );
    }
  };
};

App.js

// 페이지가 처음 로드될 때 실행 됨
  useEffect(() => {
    dispatch(fetchCartData());
    dispatch(fetchProductData());
  }, [dispatch]);

아래는 위에서 언급한 연쇄 반응을 차단하기 위한 로직을 추가한 코드이다.

App.js

  useEffect(() => {
    if (isInitial.product) { // 처음 로드할 때 실행되는 것을 방지하고
      isInitial.product = false;
      return;
    }
    
    if (product.changed) { // 데이터를 받아올 때 반응하는 것을 방지한다.
      dispatch(sendProductData(product));
    }
  }, [product, dispatch]);

📌 완성된 모습


🧩 마치며

복잡하진 않았던 프로젝트였지만 나만의 기능을 만들어보면서 'Redux 를 어떤 식으로 사용해야겠구나' 를 알 수 있었던 경험이었다. 특히 Thunk 를 활용해서 Dispatch() 함수를 리액트 컴포넌트 밖에서 사용하는 개념은 정말 이해하기 어려웠지만 구글링하고 블로그에 정리하는 과정에서 이해하는데 많은 도움이 되었던 것 같다.

그 외에도 useEffect() 가 의도치 않게 호출되는 것을 막는 로직 등 전반적인 리액트 앱에 대한 이해도를 높일 수 있었던 경험이었다.

끝.

profile
"신은 주사위 놀이를 하지 않는다."
post-custom-banner

0개의 댓글