치춘짱베리굿나이스
React의 useContext 본문
useContext
개요
이번에 쪼끄만 자기소개 페이지를 SSR과 SSG를 섞어서 만들어보고 있는데 아무래도 외부 라이브러리를 가져다가 쓸 만큼 프로젝트 규모가 크지 않다 보니… 라이브러리 설치가 조금 꺼려지는 상황에서 상태값을 자식의 자식의 자식한테 내려줘야 하는 일이 생겼다
3~4 depth 정도이니 아예 Prop Drilling 으로 처리할까 했는데, 그보다 먼저 useContext
를 이용하여 상태값의 활동 범위를 넓혀줄 수 있겠다는 생각이 들어서 여태껏 한번도 써 보지 않은 useContext
를 써 보기로 했다
useContext란?
각 컴포넌트에서 context
를 읽고 구독할 수 있도록 도와주는 React Hook 이라고 한다
쉽게 말해 어떠한 상태값을 context
형태로 감싼 뒤, 이를 다른 컴포넌트가 구독하게끔 하여 상태값의 변화를 감지하고 사용할 수 있게끔 도와주는 것이다
전역 상태관리 라이브러리와 역할이 비슷하다고 보면 된다
useContext
훅 단독으로 사용되진 않고, createContext
와 함께 사용된다
createContext
: 편지봉투를 만드는 공장context
: 편지봉투context.Provider
: 집배원의 활동 반경을 정의하는 역할useContext
: 도착한 편지 개봉하는 역할
맞는 비유인진 잘 모르겠지만 이렇게 생각하면 조금 쉽진 않을까…
사용 방법
context 생성
export const TextContext = createContext('');
다른 함수가 구독할 수 있는 context
를 하나 생성해 보자
createContext
는 인자로 context
의 기본값을 받고, context
객체를 반환한다
만약 기본값으로 설정하고픈 값이 딱히 없을 경우, null
로 설정하면 된다
주의할 점은, context
객체 자체는 값을 갖고 있지 않고, 컴포넌트가 어떤 값을 읽거나 구독해야 하는지 알려주는 객체일 뿐이다
뒤에서 사용할 <context 객체>.Provider
를 이용하여 context
가 가리킬 상태값을 정의하고, useContext
로 읽어들이도록 할 것이다
context 를 다른 컴포넌트에 제공
const App = () => {
const [text, setText] = useState('');
return (
<TextContext.Provider value={text}>
<Page />
</TextContext>
);
};
context
값을 다른 컴포넌트에 전달할 때는 Provider
로 컴포넌트를 감싼다
여러 컴포넌트가 context
를 구독하게끔 하고 싶다면 상위 컴포넌트를 감싸거나, 한번에 여러 컴포넌트를 Fragment
를 이용하여 감싸주면 된다
위의 text
처럼 상위 컴포넌트에서 context
로 사용할 상태값을 정의하고 value
로 상태값을 넘겨주면 하위 컴포넌트는 이 context
를 구독함으로서 안에 있는 상태값을 꺼내 사용할 수 있다
제공받은 context 읽어들이기
const Title = () => {
const text = useContext(TextContext);
return <h1>{text}</h1>;
};
자식 컴포넌트에서 context
를 읽어들이기 위해서는 useContext
를 호출한다
useContext
는 context
에서 상태값을 읽어들이며, 읽은 상태값을 반환한다
useContext
는 조상 컴포넌트 중 가장 가까이 있는 <context 객체>.Provider
를 찾아 그 value
를 가져오는데, 만약 적절한 Provider
가 없을 경우 createContext
함수에 인자로 넘겼던 기본값 (default value) 을 사용한다
이 <context 객체>.Provider
는 반드시 useContext
를 호출하는 컴포넌트의 상위에 존재해야 한다
useContext의 문제점
const Test = () => {
return (
<TestProvider>
<>
<TestComponent2 />
<TestComponent3 />
</>
</TestProvider>
);
};
const TestProvider = ({ children }: { children: JSX.Element }) => {
const [text, setText] = useState("");
return (
<TestContext.Provider value={{ text, setText }}>
{children}
</TestContext.Provider>
);
};
const TestComponent1 = () => {
const { text, setText } = useContext(TestContext);
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
},
[setText]
);
return (
<div>
{text} in TestComponent1
<input value={text} onChange={handleChange} />
</div>
);
};
const TestComponent2 = () => {
console.log("TestComponent2 Rerendered");
return <TestComponent1 />;
};
const TestComponent3 = () => {
useContext(TestContext);
console.log("TestComponent3 Rerendered");
return <div>This is TestComponent3</div>;
};
이런 코드가 있다고 해 보자
TestComponent1
에서는 text
조작과 출력을 모두 하므로, text
가 변경될 때마다 리렌더링이 발생한다
TestComponent2
는 TestComponent1
의 부모일 뿐, text
와 setText
를 사용하지 않는다
부모 컴포넌트가 리렌더링될 때 자식 컴포넌트가 리렌더링되는 건 맞지만, 자식 컴포넌트가 리렌더링된다고 해서 부모 컴포넌트가 리렌더링 되진 않는다
또한 TestComponent
3은 useContext
를 호출만 할 뿐, 아무런 값도 사용하지 않으므로 (리렌더링에 관여하는 상태값이 아예 없으므로) 리렌더링이 발생해서는 안 된다
따라서 두 컴포넌트 모두 text
상태값이 변경되어도 리렌더링이 되지 않아야만 할 것 같다
타자를 칠 때마다 TestComponent3
이 리렌더링된다 (ㅋㅋ)
Provider
아래에서 useContext
를 구독하는 모든 컴포넌트들은 Provider
의 value
가 변경될 때마다 자동으로 리렌더링된다고 한다
TestComponent3
은 리렌더링 될 이유가 없는데 리렌더링이 일어나고 있는 것이다 (????)
컴포넌트의 리렌더링 조건
컴포넌트는 다음과 같은 조건에서 리렌더링 된다
- 컴포넌트 내부의 상태값, 또는 외부에서 받는 Props에 변화가 있을 때
- 부모가 리렌더링되었을 때
context
구독 중에 (useContext 훅 호출됨),context
에 변화가 발생하였을 때- Force Update (클래스 컴포넌트 시절에 있던 메서드)
해결법
...
const TestComponent2 = React.memo(() => {
console.log('TestComponent2 Rerendered');
return <TestComponent1 />;
});
const TestComponent3 = React.memo(() => {
console.log('TestComponent3 Rerendered');
return <div>This is TestComponent3</div>;
});
위와 같이 TestComponent3
을 메모이제이션 하면 더이상TestComponent3
에서는 리렌더링이 발생하지 않기는 한다
하지만 위처럼 간단한 상황이 아니라 엄청 많은 컴포넌트가 엮어져 있는 상황이라면 모든 컴포넌트에 React.memo
를 붙여줘야 할까…?
return (
<>
<TestContext.Provider value={{ text }}>
<TestComponent1 />
</TestContext.Provider>
<SetTestContext.Provider value={{ setText }}>
<TestComponent3 />
</SetTestContext.Provider>
</>
);
또 하나의 방법으로는 상태값을 분리해서 Provider
를 여러 개 분리하고, context
가 필요한 컴포넌트 트리에만 Provider
로 감싸주는 방법이 있으나, 이는 상태값 구조가 복잡해질 수록 오히려 골치아파질 수도 있다
언제 useContext를 사용할까?
내가 생각하기에 useContext를 사용하기 적합한 타이밍은 다음과 같다 (주관적)
- Props Drilling은 일어나지만,
useContext
에 의한 리렌더링이 발생해도 다른 컴포넌트에 영향이 적을 정도로 간단한 구조일 때 (Depth가 비교적 얕을 때) - 프로젝트 전체에 전역 상태관리를 할만한 값이 그렇게 많지 않을 때
아무래도 프로젝트가 커지면 커질 수록 context
하나에 영향받는 컴포넌트가 늘어날 테니 그럴 때는 Redux나 Recoil을 찾아가는 게 낫지 않을까 하는 생각이 든다…
참고 자료
https://yrnana.dev/post/2021-08-21-context-api-redux/
https://leewarrick.com/blog/the-problem-with-context/
'ClientSide > React' 카테고리의 다른 글
useMemo, useCallback (0) | 2023.07.24 |
---|---|
마운트와 렌더링 (0) | 2023.07.24 |
Suspense와 Error Boundary를 이용한 로딩과 예외처리 (0) | 2022.12.10 |
React를 클론코딩 #1 가상 돔 (0) | 2022.10.05 |
React (0) | 2022.10.01 |