Context는 ‘Provider 아래 전부를 리렌더’하지 않는다 ("No, react context is not causing too many renders" 정리)

okorion·2025년 10월 3일

Context는 ‘변경될 때마다 Provider 아래 전부를 리렌더’하지 않는다. 바뀐 값을 실제로 읽는(consume) 컴포넌트만 리렌더된다.

1) Context의 리렌더 전파 규칙 — “값을 읽는 소비자만”

  • 값 비교: Provider의 valueObject.is 기준으로 달라지면(원시값 변경, 혹은 객체 참조 변경), 해당 Context를 읽는 자식들이 리렌더된다. Context를 안 읽는 컴포넌트는 이 변화로 인해 리렌더 대상이 아니다.
  • 부모 최적화와 무관하게 강제 전파: Context는 일반적인 props 경로와 별도 경로로 전파되므로, 상위에서 memo/shouldComponentUpdate 등으로 스킵해도 소비자까지 값이 내려가면 소비자는 업데이트된다. 이것도 문서에 명시돼 있다.

데모와 동일하게 “Render all” 같은 버튼으로 최상단에서 강제로 렌더를 유도하면 트리 전체가 한 번씩 비교를 거치기 때문에 넓게 리렌더가 보인다. 반면 “Change state(컨텍스트 값 변경)”는 Context 소비자만 반응한다 — 바로 위 규칙 때문.


2) 오해의 근원 ①: “Provider 하나에 모든 상태를 욱여넣기”

  • 한 Provider에 여러 슬라이스(state 조각) 혼합 → 어느 하나라도 바뀌면 그 Context를 읽는 모든 소비자가 리렌더. 서로 무관한 화면들까지 “같은 Context를 읽는다”면 같이 리렌더된다.
  • 해결: 관심사별로 Provider 쪼개기가 정석. 컨텍스트 값도 불변/참조 안정화(예: useMemo{foo, setFoo} 묶음 메모)로 필요없는 변경을 줄인다. (단, React Compiler를 쓰면 이런 메모 작업 상당 부분이 자동화된다. 아래 5) 참조)

3) 오해의 근원 ②: “부모가 렌더되면 자식도 전부 렌더된다” vs props.children

겉보기에 비슷해 보여도 자식을 “어디서 생성했는지”가 다르면 결과가 갈린다.

A. 컴포넌트 내부에서 직접 <Child />를 생성

부모가 리렌더될 때마다 새 React Element 객체가 생긴다. 이전 렌더의 자식 요소와 참조가 달라 React가 하위 비교를 위해 내려가게 된다. (이 과정에서 자식이 실제로 화면 갱신을 하느냐는 별개지만, 적어도 “들여다보는 비용”이 발생)

B. 자식을 props.children으로 받아 그대로 내보내기

부모가 같은 children 참조를 계속 전달하면, React는 동일 요소(참조 동일)로 보고 서브트리 진입을 스킵(bailout)할 수 있다. 그래서 데모에서 ChildrenStyleTwo는 카운트가 바뀌어도 전달된 children 참조가 그대로면 하위로 안 내려간다. 이건 Context와 무관하게 리컨실리에이션의 ‘요소 참조 동일성’ 규칙 때문이다.

요약: Context가 아니라, 요소(React Element) 참조의 안정성이 리렌더 범위를 좌우한다. 같은 이유로 부모가 매 렌더마다 새 요소를 만들어서 children에 넣어주면, 다시 내려가게 된다.


4) “Context = 느리다”가 아니라, “설계·참조 관리가 중요”

권장 패턴

  1. Provider 분리: 무관한 상태는 별도 Provider로 쪼갠다.
  2. 값 안정화: value={{ foo, setFoo }}처럼 객체를 매번 새로 만들면 소비자 전부 리렌더. useMemo묶음 참조를 고정한다.
  3. 소비자 최소화: Context를 직접 읽는 컴포넌트를 표면적(leaf)에 가깝게 배치해 영향 범위를 좁힌다.
  4. 선택적 구독(고급): 라이브러리의 selector 기반 useContext 패턴을 쓰면 큰 Context에서 필요한 조각만 구독해 리렌더를 축소할 수 있다.
  5. children 참조 안정화: 부모에서 만든 자식 요소를 같은 참조로 재사용하거나, 자식 컴포넌트는 memo로 감싸 props 변동 없을 때 호출 억제. (불필요한 메모 남발은 금물)

5) React 19 / React Compiler와의 관계

  • React Compiler자동 메모화(Automatic Memoization)로 “언제 어디를 스킵할지”를 빌드 타임에 결정해 많은 수작업 메모화를 없앤다. “use memo” 지시어로 부분 도입도 가능. Next/Expo 등에서 가이드가 제공된다. 이 환경에서는 위에서 말한 children 참조 안정화와 같은 미세 튜닝의 체감 효과가 줄어들 수 있다.
  • 그래도 설계(Provider 분리·의존 경계)가 잘못돼 있으면, Compiler가 자동으로 의미의 경계를 나눠주지는 않는다. “어떤 상태를 누구와 공유할지”는 여전히 아키텍처 이슈다.

6) “진짜” 성능 귀신은? — 컨트롤드 입력

문서/실무에서 자주 지적되듯, 컨트롤드 인풋은 타이핑마다 상태 갱신 → 리렌더다. 이것 자체는 정상 동작. 다만 성능 민감 구간이라면:

  • 디바운스/스로틀로 파생 연산(검색, 검증) 지연
  • useDeferredValue로 비싼 하위 렌더 지연
  • useRef + 비제어 입력 조합 (제출 시점에만 읽기)
    같은 현실적 방안을 고민하는 쪽이 체감 효과가 크다.

7) Context vs 전역 상태 관리자(예: Redux/Zustand)

  • Context가 더 낫다: 특정 페이지 내부에서 분기된 두 컴포넌트끼리만 상태 공유가 필요하고, 앱 전반과 무관한 상태라면 Context가 더 간결하고 깔끔하다.
  • 전역 상태 관리자가 낫다: 여러 페이지/도메인에 걸친 교차 상태, 엄격한 업데이트 규칙/미들웨어/디버깅 도구가 필요하거나, 비동기 흐름/캐싱/쿼리를 표준화해야 한다면 전역 스토어(RTK/Zustand/Query 등)가 맞다.

8) 이번 글과 데모가 시사하는 결론

  • Context 자체가 성능 악당이 아니다. 바뀐 값을 읽는 소비자만 리렌더된다.
  • 문제는 설계(Provider 과밀, 값 객체 매번 새로 생성)와 요소 참조 안정성(직접 생성 vs props.children)을 혼동하는 데서 온다.
  • 규모가 커질수록 Provider 분리 + 값 안정화 + 소비자 최소화가 이득. React 19/Compiler를 쓰면 자동 메모화로 수고를 덜 수 있다.

더 읽을 거리 / 출처

  • React 공식 문서 – Context: “값이 바뀌면 Context 읽는 컴포넌트가 리렌더” 및 Object.is 비교 규칙. react.dev+1
  • React 공식 문서 – useContext: 소비자 리렌더 전파 규칙. react.dev
  • React 공식 문서 – memo: 부모 리렌더에서도 props 불변이면 스킵 가능. react.dev
  • React Compiler 소개/가이드 및 "use memo" 지시어. react.dev+2react.dev+2
  • 원문 글 & 예제 저장소(David Johnston): 글 전문과 데모 코드. Black Sheep Code

원문 - No, react context is not causing too many renders

profile
okorion's Tech Study Blog.

0개의 댓글