본 포스트는 Udemy 의 리액트 강좌를 정리한 글입니다.
- 상품 목록 삭제 시 장바구니에서도 동일 품목 삭제하기
- Redux 와 Firebase 의 상품 목록 동기화하기
- 페이지 로드할 때 Firebase 에서 저장된 상품 목록 받아오기
상품이 품절되서 품목에서 제외하면 장바구니에서도 삭제되어야한다.
이 기능은 cart-reducer
에 관련 함수를 추가해서 간단하게 구현할 수 있었다.
cart-slice.js
상품 목록을 삭제할 때 위의 함수도 같이 실행한다.
payload로 ID를 넘기면 장바구니에 상품이 있는지 ( find() ) 확인하고 해당 상품을 삭제한다. ( filter() )
앱이 꺼져도 상품 목록이 저장 되도록 Firebase 에도 상품 목록을 업데이트 해준다.
- 상품 목록의 데이터를 Firebase 에 보내는 sendProductData() 함수를 생성한다.
- App.js 파일에서 useEffect() 함수로 Redux 의 상품 목록이 변경될 때마다 위의 함수가 실행되도록 설정한다.
한가지 문제점이 있다.
useDispatch() 함수는 React 함수 내에서만 사용할 수 있다.
그러나 sendProductData() 함수는 다른 파일에 정의되었기 때문에 보통처럼 useDispatch() 를 사용할 수 없다.
이를 위한 해결책으로 두가지를 생각해 볼 수 있다.
- sendProductData() 함수를 그냥 App.js 의 리액트 함수 내에 생성한다. 하지만 코드가 더러워지는 것을 피할 수 없다.
- 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 변수를 따로 만들어서 문제를 해결했다.
앱이 실행될 때 Firebase 에서 기존의 상품 목록 정보를 받아온다.
구현하는 방식은 위와 비슷하다.
- Firebase 에서 상품 목록에 대한 정보를 받아오는 fetchProductData() 함수를 생성한다. (위와 같은 방식으로)
- 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() 가 의도치 않게 호출되는 것을 막는 로직 등 전반적인 리액트 앱에 대한 이해도를 높일 수 있었던 경험이었다.
끝.