모든 코드 예시는 여기서 찾아볼 수 있습니다.
https://blacksheepcode.com/posts/no_react_context_is_not_causing_too_many_renders
사람들은 React Context가 상태 관리에 적합하지 않다고 흔히 믿습니다. 왜냐하면 상태가 변경될 때마다 React Provider 아래에 있는 모든 요소가 리렌더링되기 때문입니다.
이로 인해 사람들은 Context 사용을 피하고, Redux나 Zustand 같은 도구로 바로 넘어가는 경향이 있습니다.
그것은 잘못된 표현이며, 저는 그것을 반박하고 싶습니다.
여기 애플리케이션이 있습니다.
export function ReactRenders1() {
const [value, setValue] = React.useState("foo");
return <MyProvider>
<button onClick={() => {
setValue(`${Math.random()}`);
}}
className="global-render-button"
> Render all</button>
<div className="render-tracker-demo">
<StateChanger />
<StateDisplayer />
<SomeUnrelatedComponent />
<SomeUnrelatedComponent />
<SomeUnrelatedComponent />
</div>
</MyProvider >
}
// 애플리케이션 상단에 전체 애플리케이션을 다시 렌더링하는 버튼이 있습니다.
// 이는 여기에 속임수가 없음을 보여주기 위한 것입니다.
const MyProvider = ({ children }: { children: React.ReactNode }) => {
const [value, setValue] = React.useState("foo");
const contextValue: MyContextType = { value, setValue };
return <MyContext.Provider value={contextValue}>{children}</MyContext.Provider>;
};
// 제 Context Provider 는 간단합니다. 상태를 useState 훅에 저장하고, 이를 Context Provider 를 통해 제공합니다.
function StateChanger() {
const { setValue } = useContext(MyContext);
return <div className="state-changer">
<strong>State Changer</strong>
<button onClick={() => setValue(`${Math.random()}`)}>Change state</button>
<RenderTracker />
</div >
}
function StateDisplayer() {
const { value } = useContext(MyContext);
return <div className="state-displayer">
<strong>State Displayer</strong>
<div>{value}</div>
<RenderTracker />
</div>
}
// 두 개의 컴포넌트가 있으며, 둘 다 이 컨텍스트를 사용합니다.
function SomeUnrelatedComponent() {
return <div className="some-unrelated-component">
<strong>Some unrelated component</strong>
<RenderTracker />
</div>
}
// 컨텍스트를 사용하지 않는 관련 없는 컴포넌트의 인스턴스가 여러 개 있습니다.
export function RenderTracker() {
let randX = Math.floor(Math.random() * 100);
let randY = Math.floor(Math.random() * 100);
return <div className="render-tracker">
<strong>Render Tracker</strong>
<div className="render-tracking-dot" style={{ top: `${randY}%`, left: `${randX}%` }}>
</div>
</div >
}
// 내 렌더링 추적 컴포넌트는 렌더링할 때마다 점을 다른 위치에 표시합니다.
결과는 어땠을까요?
직접 확인해보세요.
interative demo 가 있습니다. 원문 페이지로 가 직접 확인해보세요.
'Render all' 버튼을 클릭하면 실제로 애플리케이션 전체가 렌더링되는 걸 확인해보세요.
'Change state' 버튼을 클릭하면 컨텍스트를 사용하는 컴포넌트에만 영향을 미친다는 걸 확인해보세요.
제가 생각하기엔 이 혼란은 두 가지에서 비롯된 것 같습니다.
동일한 Context Provider 에 color/setColor, foo/setFoo, bar/setBar 쌍을 추가하고, 이러한 새로운 상태를 사용하는 새 컴포넌트 FooComponent가 있다면, 이들의 상태 변경은 리렌더링을 유발합니다. 이 Context Provider 를 사용하는 모든 컴포넌트는 상태가 변경될 때 마다 리렌더링됩니다.
function FooComponent() {
const { color, setColor } = useContext(MyContext);
return <div className="foo-component">
<strong>Foo Component</strong>
<button onClick={() => {
// This is Copilots suggestion lol
const randomColor = `#${Math.floor(Math.random() * 16777215).toString(16)}`;
setColor(randomColor);
}}>Randomize color</button>
<div className="color-display" style={{ backgroundColor: color }}></div>
<RenderTracker />
</div>
}
interative demo 가 있습니다. 원문 페이지로 가 직접 확인해보세요.
관련된 데이터라면 변경 사항을 표시해야 했으니 괜찮습니다.
하지만 관련 없는 두 세트의 데이터라면, 그냥 두 개의 Context Provider를 사용하면 됩니다!
컴포넌트의 렌더링이 그 모든 하위 컴포넌트의 렌더링을 유발한다는 사실때문에 많은 혼란이 발생하는 것 같습니다.
또한 Context Provider 는 일반적으로 애플리케이션 최상위에 위치하기 때문에, 사람들이 Context Provider 가 리렌더링될 때 그 아래에 있는 모든 요소가 렌더링될 것이라고 믿게 됩니다.
용어 사용이 다소 혼란스럽네요!
겉보기에는 비슷해 보이는 두 컴포넌트를 봐봅시다.
export function ChildrenStyleOne() {
const [value, setValue] = React.useState(0)
return <div className="some-parent-component">
<strong>ChildrenStyleOne</strong>
<button onClick={() => {
setValue((prev) => prev + 1);;
}}>Increase count: {value}</button>
{/* 👇 Here we declare the RenderTracker directly in the component */}
<RenderTracker />
</div >
}
export function ChildrenStyleTwo(props: React.PropsWithChildren) {
const [value, setValue] = React.useState(0)
return <div className="some-parent-component">
<strong>ChildrenStyleTwo</strong>
<button onClick={() => {
setValue((prev) => prev + 1);;
}}>Increase count: {value}</button>
{/* 👇 Here, it is passed from the parent via the `children` prop */}
{props.children}
</div >
}
export function ReactRenders3() {
return <div className="render-tracker-demo">
<ChildrenStyleOne />
<ChildrenStyleTwo>
<RenderTracker />
</ChildrenStyleTwo>
</div >
}
첫 번째는 RenderTracker를 직접 렌더링합니다.
두 번째는 children 속성을 통해 전달됩니다.
용어는 다소 모호하지만, 두 경우 모두 일반적으로 '자식'이라고 부를 수 있습니다.
하지만 동작 방식은 매우 다릅니다!
interative demo 가 있습니다. 원문 페이지로 가 직접 확인해보세요.
React 컨텍스트는 흔히 알려진 것처럼 성능의 악마가 아닙니다.
이 흔한 오해 때문에 사람들은 실제로 필요하지 않은데도 Redux나 Zustand 같은 도구를 찾게 됩니다.
물론, 하나의 Context Provider 에 수십 개의 상태 조각을 담으면 문제가 생길 수 있습니다.
하지만 애플리케이션의 서로 다른 부분에 위치한 컴포넌트 간에 상태를 전달하는 용도로만 사용한다면 전혀 문제없으며, 오히려 Redux나 Zustand 같은 전역 상태 관리자를 사용하는 것보다 훨씬 깔끔한 해결책이라고 말해도 과언이 아닙니다.
진정한 성능의 악마를 찾고 싶다면, 바로 제어 컴포넌트입니다.
예를 들어 여기에서 볼 수 있듯이, 키 입력마다 렌더링이 발생합니다.
interative demo 가 있습니다. 원문 페이지로 가 직접 확인해보세요.
Context Provider 를 두려워하지 마세요. 종종 작업에 딱 맞는 도구입니다.
전역 상태 관리자가 필요 없다는 말인가요?
아닙니다.
어느 정도 규모가 있는 애플리케이션이라면, 전역 상태 관리를 위해 컨텍스트를 사용하는 것은 번거로워질 것입니다. 특히 상태 조각들이 서로 상호작용해야 할 가능성이 있다면 더욱 그렇습니다.
예시 ) 사용자 정보(User) 와 권한(Permission)
: user 상태에는 로그인한 사용자의 정보가 담겨 있고, permission 상태는 사용자의 역할(role)에 따라 권한을 관리.const user = { id: 1, role: 'admin' }; const permission = { canEdit: user.role === 'admin' };
- permission은 user.role에 의존
- 사용자가 로그아웃하거나 다른 계정으로 전환되면 permission도 같이 갱신되어야 함
- 이런 식의 의존성이 많아질수록 단순한 Context만으로는 관리가 어려워짐
하지만 예를 들어, 단일 페이지에 컴포넌트 계층 구조의 서로 다른 분기에 위치한 두 컴포넌트가 상태를 공유해야 하고, 이 페이지가 해당 컴포넌트들이 사용되는 유일한 장소라면—이럴 때 Context Provider 사용이 적절할 수 있습니다. 애플리케이션의 나머지 부분과 관련 없는 상태를 글로벌 상태 관리자에 추가하는 것보다 더 깔끔한 해결책이 될 수 있다고 제안합니다.
즉, 제한된 범위 내에서 사용한다면 더 깔끔한 해결책이 될 수 있다는 의미.