완벽 가이드 - 실습: 음식 주문

primav·2024년 12월 11일

React

목록 보기
28/35

📌 백엔드 연결 (fetch)

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 을 사용하여 비동기 함수로 처리하는 것이 좋다.

왜 useState를 사용했을까?

  • meals 는 백엔드에서 가져온 데이터를 저장하기 위한 상태이다.
    백엔드에서 데이터를 받아오면 해당 데이터를 UI에 반영해야 한다.
    리액트에서는 상태가 변경될 때 컴포넌트가 자동으로 다시 렌더링 되므로, useState를 사용해 상태를 정의한 것이다.

  • 상태를 사용하지 않으면, 데이터를 받아오더라도 React가 이를 인식하지 못해 화면에 변화가 생기지 않는다.

왜 useEffect를 사용했을까?

  • 컴포넌트가 렌더링된 이후에 데이터를 가져오기 위해 사용했다.
    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>

📌 Context

장바구니 기능을 만드려면 제일 부모인 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를 사용하는 이유

  • Context는 리액트에서 상태나 데이터를 여러 컴포넌트에 걸쳐 쉽게 공유할 수 있도록 돕는다.
  • prop drilling 문제를 피할 수 있다. (props 안쓰면서 계속 전달)
  • 여러 컴포넌트가 데이터를 공유할 때는, 효율적으로 데이터를 관리할 수 있다. (ex: 장바구니)

1. Context 생성

  • createContext : Context 객체 생성
  • 공유할 데이터를 담는 컨테이너 역할을 한다.
import { createContext, useReducer } from "react";

const CartContext = createContext({
  // 컨텍스트 기능 그 자체만으로는 상태 관리를 하지 않는다.
  // 값을 바꾸거나 컴포넌트 업데이트를 하지 않는다.

  // 대신 데이터를 컴포넌트에 펼치는 작업을 한다.
  // 여기서 펼친 데이터는 상태 유지 중이므로 밑의 CartContextProvider 컴포넌트가 데이터 변화를 제어한다.
  items: [], 
  addItem: (item) => {},
  removeItem: (id) => {}, // 초기 상태 제공
});

2. ContextProvider 컴포넌트로 데이터 관리

  • CartContextProvider - 상태관리와 업데이트 로직을 포함하는 컴포넌트
  • CartContext.Provider - 하위 컴포넌트에 Context 데이터를 전달하는 컴포넌트
    • value 속성으로 공유할 데이터를 넘겨줄 수 있다. ({items, addItem, removeItem})
  • children - App 등 하위 컴포넌트를 감싸는데 사용된다.
    ➡️ 해당 트리의 모든 컴포넌트가 Context에 접근할 수 있다.
export function CartContextProvider({ children }) {
  // 위의 컨텍스트를 제공
  // 여기에서 실질적인 데이터 관리 기능이 이루어짐
  
  return <CartContext.Provider>{children}</CartContext.Provider>;
  // CartContext는 Provider 객체를 가진 속성이 됨
  // 이 속성은 컴포넌트로서 반환값이 된다. (감싼 곳이 컴포넌트)
}

3. reducer 함수 생성 & 상태 업데이트

  • 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 문에 안들어가면) 기존 상태 그대로 반환
}

🔑 ADD_ITEM

기존 상태 변경 문제점

  • 상태를 직접 수정하면 리액트가 변경을 감지하지 못함
  • 직접 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 }; // 원본 배열, 업데이트된 배열
}

🔑 REMOVE_ITEM

4. useReducer 함수 연결

  • useReducer - 복잡한 상태 업데이트 로직을 처리한다.

export function CartContextProvider({ children }) {
	useReducer(cartReducer, { items: [] }); // 더 복잡한 상태 관리를 더 쉽게 할 수 있다.
    // 리듀서 함수, 초기 값
   // 여기서 보내는 아이템은 cartReducer에서 state!!
}

5. 객체로 내보내기

export function CartContextProvider({ children }) // 뒤에 생략
export default CartContext;

6. App에 적용

  • CartContextProviderHeaderMenu를 감싸면, 이 두 컴포넌트와 하위 컴포넌트들은 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;

작동방식

  • CartContextProvideruseReducer를 사용하여 상태 관리 로직을 구현한다.
  • CartContext.Providervalue로 상태와 상태 업데이트 함수를 전달한다.
    (items, additems, removeItems)
  • 하위 컴포넌트는 useContext(CartContext)를 사용하여 공유된 상태와 함수를 사용한다.

0개의 댓글