useContext에 대해 이해하려면 먼저 리액트의 Context에 대해 알아야 합니다. Context부터 먼저 살펴보고 useContext로 넘어가겠습니다.
Context란?
리액트는 기본적으로 부모 컴포넌트와 자식 컴포넌트로 이뤄진 트리 구조를 갖고 있기 때문에 부모가 가지고 있는 데이터를 자식에서도 사용하고 싶다면 props로 데이터를 넘겨주는 것이 일반적입니다. 그러나 전달해야 하는 데이터가 있는 컴포넌트와 전달받아야 하는 컴포넌트의 거리가 멀어질수록 코드는 복잡해집니다.
<A props={somthing}>
<B props={somthing}>
<C props={somthing}>
<D props={somthing}/>
</C>
</B>
</A>
A컴포넌트에서 제공하는 데이터를 D컴포넌트에서 사용하려면 props를 하위 컴포넌트로 필요한 위치까지 계속해서 넘겨야 합니다. 이러한 기법을 props drilling이라고 합니다.
이러한 props drilling을 극복하기 위해 등장한 개념이 바로 Context입니다. Context를 사용하면, 이러한 명시적인 props 전달 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있습니다. 이제 Context의 용도를 살펴봤으니 Context를 실제로 선언하는 방법과 useContext에 대해 살펴봅시다.
useContext 사용법
const { createContext } = require("vm");
const Context = createContext<{ hello: string }>(undefined)
function ParentComponent() {
return (
<>
<Context.Provider value={{hello: '안녕하세요'}}>
<Context.Provider value={{hello: '코딩병원'}}>
<ChildComponent />
</Context.Provider>
</Context.Provider>
</>
)
}
function ChildComponent() {
const value = useContext(Context);
return <>{value ? value.hello : ''}</>
}
useContext는 상위 컴포넌트에서 만들어진 Context를 함수 컴포넌트에서 사용할 수 있도록 만들어진 훅입니다. useContext를 사용하면 상위 컴포넌트 어딘가에서 선언된 `<Context.Provider />`에서 제공한 값을 사용할 수 있게 됩니다. 만약 여러 개의 Provider가 있다면 가장 가까운 Provider의 값을 가져오게 됩니다. 따라서, 예제어는 가까운 Context의 값인 코딩병원이 반환되었습니다.
ntext와 해당 Context를 함수 컴포넌트에서 사용할 수 있게 해주는 useContext는 다음과 같이 사용할 수 있습니다.
컴포넌트 트리가 복잡해질수록 Context를 사용하는 것도 만만치 않을 것입니다. useContext로 원하는 값을 얻으려고 했지만 정작 컴포넌트가 실행될 때 인 Context가 존재하지 않아 예상치 못하게 에러가 발생한 경험이 종종 있을 것입니다. 이러한 에러를 방지하려면 useContext 내부에서 해당 Context가 존재하는지, 즉 Context가 한 번이라도 초기화되어 값을 내려주고 있는지 확인해보면 됩니다.
function ParentComponent({
children,
text
}: {
children: React.ReactNode,
text: string
}) {
return (
<Context.Provider value={{hello: '코딩병원'}}></Context.Provider>
)
}
function useMyContext() {
const context = useContext(Context);
if (context === undefined) {
throw new Error('useMyContext는 ParentComponent 내부에서만 사용할 수 있습니다.');
}
return context;
}
function ChildComponent() {
// 타입이 명확히 설정돼 있어서 굳이 undefined 체크를 하지 않아도 됩니다.
// 이 컴포넌트가 Provider 하위에 없다면 에러가 발생할 것입니다.
const { hello } = useMyContext();
return <>{hello}</>
}
다수의 Provider와 useContext를 사용할 때, 특히 타입스크립트를 사용하고 있다면 위와 같이 별도 함수로 감싸서 사용하는 것이 좋습니다. 타입 추론에도 유용하고, 상위에 Provider가 없는 경우에도 사전에 쉽게 에러를 찾을 수 있습니다.
useContext를 사용할 때 주의할 점
useContext를 함수 컴포넌트 내부에서 사용할 때는 항상 컴포넌트 재활용이 어려워진다는 점을 염두에 둬야합니다. useContext가 선언돼 있으면 Provider에 의존성을 가지고 있는 셈이 되므로 아무데서나 재활용하기에는 어려운 컴포넌트가 됩니다. 해당 함수 컴포넌트가 Provider 하위에 있지 않은 상태로 useContext를 사용한다면 예기치 못한 작동 방식이 만들어집니다. 즉, useContext가 있는 컴포넌트는 그 순간부터 눈으로는 직접 보이지도 않을 수 있는 Provider와의 의존성을 갖게 되는 셈입니다.
이러한 상황을 방지하려면 useContext를 사용하는 컴포넌트를 최대한 작게 하거나 혹은 재사용되지 않을 만한 컴포넌트에서 사용되어야 합니다. 이러한 문제를 방지하기 위해 모든 Context를 최상위 루트 컴포넌트에 넣는 것은 어떨까요?
앞서 언급한 에러는 줄일 수 있지만 리액트 관점에서는 그다지 현명한 접근법은 아닙니다. Context가 많아질수록 루트 컴포넌트는 더 많은 Context로 둘러싸일 것이고 해당 Props를 다수의 컴포넌트에서 사용할 수 있게끔 해야 하므로 불필요하게 리소스가 낭비됩니다. 따라서 Context가 미치는 범위는 필요한 환경에서 최대한 좁게 만들어야합니다.
마지막으로 일부 리액트 개발자들이 Context와 useContext를 상태 관리를 위한 리액트의 API로 오해하고있다는 것입니다. 엄밀히 따지면 Context는 상태를 주입해 주는 API입니다. 상태 관리 라이브러리가 되기 위해서는 최소한 다음 두 가지 조건을 만족해야 합니다.
- 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 합니다.
- 필요에 따라 이러한 상태 변화를 최적화할 수 있어야 합니다.
그러나 Context는 둘 중 어느것도 하지 못합니다. 단순히 Props의 값을 하위로 전달해 줄 뿐, useContext를 사용한다고 해서 렌더링이 최적화되지는 않습니다.
예제
react Hooks의 useContext를 사용하기 위해서는 react에서 createContext와 useContext를 import 받아야 합니다.
import { createContext, useContext } from 'react';
react hooks의 useContext를 사용하기 위해 AgeContext에 초기값 null을 설정합니다.
import { createContext } from 'react';
export const AgeContext = createContext(null);
AgeContext와 마찬가지로 초기값을 null로 설정하였습니다.
import { createContext } from 'react';
export const NameContext = createContext(null);
AgeContext.Provider, NameContext.Provider를 이용하여 트리 안에 포함된 컴포넌트(Header)에 28과 홍길동을 전달하였습니다.
import { AgeContext } from './Context/AgeContext';
import { NameContext } from './Context/NameContext';
import Header from 'Header';
function App() {
return (
<AgeContext.Provider value="28">
<NameContext.Provider value="홍길동">
<div className="App">
<Header/>
</div>
</NameContext.Provider>
</AgeContext.Provider>
);
}
export default App;
useContext의 인수로 AgeContext와 NameContext를 받아 age, user 변수에 저장해주었습니다. useContext로 전달한 인자는 context 객체 그 자체여야 합니다.
import { useContext } from 'react';
import { AgeContext } from './Context/AgeContext';
import { NameContext } from './Context/NameContext';
import './App.css';
const Header = () => {
const age = useContext(AgeContext);
const user = useContext(NameContext);
console.log('user -----------> ', user);
console.log('age -----------> ', age);
return (
<header className="App-header">
<p>환영합니다. {user}님의 나이는 {age}입니다. </p>
</header>
)
}
export default Header;
실행 화면
Reference
React Deep Dive
'1. 웹개발 > 1_1_5 React JS' 카테고리의 다른 글
[React] 로컬에 있는 json으로 url을 설정하는 방법 (0) | 2022.05.15 |
---|---|
[React] useMemo 사용법 및 예제 (0) | 2022.04.06 |
[React] useRef 사용법 및 예제 (2) | 2022.03.27 |
[React] useEffect란? (0) | 2022.03.25 |
[React] useState란? (0) | 2022.03.18 |