React 컨텍스트를 쓰면 상태가 바뀔 때마다 Provider 아래의 모든 것이 리렌더링된다는 흔한 오해를 반박하고, 실제로 무엇이 리렌더링을 유발하는지 예제로 설명합니다.
게시일: 2025년 5월 9일
info
저는 사람들이 React 컨텍스트가 상태 관리에 적합하지 않다고 믿는 경우를 자주 봅니다. 컨텍스트의 상태가 바뀔 때마다 React Provider 아래에 있는 모든 것이 리렌더링된다고 생각하기 때문이죠.
이 때문에 사람들은 컨텍스트 사용을 피하고, Redux나 Zustand 같은 도구로 곧바로 넘어가곤 합니다.
하지만 그건 오해입니다. 저는 그 오해를 반박하려고 합니다.
제가 만든 예제 애플리케이션은 다음과 같습니다.
tsx26export function ReactRenders1() { 27 const [value, setValue] = React.useState("foo"); 28 29 return <MyProvider> 30 31 <button onClick={() => { 32 setValue(`${Math.random()}`); 33 }} 34 className="global-render-button" 35 > Render all</button> 36 37 <div className="render-tracker-demo"> 38 39 <StateChanger /> 40 <StateDisplayer /> 41 42 <SomeUnrelatedComponent /> 43 <SomeUnrelatedComponent /> 44 <SomeUnrelatedComponent /> 45 </div> 46 </MyProvider > 47} 48
애플리케이션 최상단에는 전체 앱을 실제로 리렌더링하는 버튼을 하나 두었습니다.
이건 여기서 어떤 꼼수도 쓰지 않는다는 걸 보여주기 위한 장치입니다.
tsx18 19const MyProvider = ({ children }: { children: React.ReactNode }) => { 20 const [value, setValue] = React.useState("foo"); 21 const contextValue: MyContextType = { value, setValue }; 22 return <MyContext.Provider value={contextValue}>{children}</MyContext.Provider>; 23};
컨텍스트 Provider는 단순합니다. useState 훅에 상태를 저장하고, 그것을 컨텍스트 Provider로 내려줍니다.
tsx51function StateChanger() { 52 const { setValue } = useContext(MyContext); 53 return <div className="state-changer"> 54 <strong>State Changer</strong> 55 56 <button onClick={() => setValue(`${Math.random()}`)}>Change state</button> 57 <RenderTracker /> 58 </div > 59} 60 61function StateDisplayer() { 62 const { value } = useContext(MyContext); 63 return <div className="state-displayer"> 64 <strong>State Displayer</strong> 65 <div>{value}</div> 66 <RenderTracker /> 67 </div> 68}
컨텍스트를 사용하는 컴포넌트는 두 개입니다.
tsx70function SomeUnrelatedComponent() { 71 return <div className="some-unrelated-component"> 72 <strong>Some unrelated component</strong> 73 <RenderTracker /> 74 </div> 75}
컨텍스트를 사용하지 않는(관련 없는) 컴포넌트도 여러 개 두었습니다.
tsx2export function RenderTracker() { 3 4 let randX = Math.floor(Math.random() * 100); 5 let randY = Math.floor(Math.random() * 100); 6 7 8 return <div className="render-tracker"> 9 <strong>Render Tracker</strong> 10 <div className="render-tracking-dot" style={{ top: `${randY}%`, left: `${randX}%` }}> 11 </div> 12 </div > 13}
렌더 추적 컴포넌트는 렌더링될 때마다 점(dot)을 다른 위치에 표시합니다.
결과는 어떨까요?
직접 확인해 볼 수 있습니다:
☝️ 인터랙티브 데모
State Changer
Render Tracker
Mount value: 0.0177
State Displayer
foo
Render Tracker
Mount value: 0.2213
render all 버튼을 클릭하면 действительно로 전체 앱이 렌더링된다는 것을 볼 수 있습니다.
반면 Change state 버튼을 클릭하면, 컨텍스트를 소비하는 컴포넌트들에만 영향이 있다는 것을 확인할 수 있습니다.
저는 이 혼동이 두 가지에서 온다고 생각합니다.
만약 제가 같은 컨텍스트 Provider에 color/setColor, foo/setFoo, bar/setBar 같은 상태 쌍들을 추가하고, 새 컴포넌트 FooComponent가 그 새로운 상태 일부를 사용하도록 만든다면, 이런 상태 변화는 리렌더링을 유발합니다. 하나의 컨텍스트 Provider를 소비하는 모든 consumer들은 상태가 바뀌면 리렌더링됩니다.
☝️ 인터랙티브 데모
Code
tsx83function FooComponent() { 84 const { color, setColor } = useContext(MyContext); 85 return <div className="foo-component"> 86 <strong>Foo Component</strong> 87 <button onClick={() => { 88 // 이건 Copilot의 제안임 ㅋㅋ 89 const randomColor = `#${Math.floor(Math.random() * 16777215).toString(16)}`; 90 setColor(randomColor); 91 }}>Randomize color</button> 92 <div className="color-display" style={{ backgroundColor: color }}></div> 93 <RenderTracker /> 94 </div> 95}
State Changer
Render Tracker
Mount value: 0.6615
State Displayer
foo
Render Tracker
Mount value: 0.1276
Foo Component
Render Tracker
Mount value: 0.8689
색상을 무작위로 바꾸면 다른 컨텍스트 consumer들까지 렌더링되는 걸 확인할 수 있습니다.
이게 전부 서로 관련된 데이터이고, 어차피 변경을 보여줘야 했다면 괜찮습니다.
하지만 서로 관련 없는 데이터 두 덩어리가 있다면, 컨텍스트 Provider를 두 개 쓰면 됩니다!
혼동의 많은 부분은 “컴포넌트가 렌더되면 그 자손(descendant)들도 모두 렌더된다”는 사실을 알고 있기 때문이라고 생각합니다.
그리고 컨텍스트 Provider는 보통 앱 최상단에 있기 때문에, Provider가 리렌더링되면 그 아래의 모든 것도 리렌더링된다고들 믿습니다.
안타깝게도, 여기서 용어가 조금 헷갈립니다!
겉보기에는 비슷한 두 컴포넌트를 보겠습니다:
tsx8export function ChildrenStyleOne() { 9 const [value, setValue] = React.useState(0) 10 return <div className="some-parent-component"> 11 <strong>ChildrenStyleOne</strong> 12 <button onClick={() => { 13 setValue((prev) => prev + 1);; 14 }}>Increase count: {value}</button> 15 {/* 👇 여기서는 RenderTracker를 컴포넌트 안에서 직접 선언해 렌더링합니다 */} 16 <RenderTracker /> 17 </div > 18} 19 20export function ChildrenStyleTwo(props: React.PropsWithChildren) { 21 const [value, setValue] = React.useState(0) 22 return <div className="some-parent-component"> 23 <strong>ChildrenStyleTwo</strong> 24 <button onClick={() => { 25 setValue((prev) => prev + 1);; 26 }}>Increase count: {value}</button> 27 {/* 👇 여기서는 부모가 `children` prop으로 전달한 것을 렌더링합니다 */} 28 {props.children} 29 </div > 30} 31 32export function ReactRenders3() { 33 return <div className="render-tracker-demo"> 34 <ChildrenStyleOne /> 35 <ChildrenStyleTwo> 36 <RenderTracker /> 37 </ChildrenStyleTwo> 38 </div > 39}
첫 번째는 RenderTracker를 직접 렌더링합니다.
두 번째는 children prop으로 전달받아 렌더링합니다.
일상적인 표현으로는 두 경우 모두 “자식(children)”이라고 부를 수 있어 용어가 다소 애매합니다.
하지만 동작은 꽤 다릅니다!
☝️ 인터랙티브 데모
ChildrenStyleOne
RenderTracker is directly rendered
Render Tracker
Mount value: 0.0544
ChildrenStyleTwo
RenderTracker is rendered as props.children
Render Tracker
Mount value: 0.7159
children으로 전달된 RenderTracker는 상태가 바뀌어도 리렌더링되지 않는다는 점을 확인할 수 있습니다.
React 컨텍스트는 흔히 말하는 성능 괴물(boogeyman)이 아닙니다.
이 흔한 오해 때문에 실제로는 필요하지 않은데도 Redux나 Zustand 같은 도구를 먼저 집어드는 경우가 생깁니다.
물론 하나의 컨텍스트 Provider에 수십 가지 상태를 쓸어 넣으면 문제가 생길 수 있습니다.
하지만 앱의 서로 다른 위치에 있는 컴포넌트끼리 상태를 전달하는 용도라면 전혀 괜찮습니다. 그리고 감히 말하자면 Redux나 Zustand 같은 전역 상태 Provider를 쓰는 것보다 더 깔끔한 해결책일 때도 많습니다.
정말로 “성능 괴물”을 찾고 싶다면, 그건 컨트롤드 컴포넌트(controlled component)입니다.
예를 들어 아래처럼 매 키 입력마다 렌더가 발생하는 걸 볼 수 있죠:
☝️ 인터랙티브 데모
Code
tsx9export function ReactRenders4() { 10 const [value, setValue] = useState('') 11 return <div className="render-tracker-demo"> 12 <div className="some-parent-component"> 13 <input type="text" value={value} onChange={(e) => setValue(e.target.value)} placeholder="type here" /> 14 <RenderTracker /> 15 </div> 16 </div > 17}
Render Tracker
Mount value: 0.5655
텍스트 박스에 입력해 보면서, 키를 누를 때마다 렌더가 발생하는 걸 확인해 보세요.
컨텍스트 Provider를 두려워하지 마세요. 많은 경우 이게 딱 맞는 도구입니다.
아니요.
규모가 조금만 커져도, 컨텍스트로 전역 상태를 관리하는 건 금방 번거로워질 수 있습니다. 특히 상태 조각(slice)들이 서로 상호작용해야 할 가능성이 있다면 더 그렇습니다.
하지만 예를 들어, 단일 페이지에서 컴포넌트 트리의 서로 다른 가지(branch)에 있는 두 컴포넌트가 상태를 공유해야 하고, 그 페이지가 해당 컴포넌트들을 사용하는 유일한 곳이라면—이럴 때 컨텍스트 Provider가 적절할 수 있습니다. 애플리케이션의 나머지 부분과는 관련 없는 상태를 전역 상태 Provider에 굳이 추가하는 것보다, 더 깔끔한 해결책이 될 수 있다고 생각합니다.
질문? 의견? 비판? 댓글로 남겨주세요! 👇