const [meals, setMeals] = useState([]);
useEffect(() => {
async function fetchData() {
const response = await fetch("http://localhost:3000/meals");
if (!response.ok) {
//
}
const data = await response.json();
setMeals(data);
}
fetchData(); // 함수 호출
}, []);
백엔드 서버와 연결을 하려면 시간이 걸리므로 async & await 을 사용하여 비동기 함수로 처리하는 것이 좋다.
meals 는 백엔드에서 가져온 데이터를 저장하기 위한 상태이다.
백엔드에서 데이터를 받아오면 해당 데이터를 UI에 반영해야 한다.
리액트에서는 상태가 변경될 때 컴포넌트가 자동으로 다시 렌더링 되므로, useState를 사용해 상태를 정의한 것이다.
상태를 사용하지 않으면, 데이터를 받아오더라도 React가 이를 인식하지 못해 화면에 변화가 생기지 않는다.
컴포넌트가 렌더링된 이후에 데이터를 가져오기 위해 사용했다.
useEffect를 사용하지 않으면 데이터를 가져오는 작업(fetch)이 컴포넌트의 렌더링 흐름과 충돌할 수 있다.
종속성 배열 ([])로 최초 실행만 유도
이 배열이 비어있으면, fetchData는 컴포넌트가 처음 렌더링될 때 한번만 실행된다.
데이터를 가져오는 작업이 비동기 작업이므로, 시간이 걸린다.
useEffect 를 사용하지 않으면 화면이 나타나지 않는다. (아직 데이터가 도착 안해서)
💡 결론
useState의 역할백엔드에서 가져온 데이터를 저장하고 UI에 반영하기 위해 사용된다.
상태(meals)가 변경되면 React가 이를 감지하여 컴포넌트를 다시 렌더링한다.
useEffect의 역할데이터를 가져오는 비동기 작업을 컴포넌트의 렌더링 이후에 실행하기 위해 사용된다.
네트워크 요청은 시간이 걸리기 때문에, 렌더링 로직과 분리하여 처리하는 것이 중요하다.
📁 Menu.jsx
import Meal from "./Meal";
return (
<ul id="meals">
{meals.map((meal) => (
<Meal meal={meal} key={meal.id} />
))}
</ul>
);
📁 Meal.jsx
export default function Meal({ meal }) {
return (
<>
<li className="meal-item">
<article>
<img src={`http://localhost:3000/${meal.image}`} />
<div>
<h3>{meal.name}</h3>
<span className="meal-item-price">${meal.price}</span>
<p className="meal-item-description">{meal.description}</p>
</div>
<p className="meal-item-actions">
<button className="button">Add to Cart</button>
</p>
</article>
</li>
</>
);
}
📁 Button.jsx
export default function Button({ children, textOnly, className, ...props }) {
const cssClasses = textOnly
? `text-button ${className}`
: `button ${className}`;
return (
<button className={cssClasses} {...props}>
{children}
</button>
);
}
📁 Menu.jsx
import Button from "./UI/Button";
<Button textOnly>Cart</Button>
📁 Meal.jsx
import Button from "./UI/Button";
<Button>Add to Cart</Button>
장바구니 기능을 만드려면 제일 부모인 app.jsx 파일에서 데이터를 관리해야 한다.
여기서 데이터를 관리해야 각 컴포넌트에 데이터를 주고 받을 수 있기 때문이다.
하지만 이렇게 진행하면 부모의 코드가 비대해지고, prop drilling의 문제점이 생길 수 있다.
📁 App.jsx
import Header from "./components/Header";
import Menu from "./components/Menu";
function App() {
return (
<>
<Header />
<Menu />
</>
);
}
export default App;
Context는 리액트에서 상태나 데이터를 여러 컴포넌트에 걸쳐 쉽게 공유할 수 있도록 돕는다.prop drilling 문제를 피할 수 있다. (props 안쓰면서 계속 전달)createContext : Context 객체 생성import { createContext, useReducer } from "react";
const CartContext = createContext({
// 컨텍스트 기능 그 자체만으로는 상태 관리를 하지 않는다.
// 값을 바꾸거나 컴포넌트 업데이트를 하지 않는다.
// 대신 데이터를 컴포넌트에 펼치는 작업을 한다.
// 여기서 펼친 데이터는 상태 유지 중이므로 밑의 CartContextProvider 컴포넌트가 데이터 변화를 제어한다.
items: [],
addItem: (item) => {},
removeItem: (id) => {}, // 초기 상태 제공
});
CartContextProvider - 상태관리와 업데이트 로직을 포함하는 컴포넌트CartContext.Provider - 하위 컴포넌트에 Context 데이터를 전달하는 컴포넌트value 속성으로 공유할 데이터를 넘겨줄 수 있다. ({items, addItem, removeItem})children - App 등 하위 컴포넌트를 감싸는데 사용된다.export function CartContextProvider({ children }) {
// 위의 컨텍스트를 제공
// 여기에서 실질적인 데이터 관리 기능이 이루어짐
return <CartContext.Provider>{children}</CartContext.Provider>;
// CartContext는 Provider 객체를 가진 속성이 됨
// 이 속성은 컴포넌트로서 반환값이 된다. (감싼 곳이 컴포넌트)
}
state - 현재 상태를 나타낸다. // 초기 상태가 { items: [] } 일 때
const initialState = { items: [] }; // 초기 상태
action - 어떤 변경을 수행할지 알려주는 객체 (type, payload를 포함) const action = {
type: "ADD_ITEM", // 실행할 동작의 유형
item: { id: 1, name: "Pizza", price: 9.99 }, // 추가할 항목 데이터
};
function cartReducer(state, action) {
// 업데이트된 상태를 반환
// action 객체 : 함수에게 업데이트 방식 알려줌
// 실행되는 액션을 알 수 있음 (임의로 이름 정하기)
if (action.type === "ADD_ITEM") {
// 추가된 데이터 상태 업데이트
}
if (action.type === "REMOVE_ITEM") {
// 제거된 데이터 상태 업데이트
}
return state; // 아무 액션도 없으면 (if 문에 안들어가면) 기존 상태 그대로 반환
}
기존 상태 변경 문제점
push 하면 메모리에서 기존 배열이 바뀌므로, 변경을 추적하지 못해 UI가 업데이트 안됨 ➡️ 새로운 배열을 생성(spread 연산자) 하여 상태를 업데이트 한다.
❌ 기존 상태 변경
state.items.push(action.item)
새로운 배열 생성하여 상태 업데이트
✅ 새로운 배열 생성하여 상태 업데이트
// 1. 항목 수량 관리
const existingCartItemIndex = state.items.findIndex(
(item) => item.id === action.item.id
);
const updatedItems = [...state.items]; // 복사본 생성
// 2. 수량 업데이트
if (existingCartItemIndex > -1) { // 항목 존재
const existingItem = state.items[existingCartItemIndex];
const updatedItem = {
...existingItem, // 복붙
quantity: existingItem.quantity + 1, // 수량 늘리기
};
updatedItems[existingCartItemIndex] = updatedItem;
} else { // 항목 없으면
updatedItems.push({ ...action.item, quantity: 1 }); // 항목 추가
}
return { ...state, items: updatedItems }; // 원본 배열, 업데이트된 배열
}
useReducer - 복잡한 상태 업데이트 로직을 처리한다.export function CartContextProvider({ children }) {
useReducer(cartReducer, { items: [] }); // 더 복잡한 상태 관리를 더 쉽게 할 수 있다.
// 리듀서 함수, 초기 값
// 여기서 보내는 아이템은 cartReducer에서 state!!
}
export function CartContextProvider({ children }) // 뒤에 생략
export default CartContext;
CartContextProvider로 Header와 Menu를 감싸면, 이 두 컴포넌트와 하위 컴포넌트들은 Context를 통해 상태와 함수에 접근할 수 있다.import { CartContextProvider } from "./store/cart-context";
import Header from "./components/Header";
import Menu from "./components/Menu";
function App() {
return (
<CartContextProvider>
<Header />
<Menu />
</CartContextProvider>
);
}
export default App;
CartContextProvider는 useReducer를 사용하여 상태 관리 로직을 구현한다.CartContext.Provider의 value로 상태와 상태 업데이트 함수를 전달한다.items, additems, removeItems)useContext(CartContext)를 사용하여 공유된 상태와 함수를 사용한다.