useContext란?

useContext는 React에서 상태 관리를 간편하게 하기 위한 Hook 중 하나이다. 이 Hook을 사용하면 React 컴포넌트 간에 데이터를 전달하고 공유할 수 있다. 주로 전역 상태 관리나 테마 설정과 같이 컴포넌트 트리를 횡단하는 데이터를 처리하는 데 사용된다.

useContext를 사용하는 주요 단계

1. Context 객체 생성

export const OrderContext = createContext();

2. Context 제공자(Component) 생성

  • 데이터를 다른 컴포넌트에 제공하는 컴포넌트를 생성한다. 이 컴포넌트는 OrderContext.Provider컴포넌트로 감싸야 한다.
  • value prop을 사용하여 공유할 데이터를 설정한다.
return (
  <OrderContext.Provider value={value}>{props.children}</OrderContext.Provider>
);

이때, 위 코드를 다음과 같이 전개 연산자를 사용하여 표현할 수도 있다.

return <OrderContext.Provider value={value} {...props} />;

3. Context 사용

  • useContext Hook을 사용하여 컴포넌트 내에서 공유 데이터에 접근한다.
  • 이 Hook은 OrderContext.Provider에서 제공한 value를 반환한다.
import React, { useContext } from "react";

...

const [orderData, updateItemCount] = useContext(OrderContext);

...

return <p> 가격: {orderData.totals[orderType]}</p>

여기에서 배열의 형태로 반환된 value를 받아온 이유는 OrderContext.Provider에서 다음과 같은 형태로 prop을 내려주기 때문이다.

const value = useMemo(() => {
    ...

    return [{ ...orderCounts, totals }, updateItemCount];
  }, [orderCounts, totals]);

4. 컴포넌트 트리에 Context 제공자 추가

  • 앱의 루트 컴포넌트 또는 필요한 범위에서 OrderContextProvider를 사용하여 Context 제공자를 추가한다.
root.render(
  <OrderContextProvider>
    <App />
  </OrderContextProvider>
);

불변성 보존(Immutable)

OrderContext.Provider에서 prop으로 내리는 value부분의 코드는 다음과 같다.

const [orderCounts, setOrderCounts] = useState({
    products: new Map(),
    options: new Map(),
});

...

const value = useMemo(() => {
  function updateItemCount(itemName, newItemCount, optionType) {
    // 불변성을 지키기 위해 새로운 Map을 만들어서 반환
    const oldOrderMap = orderCounts[optionType];
    const newOrderMap = new Map(oldOrderMap);

    newOrderMap.set(itemName, parseInt(newItemCount));

    // orderCounts를 업데이트
    const newOrderCounts = { ...orderCounts };
    newOrderCounts[optionType] = newOrderMap;

    setOrderCounts(newOrderCounts);
  }

  return [{ ...orderCounts, totals }, updateItemCount];
}, [orderCounts, totals]);

위 코드의 updateItemCount 함수 내에서, orderCounts라는 상태는 optionType별로 각각의 객체가 존재한다. 이때, 직접 orderCount[optionType]에 접근하여 값을 수정하면 불변성을 유지하지 못한다. 따라서, 새로운 객체인 newOrderMap을 생성하여 itemName이라는 key를 가지고 있는 요소의 valueparseInt(newItemCount) 형태로 설정해주었다.

이후, orderCounts 상태의 불변성 역시 유지되어야하므로, 전개 연산자를 이용하여 기존 상태를 얕은 복사(Shallow Copy) 해준 후, 새로 생성된 newOrderCounts내의 optionType에 해당하는 객체를 위에서 생성한 newOrderMap으로 설정하고, setOrderCounts 함수로 상태를 update한다.

추가로, 의존성 배열과 useMemo를 사용하여 orderCountstotal값이 변경될 때만 콜백함수를 다시 실행하도록 하여 불필요한 렌더링을 방지해주었다.

useEffect를 활용한 종속성 부여

