정의
에디터·폼 빌더처럼 자주 바뀌는 큰 상태를 여러 컴포넌트가 함께 봐야 할 때, Context를 잘못 쓰면 관계없는 부분까지 다 다시 그려지는 문제를 다룬다. React Context render granularity(컨텍스트 리렌더 세밀도)는 Context Provider(하위 트리에 값을 내려주는 컴포넌트)의 value에 자주 변경되는 큰 상태(전체 문서 트리, 실시간 전체 목록 등)를 넣을 때, 그 Context를 구독하는 모든 하위 컴포넌트가 매 변경마다 리렌더되는 상황을 말한다. 이때 React.memo(props가 안 바뀌면 리렌더를 막아주는 래퍼)·props 동일성·structural sharing(구조적 공유: 바뀐 부분만 새로 만들고 나머지는 재사용) 같은 최적화가 전부 무력화된다. 해법은 구독 범위를 좁히는 Provider 분리 — 자주 바뀌는 조각과 안 바뀌는 조각을 별도 Provider로 쪼개는 것이다.
맥락에서 이해하기
행 단위 리렌더를 O(1)로 줄인 뒤에도 최상단 Provider가 useState로 전체 문서를 들고 있으면, 입력 한 번마다 Provider 함수가 다시 실행된다. 자식 행은 external store로 격리했는데 껍데기 컨테이너만 매번 도는 “마지막 꼬리”가 남는다. 이 패턴은 자주 바뀌는 조각을 Context value에서 빼고, 고정된 store + useSyncExternalStore로 각 UI 조각이 필요한 스냅샷만 구독하게 한다.
왜 필요한가
에디터·폼 빌더는 actions·meta·live document를 한 EditorProvider에 묶기 쉽다. 본문이 한 글자 바뀔 때마다 value 객체가 새 참조가 되면, 목차·통계·툴바까지 관련 없는 블록이 전부 리렌더된다. 대규모 트리에서는 입력 지연·테스트 실패(render count)로 이어진다.
동작 원리
- Consumer는
useContext로 Provider value를 구독한다. - Provider가 re-render되면 value 참조가 바뀌는 한 모든 consumer가 re-render된다 — 중간 memo가 props를 막아도 context 구독은 막지 못한다.
value={{ handlers, document }}inline 객체는 매 render 새 참조다.- document만 바뀌어도 handlers만 쓰는 자식까지 갱신된다.
- 좁은 Context는 document·파생 통계만 담고, TOC·word count 컴포넌트만 그 Provider 아래에 둔다.
- 넓은 runtime context는
useCallback/useMemo로 안정화된 actions·meta만 유지한다.
실무 적용
- 라이브 파생(heading 목록, 글자 수, sync 상태)은 전용 Context 또는 외부 store + selector.
- runtime bag에는 dispatch·stable callback ref·readOnly 플래그 등 변하지 않거나 안정화된 것만.
- 블록/행 단위
renderCount테스트로 “입력 1회에 N개 블록만 갱신” 회귀를 잡는다. - closed union(인라인 노드 variant) 확장 시
type === 'text'가정 지점을 guard — 타입체크가 파급을 드러냄.
트레이드오프
- Provider 분리는 보일러플레이트·트리 깊이를 늘린다.
- context selector 라이브러리·React Compiler는 일부 케이스를 완화하지만, 설계 단계 분리가 여전히 명확하다.
- 과도한 memo는 디버깅을 어렵게 — render count 테스트로 의도를 문서화한다.
사용하면 안 되는 경우
- 작은 폼(필드 3개) — context 분리 비용이 이득보다 큼.
- 이미 전역 store(Zustand selector 등)로 granularity가 해결된 경우 — Context 중복.
- 서버 컴포넌트만 있는 서브트리 — client context 이슈 해당 없음.
흔한 실수
useMemo(() => ({ ...handlers, doc }), [handlers, doc])로 doc을 runtime에 계속 포함.- 파생 통계를 매 render inline 계산해 value에 넣음.
- memo된 Block이 context 구독을 자식이 아닌 자신이 하게 함.
- union 확장 후 markdown·range 연산에서 text-only 가정 유지.
관련 개념
- focused-guest-editor — host/guest·구독 경계
- usesyncexternalstore-snapshot-identity — 외부 store 스냅샷
- list-virtualization-windowing — 대량 리스트 렌더 절감