[React] Context API & useReducer

J.A.Y·2024년 7월 25일
0

Prop drilling과 Context

useContext에 관해 이해하기 앞서 context가 무엇인지부터 알아보겠습니다.

Context란?

리액트 애플리케이션은 부모 컴포넌트와 자식 컴포넌트로 이루어져 있습니다. 이에, 부모 컴포넌트에서 사용하고 있는 데이터를 자식 컴포넌트에서도 사용하려면 props로 넘겨줘야 합니다.

<Parent >
    <Children props={date}/>
</Parent>

그런데 부모 컴포넌트와 데이터를 전달받는 자식 컴포넌트 사이의 거리가 멀면 props를 그 거리만큼 계속해서 전달해줘야 합니다. 마치 아래의 예제처럼 말이죠. 이러한 기법을 prop 내려주기 (=prop dirlling)이라고 합니다.

<Parent >
    <ChildrenA props={date}>
        <ChildrenB props={date}>
            <ChildrenC props={date}>
                <ChildrenD props={date}>
                    <ChildrenE props={date}/>
                </ ChildrenD>
            </ ChildrenC>
        </ ChildrenB>
    </ChildrenA>
<Parent/>

prop drilling은 리액트 애플리케이션에 그다지 좋은 영향을 주진 않는데요, 그 이유는 해당 데이터를 사용하지 않는 컴포넌트에서도 오로지 값을 전달하기 위해 props를 받아야 하고, 이를 받는 컴포넌트에서도 props가 제대로 전달되었는지 확인해야 하는 등 번거로운 작업을 해줘야하기 때문입니다. 이런 prop drilling을 극복하기 위해 등장한 개념이 context입니다.

context를 사용하면 명시적으로 props를 전달해주지 않아도 자식 컴포넌트 모두가 부모의 데이터를 자유자재로 사용할 수 있게 됩니다. useContext는 이러한 context를 함수 컴포넌트에서 사용할 수 있도록 해주는 React 훅입니다.

useContext 사용 방법

1. Context 생성 및 초기값 설정

createContext로 컨텍스트를 생성한 후 초기값을 설정해줍니다. 그런 뒤, 하위 컴포넌트 모든 곳에서 값을 참조할 수 있도록 <Context.Provider /> 컴포넌트로 하위 컴포넌트들을 감싸주고 value를 사용해 값을 전달합니다.

import React, { createContext, useState, ReactNode } from 'react';

export const CartContext = createContext<any>(null);

const App = () => {
  const [cart, setCart] = useState<string[]>([]);

  const addToCart = (item: string) => {
    setCart((prevCart) => [...prevCart, item]);
  };

  const value = {
    cart,
    addToCart,
  };

  return (
    <CartContext.Provider value={value}>
      <div>
        <Header />
        <ProductList />
        <Cart />
      </div>
    </CartContext.Provider>
  );
};

export default App;

2. 하위컴포넌트에서 useContext로 데이터 가져오기

이제 useContext를 이용해서 <Context.Provider/>에서 제공하는 상태 값을 가져와서 사용해주면 됩니다. 확실히 useContext를 사용해주니 전보다 코드가 굉장히 깔끔해졌죠?

import React, { useContext } from 'react';
import { CartContext } from './App';

