본 포스트는 Udemy 리액트 완벽가이드 2024 를 듣고 정리한 내용입니다.
목차 🌳
1️⃣ redux로 비동기 작업
2️⃣ 🛒 컴포넌트 안에서 비동기 다루기
3️⃣ 🛒 action creators(액션 생성자) 안 에서 비동기 다루기
이전 강의에서 배운 리덕스에서 리듀서함수는 무조건 순수 함수여야만 하며, side-effect 을 다룰 수 없고, 동기적으로 작동해야 한다고 배웠다. 이에 따라, 동일한 input을 넣었을때 항상 같은 output을 내는 함수를 만들었다. 그치만 여기에서 드는 궁금증!
리덕스로 작업할때, Http 요청은 어떻게 하나? / 비동기 코드는 어디에 넣어야 할까? 🤔
방법은 두가지가 있어요! ✌️
1️⃣ 컴포넌트 안에서 다루기 (useEffect 로 제어)
2️⃣ action creators(액션 생성자) 안 에서 다루기
firebase를 통해, 데베에 장바구니 정보를 저장하고 꺼내쓸 수 있도록 해보자!
-> 1️⃣ 장바구니 수량 추가 및 삭제시, put 요청 보내기
-> 2️⃣ 디비에 저장된 장바구니 정보 get 요청 보내기
단순 cart 값 PUT Req 보내기
function App() {
// 카트 상태를 store에서 가져오기
const cart = useSelector((state) => state.cart.items);
// useEffect를 통해, cart 상태 변경시, store에서 cart를 꺼내와 fetch 함수를 통해 데베에 덮어쓰기
useEffect(() => {
fetch('https://redux-pracice-default-rtdb.firebaseio.com/cart.json', {
method: 'PUT', // PUT method를 통해 데이터를 덮어씀
body: JSON.stringify(cart),
});
}, [cart]);
return (
<Layout>
<Cart />
<Products />
</Layout>
);
}
근데 이렇게 useEffect를 쓰는데 문제점!🚨
앱이 시작될때, 실행이 된다는 점!!
(이게 왜 문젠데요?) -> 왜냐면, 비어있는 초기 카드를 백엔드에 보내고, 거기에 저장된 모든 데이터를 덮어쓰기 때문!!!! 우리는 카트를 클릭을 했을때만 작동하길 원하지만, 마운트시 useEffect가 작동되기 때문이다.
boolean
을 담은 변수를 하나 선언해서, 최초 랜더링인지를 판별하여 실행
const isInitial = true // 전역으로 선언해줌
function App () {
useEffect(() => {
const sendCartData = async () => {
// ... 생략
};
// 초기 랜더링일 경우, isInitial를 뒤집고 return
if (isInitial) {
isInitial = false;
return;
}
sendCartData().catch((error) => {
// ... 생략
});
}, [cart, dispatch]);
}
useEffect에 side Effect을 다루다보니 상당히 component가 길어진 경향이 있다...
2번째 방법을 통해 다른 방법도 찾아보자!
redux Toolkit
에서 우린 createSlice
를 통해 자동으로 생성된 액션 생성자를 사용해왔다.
// 액션 생성자 자동 생성
export const cartActions = cartSlice.actions;
위와 같이 .acions
를 이용했었지만, 사실은 개발자가 직접 action
을 만들 수도 있다!
우린 action에 비동기 작업을 담기 위해, action creator
를 thunk
를 이용하여 만들어보려고 한다.
thunk
란 ? 🥸
다른 작업이 완료될때까지 작업을 지연시키는 단순한 함수! (나중에 실행할 코드 조각이란 뜻)
-> 작업 객체를 즉시 반환하지 않는 action creators를 작성할 수 있음
action 에 대한 return 값으로 함수를 넘겨주고, 그 함수 안에side Effect
을 넣는다.
(이로써 지연을 통해, action을 보내기 전에 비동기적으로 작업을 수행할 수 있다)
slice 파일 안에서, thunk 함수 정의
const cartSlice = createSlice({
// .. 생략
});
// 슬라이스 외부에 Thunk 함수를 만듦
export const sendCartData = (cart) => {
return async (dispatch) => {
// 첫 번째 액션: 요청을 시작했다는 알림을 디스패치
dispatch(
modalActions.showNotification({
status: 'pending',
title: 'sending...',
message: 'sending cart data!',
})
);
const sendReq = async () => {
const response = await fetch('https://redux-pracice-default-rtdb.firebaseio.com/cart.json', {
method: 'PUT',
body: JSON.stringify(cart),
});
if (!response.ok) {
throw new Error('sending cart data failed');
}
};
try {
// 실제 비동기 Http Req
await sendReq();
// 두 번째 액션: 요청이 성공했음을 알리는 알림을 디스패치
dispatch(
modalActions.showNotification({
status: 'success',
title: 'Success!',
message: 'send cart data successfully',
})
);
} catch (error) {
// 세 번째 액션: 요청이 실패했음을 알리는 알림을 디스패치
dispatch(
modalActions.showNotification({
status: 'error',
title: 'Failed!',
message: 'fail to send cart data :(',
})
);
}
};
};
사용할땐 useEffect
훅 안에서 dispatch
의 인자로thunk
함수를 호출하면 된다.
(이때 자동으로 dispatch
가 thunk
함수에 대한 인자로 전달됨)
function App() {
// ...생략
useEffect(() => {
// 마운트시 실행 방지
if (isInitial) {
isInitial = false;
return;
}
dispatch(sendCartData(cart));
}, [cart, dispatch]);
return (
<>
// ...생략
</>
);
}
export default App;
fetch 를 담은 thunk 함수 정의
export const fetchCartData = () => {
return async (dispatch) => {
const fetchData = async () => {
const res = await fetch('https://redux-pracice-default-rtdb.firebaseio.com/cart.json');
if (!res.ok) {
throw new Error('could not fetch data');
}
const data = await res.json();
return data;
};
try {
const cartData = await fetchData();
console.log(cartData);
// 장바구니에 물건이 없을 경우를 대비하여, [] 설정
dispatch(cartActions.replaceCart({ items: cartData.items || [], totalQuantity: cartData.totalQuantity }));
} catch (err) {
console.log(err);
dispatch(
modalActions.showNotification({
status: 'error',
title: 'Failed!',
message: 'fail to get cart data :(',
})
);
}
};
};
component 안에서 useEffect로 실행
// 카트 정보 fetch
useEffect(() => {
dispatch(fetchCartData());
}, [dispatch]);
// 카드 정보 put
useEffect(() => {
if (isInitial) {
isInitial = false;
return;
}
// 장바구니에 변경 사항이 있을때만 작동되도록
if (cart.changed) {
dispatch(sendCartData(cart.items, cart.totalQuantity));
}
}, [cart, dispatch]);
리덕스 slice가 복잡해질때, 전체 리덕스 스토어의 현재 상태를 살펴볼때 사용!
어떤 action 이 불려,
상태에 어떠한 변화가 이루어졌는지 보기 쉬움