📎 리액트 Context API 공식문서
context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다.
리액트에서 제공하는 built-in API로 State 관리를 외부 라이브러리 없이 할 수 있다.
또한 리액트에서 Context API를 위해 훅스도 제공한다.
State Management 라이브러리는 리덕스, Mobx, recoil.js 등이 있다.
개인적인 의견이지만 Context API는 리덕스와 비슷하지만 좀 더 이해하기 쉽고 알아야 하는 것도 상대적으로 적기 때문에 이것만으로도 꽤나 장점이 있다. 그렇지만 리덕스에서의 강력한 미들웨어 기능은 없다. 각각의 장단점이 있기에 상황에 맞게 잘 써야할 것 같다.
일단 리덕스에서의 store와 같은 하나의 Context 객체가 필요하다.
import React, {createContext} from 'react';
const MyContext = createContext();
export default MyContext;
createContext 를 실행하면 Provider와 Consumer을 담고 있는 컨텍스트 객체가 생성된다.
Provider는 state나 action.type에 따른 dispatch 함수들을 value prop에 넣어서 제공하는 역할.
Consumer는 Provider에 담긴 state와 dispatch 함수들을 필요한 컴포넌트에서 접근할 수 있게 만드는 역할.
위 처럼 context 객체를 생성하고 export 하였다면 Provider를 생성 할 수 있다.
Provider는 context의 뿌리라고 할 수 있다. 필요한 모든 것을 담고 있고,
Consumer로 wrpping된 컴포넌트는 Provider에 접근 할 수 있다.
Provider를 리덕스처럼 자식 컴포넌트들을 wrapping 한다.
여기서 약간의 차이점이라고 한다면 리덕스는 하나의 store를 사용하는 것이 기본적인 룰인데 반해, Context API는 다수의 Context를 만들 수 있다.
import { createContext, useReducer } from "react";
import { initialState } from "../assets/state";
import { cartReducer } from "./reducer/cartReducer";
import { checkReducer } from "./reducer/checkReducer";
export const MarketContext = createContext();
export const MarketContextProvider = ({ children }) => {
const [cartState, cartDispatch] = useReducer(
cartReducer,
initialState.cartItems
);
const [checkState, checkDispatch] = useReducer(
checkReducer,
cartState.map((el) => el.itemId)
);
return (
<MarketContext.Provider
value={{
items: initialState.items,
cart: { cartState, cartDispatch },
check: { checkState, checkDispatch },
}}
>
{children}
</MarketContext.Provider>
);
};
리액트의 빌트인 훅인 useContext를 통하여 컴포넌트는 간단하게 자신을 wrapping 하고 있는 Provider의 value에 접근 가능하다.
위의 코드에서는 itmes, cart객체, check객체 (cart, check 객체는 각각 state와 dispatch 액션을 담고 있다.)
import React, { useContext } from "react";
import Item from "../components/Item";
import { addItem } from "../context/action/cartAction";
import { addCheck } from "../context/action/checkAction";
import { MarketContext } from "../context/index";
function ItemListContainer() {
const { items, cart, check } = useContext(MarketContext);
const { cartDispatch, cartState } = cart;
const { checkDispatch } = check;
...
return (
<div id="item-list-container">
<div id="item-list-body">
<div id="item-list-title">쓸모없는 선물 모음</div>
{items.map((item, idx) => (
<Item
item={item}
key={idx}
handleClick={() => handleClick(item.id)}
/>
))}
</div>
</div>
);
}
export default ItemListContainer;
useContext 훅은 하나의 인자를 받는다.
그건 바로 createContext를 사용하고 리턴받은 객체이다.
이렇게 해서 ItemListContainer 는 state와 dispatch 함수에 직접 접근을 할 수 있다.
Context API는 렌더링에 있어 다소 아쉬운 점이 있다.
Provider의 value prop에 있는 state와 dispatch가 변할 때 마다, Provider를 구독하고 있는 모든 컴포넌트들이 리렌더링이 된다.
useMemo를 통해 Provider의 value props를 메모이제이션 하거나,
독립적인 context를 만들어주는 방법이 있다.
Context는 useReducer hook과 함께 사용한다면 더욱 직관적이며 코드의 양도 눈에 띄게 준다.
리덕스로 상태관리를 하라는 문제였지만 Context로 한번 시도해봤고 그 뒤 리덕스로 다시 리팩토링 하였다.
앱이 복잡하지 않다면 context와 useReducer 훅으로 상태관리를 하는 것이 조금 더 편할 것 같다.
const cartReducer = (state, { type, id, quantity }) => {
switch (type) {
case ADD_ITEM:
return [...state, { itemId: id, quantity: 1 }];
case INCRE_QUANTITY:
return state.map((item) =>
item.itemId === id ? { ...item, quantity: item.quantity + 1 } : item
);
case DELETE_ITEM:
return state.filter((item) => item.itemId !== id);
case CHANGE_QUANTITY:
return state.map((item) =>
item.itemId === id ? { ...item, quantity } : item
);
default:
return state;
}
};
export const MarketContextProvider = ({ children }) => {
const [cartState, cartDispatch] = useReducer(
cartReducer,
initialState.cartItems
);
const [checkState, checkDispatch] = useReducer(
checkReducer,
cartState.map((el) => el.itemId)
);
...