const Cart = () => {
  const { cart } = useContext(CartContext);

  return (
    <div>
      <h2>Shopping Cart</h2>
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default Cart;
import React, { useContext } from 'react';
import { CartContext } from './App';

const ProductList = () => {
  const { addToCart } = useContext(CartContext);

  const products = ['Apple', 'Banana', 'Orange', 'Mango'];

  return (
    <div>
      <h2>Products</h2>
      <ul>
        {products.map((product) => (
          <li key={product}>
            {product}
            <button onClick={() => addToCart(product)}>Add to Cart</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default ProductList;

useContext 사용시 주의점

그러나, useContext를 사용할 때 몇 가지 주의해야 할 점이 있습니다.

1. Provider에 의존하는 관계로 재사용하기 까다로움

일반적으로 useContext를 사용하게 되면 해당 컴포넌트는 Provider의 하위에 있어야 합니다. 이는 즉, useContext를 내부에서 사용하고 있는 함수 컴포넌트는 Provider에 의존하고 있는 관계로 일반 함수 컴포넌트에 비해 재사용하기 어렵다고 볼 수 있습니다. 그렇다고 모든 컨텍스트를 프로젝트 루트 컴포넌트에 등록해버리면 필요치 않은 리소스 낭비가 발생할 수 있기 때문에 가능하면 좁은 범위에서 꼭 필요할 때 사용하는 것이 좋습니다.

2. 불필요한 리렌더링 발생 (Bad 프랙티스)

import React from 'react';

const Header = () => {
  console.log('Header rendered');
  return (
    <header>
      <h1>아무개님의 장바구니</h1>
    </header>
  );
};

export default Header;

App.tsx에서 Context를 생성하여 실행시켜보면, useContext를 사용하고 있지 않는 <Header/> 컴포넌트도 리렌더링되는 것을 볼 수 있습니다. Context를 컴포넌트 내부에서 생성하면, 해당 컴포넌트가 리렌더링될 때마다 새로운 Context가 생성됨에 따라 새로운 참조 값이 생성되어 자식 컴포넌트들도 리렌더링 되기 때문입니다. 그래서 <Header/> 컴포넌트를 React.memo로 메모이제이션해주거나, Context를 외부에서 생성하여 불필요한 리렌더링을 막아야 합니다. 외부에서 생성해주면 동일한 참조 값을 공유해 Context가 변경될 때 useContext를 사용하는 컴포넌트만 리렌더링시킬 수 있습니다.

useReducer

useReduceruseState와 비슷하나, 좀 더 복잡한 상태값을 사전에 정의해놓고 특정 시나리오(=action)에 따라 관리할 수 있습니다.

const [state, dispatcher] = useReducer(..)

useReducer는 두 개의 요소를 배열에 담아 반환하는데, 첫 번째는 useReducer가 가지고 있는 값(=state)이고, 두 번째는 state를 업데이트 하는 함수dispatcher입니다. 여기서 dispatcher는 state를 변경할 수 있는 action을 넘겨줍니다.

const [.., ..] = useReducer(reducer, initialState, init)

useReducer의 인수로는 최소 2개에서 3개가 필요합니다. 첫 번째는 useReducer의 기본 action을 정의한 함수reducer이고, 두 번째 인수는 userReducer의 초깃값initialState입니다. 마지막 인수는 선택인데요, 초깃값을 지연해서 생성시키고 싶을 때 사용하는 함수init입니다.

useReducer 사용법

useContext 예시에서 useState로 상태를 업데이트해줬던 부분을 useReducer를 사용해서 변경해보겠습니다.

1. 초기값 생성 & 시나리오 작성하기

interface CartItem {
  id: string;
  name: string;
}

interface State {
  cart: CartItem[];
}

type Action =
  | { type: 'ADD_ITEM'; payload: CartItem }
  | { type: 'REMOVE_ITEM'; payload: string };

const initialState: State = {
  cart: [],
};

const cartReducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'ADD_ITEM':
      return { cart: [...state.cart, action.payload] };
    case 'REMOVE_ITEM':
      return { cart: state.cart.filter((item) => item.id !== action.payload) };
    default:
      return state;
  }
};

2. useReducer를 사용하여 상태 관리하기

기존에 useState로 관리했던 상태와 업데이트 함수를 useReducer로 변경하고, 해당 상태와 업데이트 함수인 dispatcher를 context로 넘겨 모든 자식 컴포넌트에서 참조할 수 있도록 해줍니다.

export const CartContext = createContext<
  { state: State; dispatch: React.Dispatch<React.SetStateAction> } | undefined
>(undefined);

export const CartProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = useReducer(cartReducer, initialState);

  return (
    <CartContext.Provider value={{ state, dispatch }}>
      {children}
    </CartContext.Provider>
  );
};

3. useContext를 사용해 디스패치 함수 받아오기

'담기' 버튼을 클릭하면 'ADD_ITEM' 액션이 디스패치되는데, 이때 이 예제처럼 payload를 사용해서 액션에 추가적인 값을 더해줄 수도 있습니다.

const products = [
  { id: '1', name: '바나나' },
  { id: '2', name: '파인애플' },
  { id: '3', name: '오렌지' },
];

const ProductList: React.FC = () => {
  const { dispatch } = useContext(CartContext);

  const handleAddToCart = (product: (typeof products)[0]) => {
    dispatch({ type: 'ADD_ITEM', payload: product });
  };

  return (
    <div>
      <h2>과일 목록</h2>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            <p>{product.name}</p>
            <button onClick={() => handleAddToCart(product)}>담기</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default ProductList;

정리

최종 예제 코드 & Preview

이렇게 Context API와 useReducer에 대해 알아보았는데요, 두 기술을 접목해서 잘 활용하면 Redux처럼 (실제로 Redux 또한 useReducer를 기반으로 한다고 하죠.) 중앙 집중형 상태 관리로 더욱 쉽게 상태를 이용하고 변경할 수 있는 것 같습니다. 물론, 렌더링 최적화가 잘 이루어지고 있는지 적용하면서 틈틈이 확인하는 작업도 해야하겠지만요🙂

profile
Done is better than perfect🏃‍♀️

0개의 댓글