React 개발을 하다 보면 Context API의 동작 방식에 대해 혼란을 겪을 때가 있다. 특히 "Context는 해당 Context를 구독하는 컴포넌트만 리렌더링한다"는 원칙이 항상 예상대로 동작하지 않는 경우를 마주하곤 한다.
이번 글에서는 다음과 같은 핵심 질문에 답하고자 한다
"Context API는 정말 구독 컴포넌트만 리렌더링하는가? 왜 때로는 모든 하위 컴포넌트가 리렌더링되는가?"
이 질문에 답하기 위해, 먼저 흥미로운 실험 결과를 살펴보겠다.
동일한 기능을 하는 두 가지 방식의 컴포넌트를 작성했다. 하나는 children props를 사용하고, 다른 하나는 JSX 내부에서 직접 컴포넌트를 선언하는 방식이다.
import { createContext, useContext, useState } from "react";
// Context를 사용하지 않는 일반 컴포넌트
function Comp() {
console.log("Comp re-render");
return <div>Comp</div>;
}
// Context를 구독하는 컴포넌트
function CompWithContext() {
useContext(TestContext);
console.log("CompWithContext re-render");
return <div>CompWithContext</div>;
}
// TestComp1: children props 사용
export function TestComp1() {
return (
<TestProvider>
<Comp />
<CompWithContext />
</TestProvider>
);
}
// TestComp2: 직접 선언 방식
export function TestComp2() {
const [state, setState] = useState({});
return (
<TestContext.Provider value={state}>
<button onClick={() => setState({})}>forceRender</button>
<Comp />
<CompWithContext />
</TestContext.Provider>
);
}
const TestContext = createContext({});
function TestProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState({});
return (
<TestContext.Provider value={state}>
<button onClick={() => setState({})}>forceRender</button>
{children}
</TestContext.Provider>
);
}
TestComp1 실행 결과: Context를 구독하는 CompWithContext 컴포넌트만 리렌더링

TestComp2 실행 결과: Comp와 CompWithContext 모두 리렌더링

두 컴포넌트는 겉보기에 동일한 구조를 가지고 있다. 모두 Context Provider 아래에 Comp와 CompWithContext를 렌더링하고, Provider의 상태를 변경하는 버튼이 있다.
하지만 실행 결과는 완전히 달랐다. 이 차이를 이해하려면 children props가 어떻게 동작하는지 정확히 알아야 한다.
Children props는 React에서 컴포넌트의 합성(Composition)을 가능하게 하는 강력한 패턴이다.
// children을 사용한 예시 ✅
function A({ children }) {
return <div>{children}</div>;
}
function App() {
return (
<A>
<B />
</A>
);
}
이 구조에서 A 컴포넌트는 내부에 어떤 내용을 렌더링할지 외부에서 결정할 수 있으므로, 다양한 상황에 맞게 유연하게 재사용할 수 있다.
// children을 사용하지 않은 예시 ❌
function A() {
return (
<div>
<B />
</div>
);
}
function App() {
return <A />;
}
이 경우 A 내부에서 항상 B 컴포넌트만 렌더링되므로, A는 고정된 UI를 가지게 되고 재사용 범위가 제한된다.
장점
단점
언제 사용해야 하는가?
function Button({ children, onClick }) {
return <button onClick={onClick}>{children}</button>;
}
React는 JSX를 해석하기 위해 내부적으로 React.createElement 함수를 호출한다. 여기서 중요한 점은 컴포넌트를 실행하는 것과 React.createElement를 호출하는 것은 다르다는 것이다.

