React를 공부하고 스스로 프로젝트를 만들다 보면 점점 규모가 커지고 컴포넌트 구조가 복잡해지면서 관리해야 할 state 값이 많아질 것이다. 이쯤 우리는 상태 관리를 어떻게 해야 할까, 구조에 대해 고민에 빠지게 되는 것 같다.
함수형 컴포넌트 로컬 상태만 관리할 때는 우리는 useState 훅을 사용할 것이다.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Counter;
다만 프로젝트 규모가 커졌을 때, 우리는 전역 상태관리를 어떻게 해야 할까 고민을 하게 되는데 아마 서칭 해봤을 때 두 가지 방법이 나올 것이다.
1. Context
2. 상태 관리 라이브러리 ex. Redux, Recoil, MobX, Zustand 등등
그럼 우리는 이 2가지 안에서 하나를 선택해야 하는 순간이 온다.
오늘은 Context를 사용해보며, 나의 프로젝트에 맞는 상태관리인지 생각해보려고 한다.
예시로, 지금 나는
1. 상품 리스트 페이지를 만들고, 상품 리스트 목록을 호출한다.
2. 상품 아이템 컴포넌트를 만들고, '좋아요' 버튼이 있다. 좋아요 버튼을 클릭하면 상품의 좋아요 버튼이 활성화된다.
와 같은 프로세스를 가진 로직을 구현하고자 한다.
Context로 어떻게 구현이 가능할까
React 공식 문서에 사용된 위 그림과 같이 Context를 사용하면 명시적으로 props를 전달해주지 않아도 부모 컴포넌트가 트리에 있는 어떤 자식 컴포넌트에서나 (얼마나 깊게 있든지 간에) 정보를 사용할 수 있습니다.
아래 product-context.js 코드는 상품 관련 Context 생성과 Provider(제공자) 컴포넌트 코드이다.
Context을 생성할 때 createContext의 유일한 인자는 기본값이다. 여기서 나는 product 란 빈배열을 가진 객체를 기본값으로 생성했다.
ProductsContext.Provider를 사용하여 하위 컴포넌트들에게 상태를 제공할 수 있고, 여기서는 productsList란 상태를 정의해 보았다.
즉, value 속성은 productsList를 포함하는 객체로 설정하였고, ProductsContext.Provider 내부에 포함된 모든 자식 컴포넌트는 productsList에 접근할 수 있게 된다.
// ./context/product-context.js
import { createContext, useState } from 'react';
export const ProductsContext = createContext({
product: []
});
const ProductsProvider = props => {
const [productsList, setProductsList] = useState([
{
id: 'product1',
title: 'Test Product1',
isFavorite: false
},
{
id: 'product2',
title: 'Test Product2',
isFavorite: false
},
...
]);
return (
<ProductsContext.Provider value={{ products: productsList }}>
{props.children}
</ProductsContext.Provider>
)
}
export default ProductsProvider;
다음으로 ProductsProvider를 사용하여 컴포넌트 트리를 감싸야한다.
//index.js
import ProductProvider from './context/product-context'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ProductProvider>
<App />
</ProductProvider>
);
마지막으로 Context를 사용하면 된다.
useContext 훅을 사용해서 ProductsContext에 접근하여 products 값을 불러오는 것이다.
import { useContext } from 'react';
import { ProductsContext } from '../context/product-context';
import { Item } from '../components/Products/ProductItem';
function ProductList() {
const { products } = useContext(ProductsContext);
return (
<div>
<h1>Product List</h1>
<ul>
{products.map(product => (
<ProductItem
key={product.id}
id={product.id}
title={product.title}
isFavorite={product.isFavorite}
/>
))}
</ul>
</div>
);
}
export default ProductList;
그럼 지금까지 Context를 사용해서 1번을 만들어 보았다.
1. 상품 리스트 페이지를 만들고, 상품 리스트 목록을 호출한다.
2. 상품 아이템 컴포넌트를 만들고, '좋아요' 버튼이 있다. 좋아요 버튼을 클릭하면 상품의 좋아요 버튼이 활성화된다.
2번 상품 아이템 컴포넌트는 아래와 같이 만들 수 있겠다.
const ProductItem = props => {
const toggleFavoriteHandler = () => {
console.log("좋아요 버튼 테스트", props.isFavorite);
}
return (
<div class="product-item">
<h2>{props.title}</h2>
<button onClick={toggleFavoriteHandler}>
{props.isFavorite ? "좋아요 아이콘 비활성" : "좋아요 아이콘 활성"}
</button>
</div>
);
};
export default ProductItem;
좋아요 버튼에 대한 클릭 함수도 전역으로 처리가 되어야 isFavorite 상태값이 변경이 될 것이다.
그럼 Context 파일에 추가를 해보자.
// ./context/product-context.js
import { createContext, useState } from 'react';
export const ProductsContext = createContext({
product: []
toggleFavorite: (id) => {}
});
const ProductsProvider = props => {
const [productsList, setProductsList] = useState([
{
id: 'product1',
title: 'Test Product1',
isFavorite: false
},
{
id: 'product2',
title: 'Test Product2',
isFavorite: false
},
...
]);
const toggleFavoriteHandler = productId => {
setProductsList(currentProductsList => {
const productIndex = currentProductsList.findIndex(product => product.id === productId);
const newFavoriteStatus = !currentProductsList[productIndex].isFavorite;
const updatedProducts = [...currentProductsList];
updatedProducts[productIndex] = {
...currentProductsList[productIndex],
isFavorite: newFavoriteStatus
};
return updatedProducts;
});
}
return (
<ProductsContext.Provider value={{ products: productsList, toggleFavorite: toggleFavoriteHandler }}>
{props.children}
</ProductsContext.Provider>
)
}
export default ProductsProvider;
ProductContext 기본값에 toggleFavorite 빈 함수를 생성한다.
toggleFavoriteHandler 함수는 상품 아이디를 인자로 받아, 상품 인덱스를 찾고 해당 isFavorite 값을 바꿔준다.
그리고 상품 리스트를 다시 업데이트해서 setProductsList로 상태를 설정한다.
마지막으로 toggleFavoriteHandler 함수를 Provider를 통해 하위 컴포넌트에 전달하도록 한다.
그럼 다시 상품 아이템 컴포넌트로 돌아가서 toggleFavorite를 사용하면 된다.
import { useContext } from 'react';
import { ProductsContext } from '../../context/product-context';
const ProductItem = props => {
const toggleFavorite = useContext(ProductsContext).toggleFavorite
const toggleFavoriteHandler = () => {
toggleFavorite(props.id);
}
return (
<div class="product-item">
<h2>{props.title}</h2>
<button onClick={toggleFavoriteHandler}>
{props.isFavorite ? "좋아요 아이콘 비활성" : "좋아요 아이콘 활성"}
</button>
</div>
);
};
export default ProductItem;
이 프로젝트에서 Context 선택이 맞을까
위에 예시로 상품 리스트와 좋아요 버튼 클릭 로직을 Context를 이용해서 만들어보았는데, 크게 복잡하지 않고 상태 라이브러리를 사용하지 않고도 전역 상태를 관리할 수 있다는 것을 알았다. 그런데 왜 사람들은 Redux, Recoil 등과 같은 상태 관리 라이브러리를 프로젝트에 도입할까?
Context의 단점을 살펴보면,
지금은 트리 구조가 단조롭기 때문에 Context 값이 변경되더라도 성능 문제가 눈에 띄게 나타나지 않을 것이다. 만약 트리 구조가 깊어지기 시작하면 리렌더링 이슈가 생길 수 있다. 값이 변경되면 Context를 구독하고 있는 모든 컴포넌트가 리렌더링 되기 때문이다. 따라서 Context 값이 필요한 부분인지 아닌지를 구분해서 구조를 잘 만들지 않으면 성능 문제가 생길 수 있다.
그리고 지금은 ProductContext 하나만 있지만, 서비스가 여러 개 붙으면서 Context가 다양해질 수 있고 관리하는데 복잡해질 수 있다.
이런 문제로 복잡한 상태 관리를 필요로 하는 프로젝트에는 상태 관리 라이브러리를 추천하는 것 같다.
그럼, Context는 언제 사용하는게 적합한가? 상태 값이 자주 바뀌지 않는 상황에 적합할 수 있다.
예를 들어 다크모드로 테마를 변경하는 부분, 인증 상태 관리, 다국어 언어 지원 설정 관리 등 이 있을 수 있다.
일단 Context 마무리
오늘은 Context에 대해서 알아보고, 구현을 해봤는데 아직까지 Context를 제대로 잘 알고 사용하고 있는 수준이 아니라서 좀 더 상황에 맞게 써봐야겠단 생각이다.
22년도 글이지만 '다른 사람들이 안 알려주는 리액트에서 Context API 잘 쓰는 방법'에 Context를 전역 관리 뿐만 아니라 재사용 컴포넌트를 만들 때 유용하며, 전역 상태 관리를 할 수 있는 수단임을 알려주고 있다. 참고해서 Context를 좀 더 상황에 맞게 사용해 보고 상태 관리 라이브러리에 대해서 정리하는 시간을 가져봐야겠다.
'Develop' 카테고리의 다른 글
'오늘 하루 그만 보기' 정보는 어디에 저장이 될까? (0) | 2024.07.22 |
---|---|
[React + Typescript] SelectBox Component는 어떻게 만드는게 좋을까? (1) | 2024.06.16 |
[Tailwind CSS] React+Vite 환경에 설치, 그런데 적용이 안됨. 왜죠? (0) | 2024.05.22 |
[공공데이터] API 인증키 오류 _ 'SERVICE KEY IS NOT REGISTERED ERROR' (0) | 2024.05.11 |
[React] CRA vs Vite? 왜죠? (0) | 2024.03.15 |