chapter 11 / 12
내부 뜯어보기 ⑦: ErrorBoundaryGroup은 숫자 하나를 방송한다
ErrorBoundaryGroup은 100줄이지만 새 로직을 거의 만들지 않는다. 앞 장의
ErrorBoundary가 이미 가진 resetKeys 복구 기능에 "공유 숫자"를 얹었을 뿐이다.
이 영리한 재사용을 살펴본다.
- 소스:
packages/react/src/ErrorBoundaryGroup.tsx(100줄) - 의존 부품:
useIsChanged(→usePrevious),increase,SuspensiveError - 함께 export:
useErrorBoundaryGroup,ErrorBoundaryGroup.Consumer
한 문장 요약
흩어진 여러
ErrorBoundary를 묶어 한 번에 리셋하게 해주는 래퍼. "다시 시도" 버튼 하나로 그룹 안 모든 에러 경계를 복구한다.
핵심 아이디어는 단 하나, resetKey(숫자)를 context로 방송하는 것이다.
핵심 메커니즘 — resetKey 방송
이 컴포넌트의 전부는 "숫자 하나(resetKey)를 context로 방송하는 것"이다.
const [resetKey, reset] = useReducer(increase, 0);
// ...
const value = useMemo(() => ({ reset, resetKey }), [resetKey]);
return (
<ErrorBoundaryGroupContext.Provider value={value}>
{children}
</ErrorBoundaryGroupContext.Provider>
);ErrorBoundary 층 5에서 봤던 그 줄이 바로 소비자다.
// ErrorBoundary 안
const group = useContext(ErrorBoundaryGroupContext) ?? { resetKey: 0 };
// ...
resetKeys={[group.resetKey, ...(resetKeys || [])]} // ← 그룹 resetKey를 자기 resetKeys 맨 앞에 병합이렇게 루프가 완성된다.
ErrorBoundaryGroup: resetKey를 context로 방송 (생산자)
│
▼
각 ErrorBoundary: group.resetKey를 자기 resetKeys에 합침 (소비자)
│
reset() 호출 → resetKey +1 → 모든 자식 ErrorBoundary의 resetKeys가 바뀜 → 전부 자동 리셋 ✅즉 ErrorBoundaryGroup은 새 로직을 거의 안 만들고 ErrorBoundary가 이미 가진
resetKeys 자동 복구 기능에 "공유 숫자"를 얹었을 뿐이다.
useReducer(increase, 0) — reset이 곧 "증가 버튼"
const [resetKey, reset] = useReducer(increase, 0); // increase = (prev) => prev + 1useReducer의 두 번째 반환값은 원래 dispatch인데, 이걸 reset이라고 이름
붙였다.
reset()호출 → dispatch 발생 → reducerincrease(prev) => prev + 1실행 → resetKey가 +1- action 페이로드가 필요 없어
reset()처럼 인자 없이 부른다(reducer가 인자를 무시하고 증가만 함).
왜 boolean 토글이 아니라 증가하는 숫자일까? resetKeys는 Object.is로
"이전과 다른가"를 비교한다(hasResetKeysChanged). 단조 증가하는 숫자는 매번
무조건 새 값이라 변화가 100% 감지된다. 토글도 되지만 카운터가 더 안전하고
깔끔하다.
useMemo(() => ({ reset, resetKey }), [resetKey])로 context 값을 메모이즈해,
resetKey가 안 바뀌면 소비자들이 불필요하게 리렌더되지 않게 한다(reset은
useReducer dispatch라 신원이 항상 안정적).
중첩 그룹 + blockOutside — 부모가 리셋되면 자식도
그룹 안에 또 그룹을 넣을 수 있다. 이때 "부모 그룹이 리셋되면 자식 그룹도 같이 리셋될까?"라는 물음이 생긴다.
const parentGroup = useContext(ErrorBoundaryGroupContext); // 나를 감싼 부모 그룹
const isParentGroupResetKeyChanged = useIsChanged(parentGroup?.resetKey); // 부모 resetKey가 바뀌었나?
useEffect(() => {
if (!blockOutside && isParentGroupResetKeyChanged) {
reset(); // 부모가 리셋됐고 blockOutside가 아니면 → 나도 리셋
}
}, [isParentGroupResetKeyChanged, blockOutside]);- 기본(
blockOutside=false): 부모 그룹이 리셋되면 자식 그룹도 연쇄(cascade) 리셋된다. 부모 하나 리셋으로 전체가 복구된다. blockOutside={true}: 이 그룹을 부모의 리셋으로부터 격리해 내부 경계들을 외부 그룹 리셋에서 보호한다.
useIsChanged — "이전 렌더와 값이 달라졌나"
export const useIsChanged = (value) => usePrevious(value) !== value;
export const usePrevious = (value) => {
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]); // 렌더 "후"에 갱신
return ref.current; // 렌더 "중"엔 이전 값을 반환
};usePrevious는 ref를 렌더 후(useEffect)에 갱신하므로 렌더 도중엔 직전 값을 들고
있다. 그래서 usePrevious(x) !== x는 "이번 렌더에서 x가 바뀌었나?"가 된다.
이걸로 부모 resetKey의 변화를 감지해 cascade를 트리거한다.
useErrorBoundaryGroup / Consumer — 자식에서 리셋 트리거
export const useErrorBoundaryGroup = () => {
const group = useContext(ErrorBoundaryGroupContext);
SuspensiveError.assert(group != null, ...); // 반드시 그룹 안에서 써야 함
return useMemo(() => ({ reset: group.reset }), [group.reset]);
};그룹 안 어디서든 useErrorBoundaryGroup().reset()으로 그룹 전체를 리셋할 수
있다. ErrorBoundaryGroup.Consumer는 같은 걸 render-prop으로 제공한다.
SuspensiveError.assert로 "그룹 밖에서 쓰면 에러"를 런타임에 강제한다 —
ErrorBoundary의 훅들과 같은 안전장치다.
ErrorBoundaryGroup도 Object.assign 패턴으로
displayName, with, Consumer를 부착한다. ErrorBoundaryGroupContext.displayName은
개발 모드에서만 설정한다(프로덕션 미세 최적화).
전체 그림
<ErrorBoundaryGroup> ← resetKey(숫자) 관리 + context로 방송
├ useReducer(increase, 0) → reset()=resetKey+1
├ (부모 그룹 있으면) 부모 resetKey 변화 감지 → cascade 리셋 (blockOutside면 차단)
│
├─ <ErrorBoundary/> ─┐
├─ <ErrorBoundary/> ├─ 각자 group.resetKey를 resetKeys에 병합
└─ useErrorBoundaryGroup().reset() ← 버튼이 호출
│
reset() → resetKey +1 → 모든 자식 ErrorBoundary 리셋설계 철학 3줄 요약
- 새 로직 최소화 —
ErrorBoundary가 이미 가진resetKeys복구에 "공유 숫자(resetKey)"를 방송해 얹었을 뿐이다. useReducer(increase, 0)로reset= 카운터 증가 dispatch — 단조 증가라 변화 감지가 100% 확실하다.- 중첩 시
useIsChanged로 부모 resetKey 변화를 감지해 cascade 리셋,blockOutside로 격리를 선택한다.