<App />
위 코드는 React.createElement(App)으로 변환되며, 이 호출 결과는 ReactElement 객체다. 이 객체는 "App이라는 컴포넌트를 어떤 props로 렌더링할지"에 대한 정보를 담고 있는 일종의 설명서 역할을 한다.
즉, <App />이 곧바로 컴포넌트 함수를 실행하는 것이 아니라, React가 렌더링을 수행할 때 이 ReactElement를 해석하여 그 시점에 App() 컴포넌트 함수를 호출하게 된다.
App() 함수를 실행하면, App 내부의 JSX가 평가된다. 즉, App() 함수가 실행되어야 비로소 App 내부 JSX가 React.createElement("main", { id: "main" }, ...) 같은 형태로 변환되며, 새로운 ReactElement가 생성된다.
React.createElement의 인자
"main"){ id: "main" })React.createElement("p", null, "Hello"))이제 children이 어떻게 해석되는지 구체적으로 살펴보자
Children props 사용 시
function Parent({ children }) {
return <div>{children}</div>;
}
function Child() {
return <div>자식 컴포넌트</div>;
}
function App() {
return (
<Parent>
<Child />
</Parent>
);
}
React는 App()을 실행하면 App 내부의 JSX를 다음과 같이 해석한다.
React.createElement(Parent, null, React.createElement(Child, null))
<Child />는 즉시 실행되어 ReactElement로 평가된 후, Parent 컴포넌트의 children으로 전달된다.
그리고 Parent가 실행되면 Parent 내부 JSX가 다음과 같이 해석된다.
React.createElement('div', null, children)
여기서 중요한 점은, children은 JSX가 아닌, 이미 평가된 ReactElement라는 것이다. 즉, Parent는 children을 실행하는 것이 아니라, 전달받은 ReactElement를 화면에 렌더링만 한다.
즉, Parent가 리렌더링되더라도 children은 App에서 이미 평가된 값이므로 변경되지 않는다. children의 리렌더링을 유발하려면 App 컴포넌트 자체가 리렌더링되어야 한다.
직접 선언 시
function Parent() {
return (
<div>
<Child />
</div>
);
}
function Child() {
return <div>자식 컴포넌트</div>;
}
function App() {
return <Parent />;
}
App()을 실행하면:
React.createElement(Parent, null)
이후 React는 Parent를 실행하고 Parent 내부 JSX를 다음과 같이 변환한다
React.createElement('div', null, React.createElement(Child, null))
다시 Child 컴포넌트를 재귀적으로 실행하여 JSX를 해석한다
React.createElement('div', null, "자식 컴포넌트")
Child 컴포넌트는 Parent 컴포넌트가 실행되는 과정에서 함께 실행된다.
위 두 예시를 비교하면 Child 컴포넌트가 언제 실행되는지가 다르다.
Children props 사용 시: Child는 Parent의 부모인 App 컴포넌트가 실행될 때 이미 평가됨Child는 Parent 컴포넌트가 실행될 때 평가됨function Parent({ children }) {
return <div>{children}</div>;
}
function Child() {
return <div>자식 컴포넌트</div>;
}
function App() {
return (
<Parent>
<Child />
</Parent>
);
}
이는 곧, 위 코드에서 children의 실질적인 부모는 Parent가 아니라 App 컴포넌트라고 볼 수 있다.
즉, children은 Parent 내부에서 실행되는 것이 아니라, 상위 컴포넌트에서 이미 평가된 결과가 전달되는 것이다.
우리는 보통 JSX 상에서의 계층 구조를 보고 <Child /> 컴포넌트가 <Parent />의 자식이라고 생각한다. 하지만 실제로는 Child와 Parent는 형제 노드처럼 동작하며, 공통 부모인 App을 가진다.
이해를 돕기 위해 코드를 다르게 표현하면
function App() {
return <Parent children={<Child />} />;
}
이 코드를 보면, Child는 App 컴포넌트 내부에서 실행되며, 그 결과가 Parent의 children으로 전달된다.
정리
Children props 사용: 상위 컴포넌트에서 평가된 결과가 전달됨 → Parent의 진짜 자식이 아님JSX에 직접 선언: 해당 컴포넌트 내부에서 평가됨 → Parent의 진짜 자식임이제 children props의 동작 원리를 Context API에 적용하자
export function TestComp1() {
return (
<TestProvider>
<Comp />
<CompWithContext />
</TestProvider>
);
}
function TestProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState({});
return (
<TestContext.Provider value={state}>
<button onClick={() => setState({})}>forceRender</button>
{children}
</TestContext.Provider>
);
}
왜 Context 구독 컴포넌트만 리렌더링되는가?
겉보기에는 Comp와 CompWithContext가 TestProvider의 자식처럼 보인다. 그래서 TestProvider의 상태가 변경되면 두 컴포넌트 모두 리렌더링될 것으로 예상할 수 있다.
하지만 앞서 설명했듯이, children props는 실제로는 자식 컴포넌트가 아니라 상위 컴포넌트에서 평가된 ReactElement를 전달받는 형식이다. 즉, TestProvider, Comp, CompWithContext는 실행 시점상 같은 레벨의 형제 노드처럼 동작하게 된다.
이를 명확히 표현하면
export function TestComp1() {
return (
<TestProvider children={[<Comp />, <CompWithContext />]} />
);
}
TestProvider가 실행될 때 이미 children은 평가가 완료된 상태이며, 이는 TestProvider의 상태가 변경되더라도 children 자체가 다시 평가되지 않음을 의미한다.
즉, TestProvider의 상태 변경은 children을 다시 실행시키지 않는다.
정리
children은 상위 컴포넌트(TestComp1)에서 이미 평가된 ReactElementTestProvider의 상태가 바뀌더라도 children 자체는 다시 평가되지 않음Context를 구독하고 있는 컴포넌트(CompWithContext)만 리렌더링 발생export function TestComp2() {
const [state, setState] = useState({});
return (
<TestContext.Provider value={state}>
<button onClick={() => setState({})}>forceRender</button>
<Comp />
<CompWithContext />
</TestContext.Provider>
);
}
왜 모든 자식 컴포넌트가 리렌더링되는가?
TestComp2의 실행 결과는 예상대로 Comp와 CompWithContext 모두 리렌더링이 발생한다. 이는 너무나도 자연스러운 결과다.
현재 state는 TestComp2 내부에서 관리되고 있으며, 이 상태 변경은 컴포넌트 전체의 리렌더링을 유발한다. 그리고 해당 상태를 직접적으로 참조하든 아니든, TestComp2의 자식 컴포넌트인 Comp와 CompWithContext 모두 리렌더링의 영향을 받는다.
React의 기본 리렌더링 규칙
상위 컴포넌트의 상태 변경 → 컴포넌트 리렌더링 → 자식 컴포넌트 재호출 → 리렌더링
이 구조는 React의 기본 동작 방식이므로 특별할 것 없는, 당연한 결과다.
처음 제기된 질문에 대한 답은 다음과 같다
"Context API는 해당 Context를 구독하는 컴포넌트만 리렌더링되지만, 구현 방식에 따라 Provider 하위의 모든 컴포넌트가 리렌더링될 수도 있다."
즉, Context 자체는 구독하는 컴포넌트에만 영향을 주지만, Context Provider가 포함된 컴포넌트의 구조에 따라 의도치 않은 리렌더링이 발생할 수 있다.
리렌더링 범위는 Context의 특성뿐 아니라 렌더링 트리의 구조와 코드 작성 방식에 따라 달라진다.
저도 질문에 대한 답을 몰랐는데
우디 덕분에 children에 따라 다르게 동작한다는 것을 알 수 있었습니다 !