UMC 10주차 미션 및 스터디 마무리
6월 16일 부로 10주차 UMC 워크북에 구성되어 있는 미션들 중 마지막 미션을 완료하였다. 9주차 부터 Redux를 이용한 음악 플레이리스트 웹을 구현했다. 기존에는 mock 데이터를 이용하여 플레이리스트들을 구현했다면 이번주차 미션은 실제 백엔드 서버와 통신하여 가져온 플레이리스트 데이터들을 랜더링 하는것이 미션의 핵심이었다. Redux를 사용해본적이 없었는데 이번 UMC 9~10주차 스터디를 통해서 기본적인 사용법들을 배울 수 있어서 유익했다. 이전에 사용했던 Zustand 그리고 Recoil과 다르게 문법적으로 불편한 점이 있긴하지만 기본적으로 다루는 로직들은 비슷하기에 내용을 이해하는데 어렵진 않았다. 이제 여름방학 때 인하대, 숭실대, 가톨릭대 팀원분들과 팀프로젝트를 하게 될텐데 스터디를 통해 배운점들 그리고 따로 공부한 것들을 적극적으로 활용해서 1인분 이상을 하는 팀원이 되도록 노력해보자!
<백엔드 가상 서버>
-> UMC 리드님께서 제공해주신 백엔드 레포지토리를 깃허브에 fork 후 로컬로 clone하였다. 이후 npm start를 실행 시켜 서버를 가동하였다.
<구현 과정>
1. cartSlice 수정
✅ createAsyncThunk
개념: Redux Toolkit에서 비동기 액션을 생성하는 함수이다. 주로 API 호출과 같은 비동기 작업을 처리하는 데 사용된다. 또한 createAsyncThunk는 Promise 기반의 비동기 함수를 입력으로 받아 Redux 액션을 수행한다. ( ex. asnyc, await 사용 )
-> try-catch문을 통해 데이터 통신의 결과에 따라서 데이터 결과 출력 or thunkAPI.rejectWithValue를 이용해 에러메시지를 출력한다.
✅ rejectWithValue
-> createAsyncThunk 내부에서 오류처리를 할 수 있게 도와준다.
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
// 서버로부터 데이터를 가져오는 thunk 생성
export const fetchCartItems = createAsyncThunk(
"cart/fetchCartItems",
async (_, thunkAPI) => {
try {
const response = await axios.get("http://localhost:8080/musics");
console.log(response.data); // 데이터 구조 확인 겸 response.data 출력해봄
return response.data;
} catch (error) {
const errorMessage = error.response?.data || "서버 응답이 없습니다.";
return thunkAPI.rejectWithValue(errorMessage);
}
}
);
//redux-toolkit 초기 상태 설정
const initialState = {
items: [],
totalQuantity: 0,
totalAmount: 0,
status: "idle",
error: null,
};
✅ Slice 만들기(cartSlice.js)
-> createSlice는 name, initialState, reducers 세 요소를 요구한다.
name은 action 앞에 첨가되어 다른 slice의 action들과 이름 중복을 피하는 역할을 한다. initialState에는 미리 생성한 initialState를 넣는다(위에 있음). 그리고 reducers의 각각은 action 역할을 하며 동시에 state의 변화까지 구현한다.
✅ extraReducers
개념: createSlice 함수 내에서 추가적인 리듀서 로직을 정의하는데 사용된다. 주로 비동기 액션(createAsyncThunk로 생성된 액션)의 상태 변화를 처리하는데 사용된다.
→ ex : rejected, fulfilled, pending, 해당 구현 과정에서는 fetchCardItems.rejected, fetchCardItems.fulfilled, fetchCardItems.pending으로 비동기 상태를 구분한다.
✅ BuilderCallback Notation
: extraReducers에서 사용되는 콜백 함수로, builder 객체를 통해 액션 타입에 따라 리듀서를 추가하는 방식이다.
→ addCase: 특정 액션 타입에 대한 리듀서를 추가한다
✅ action.payload
- 액션과 함께 전달되는 데이터이다.
- 상태를 변경하거나 업데이트하는 데 필요한 정보를 담고 있다.
- 비동기 작업에서 fulfilled 상태일 때는 서버의 응답 데이터가, rejected 상태일 때는 에러 메시지가 payload로 전달된다.
const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
increase(state, action) {
const item = state.items.find((item) => item.id === action.payload);
if (item) item.amount++;
state.totalQuantity++;
state.totalAmount += item.price;
},
decrease(state, action) {
const item = state.items.find((item) => item.id === action.payload);
if (item) {
if (item.amount > 1) {
item.amount--;
state.totalQuantity--;
state.totalAmount -= item.price;
} else {
state.items = state.items.filter(
(item) => item.id !== action.payload
);
state.totalQuantity--;
state.totalAmount -= item.price;
}
}
},
removeItem(state, action) {
const item = state.items.find((item) => item.id === action.payload);
if (item) {
state.items = state.items.filter((item) => item.id !== action.payload);
state.totalQuantity -= item.amount;
state.totalAmount -= item.amount * item.price;
}
},
clearCart(state) {
state.items = [];
state.totalQuantity = 0;
state.totalAmount = 0;
},
calculateTotals(state) {
let totalQuantity = 0;
let totalAmount = 0;
state.items.forEach((item) => {
totalQuantity += item.amount;
totalAmount += item.amount * item.price;
});
state.totalQuantity = totalQuantity;
state.totalAmount = totalAmount;
},
},
extraReducers: (builder) => {
// 각각 pending부터 rejected까지 그에 맞는 상태값 저장
builder
.addCase(fetchCartItems.pending, (state) => {
state.status = "loading";
})
.addCase(fetchCartItems.fulfilled, (state, action) => {
state.status = "succeeded";
state.items = action.payload;
state.totalQuantity = action.payload.reduce(
(total, item) => total + item.amount,
0
);
state.totalAmount = action.payload.reduce(
(total, item) => total + item.price * item.amount,
0
);
})
.addCase(fetchCartItems.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload; // 에러 메시지를 상태에 저장
console.log("Rejected action payload:", action.payload); // 에러 메시지 출력
alert(action.payload); // 에러 메시지를 alert 창으로 표시
});
},
});
2. App.jsx에서 Loading 상태에 따른 랜더링 구현
✅ 클라이언트 단에서 데이터를 받아올 때 까지 Loading 창이 보이도록 구현
✅ 상태관리 로직에 있는 상태를 가져와 success 혹은 failed 일때 1초동안 로딩 상태이도록 구현
-> useSelector를 통해 상태 가져오고 useDispatch()를 통해 cartSlice.js에 있는 fetchCardItems 비동기 통신 함수 호출
-> setTimeOut 사용 + 컴포넌트가 언마운트될 때도 useEffect의 클린업 함수가 실행되어 이전 타이머 정리
import React, { useEffect, useState } from "react";
import Header from "./components/Header";
import CartItem from "./components/CartItem";
import Modal from "./components/Modal";
import LoadingSpinner from "./components/LoadingSpinner";
import { fetchCartItems } from "./redux/cartSlice"; // 데이터 가져오기 Thunk
import styled from "styled-components";
const AppWrapper = styled.div`
text-align: center;
`;
const CartItemsWrapper = styled.div`
max-width: 600px;
margin: 0 auto;
`;
function App() {
const dispatch = useDispatch();
const items = useSelector((state) => state.cart.items);
const status = useSelector((state) => state.cart.status);
const [localLoading, setLocalLoading] = useState(true); // 로컬 로딩 상태 추가
useEffect(() => {
if (status === "idle") {
dispatch(fetchCartItems());
}
}, [status, dispatch]);
useEffect(() => {
//setTimeout을 상태관리 관련 로직에다말고 클라이언트단에서 구현
if (status === "loading") {
setLocalLoading(true);
} else if (status === "succeeded" || status === "failed") {
const timer = setTimeout(() => {
setLocalLoading(false);
}, 1000); // 최소 1초 동안 로딩 상태 유지 -> 눈에 보이도록 로딩 되는게
return () => clearTimeout(timer);
}
}, [status]);
return (
<AppWrapper>
<Header />
<main>
{localLoading ? (
<LoadingSpinner />
) : (
<CartItemsWrapper>
{items.map((item) => (
<CartItem
key={item.id}
id={item.id}
title={item.title}
price={item.price}
amount={item.amount}
img={item.img}
/>
))}
</CartItemsWrapper>
)}
</main>
<Modal />
</AppWrapper>
);
}
export default App;
<깃허브>
https://github.com/6th-UMC-TUK/6th-UMC-Web/commit/10fdd9512d7c6ca8e23aae12b55d355cefd29bf6
10주차 미션 완료 · 6th-UMC-TUK/6th-UMC-Web@10fdd95
Jayjunyoung committed Jun 16, 2024
github.com