// orderCounts가 업데이트 될 때마다 totals도 update
// -> useEffect 사용
const [totals, setTotals] = useState({
  products: 0,
  options: 0,
  total: 0,
});

useEffect(() => {
  const productsTotal = calculateSubtotal('products', orderCounts);
  const optionsTotal = calculateSubtotal('options', orderCounts);
  const total = productsTotal + optionsTotal;
  setTotals({
    products: productsTotal,
    options: optionsTotal,
    total,
  });
}, [orderCounts]);

해당 컴포넌트에서는 orderCounts가 업데이트 될 떄마다 totals라는 상태가 자동으로 업데이트되는 기능이 필요했다. 따라서 useEffect를 활용하여 의존성 배열에 orderCounts를 추가하여 그 값이 변할 때마다 setTotals 함수로 상태를 update해주었다.

for in 문과 for of 문의 차이 및 Array.from()

위에서 사용한 calculateSubtotal 함수를 구현하다가 갑자기 궁금한 점이 생겼다.

function calculateSubtotal(orderType, orderCounts) {
  let optionCount = 0;
  for (const count of orderCounts[orderType].values()) {
    optionCount += count;
  }

  return optionCount * pricePerItem[orderType];
}

calculateSubtotal 함수는 위와 같이 구현하였는데, orderCounts[orderType].values()를 순회할 때 for of문을 사용하였다. “객체의 속성을 순회할 때는 보통 for in문을 사용하는 것으로 알고 있는데, 왜 for of문을 사용했을까?” 라는 의문이 들었다.

그 답은 values() 함수가 무엇을 반환하는지에 있었다. orderCounts[orderType]는 위에서 설명하였듯이 key-value 쌍을 가지고 있는 Map 객체이다. 이때, Map 객체에서 사용할 수 있는 함수 중 하나인 values()는 객체 내의 모든 값들을 나타내는 이터레이터(iterator) 를 반환한다.

따라서, orderCounts[orderType].values()가 반환하는 것은 반복 가능한(iterable) 객체이고, 이를 순회하는 데에는 for of문이 사용되는 것이었다.

추가로, SummaryPage에서는 다음과 같은 형태로 option들의 key값을 가져와서 배열 형태로 저장해주었다.

const optionsArray = Array.from(orderData.options.keys());

위에서 설명한 것처럼, values()keys() 함수는 배열이 아닌 iterable 한 객체를 반환하기 때문에 배열 형태로 저장하기 위해서는 Array.from() 함수를 통하여 배열로 변환해주어야 했다.

step을 사용한 페이지 이동 구현

React에서 페이지의 이동을 구현하는 방법에는 다양한 방법이 존재한다. 오늘은, 일련의 과정이 존재하는 페이지간의 이동에 대하여 step이라는 상태에 변화를 줌으로써 페이지 이동을 구현해보았다.

function App() {
  const [step, setStep] = useState(0);
  return (
    <div style=>
      {step === 0 && <OrderPage setStep={setStep} />}
      {step === 1 && <SummaryPage setStep={setStep} />}
      {step === 2 && <CompletePage setStep={setStep} />}
    </div>
  );
}
<button onClick={() => setStep(1)}>주문</button>

방법은 간단하다. 우선, App 컴포넌트에서 step 상태를 0으로 초기화해주었다. step이 0(초깃값)일 때는 OrderPage가 렌더링되고, props로 setStep 함수를 내려주어 특정 버튼을 클릭하면 다음 step으로 상태가 바뀌고, 자동으로 해당 step에 해당하는 페이지가 렌더링되도록 구현하였다.

마무리

React에서의 상태 관리 방법 중 하나인 useContext를 통하여 간단한 상태관리 앱을 만들어 보았다. 처음이라 그런지 어렵다고 느껴졌지만 열심히 공부하고 실습해보며 얼른 상태 관리에 익숙해져야겠다 🙌🙌

카테고리:

업데이트: