Rendered fewer hooks than expected 에러 관련

개요

워크플로우 템플릿 불러오기 기능을 구현하면서
"Rendered fewer hooks than expected"
에러와 함께 여러 가지 리렌더링/상태 꼬임 문제를 겪었습니다.
아래는 문제 해결을 위해 시도했던 모든 방법과 실제 적용된 코드를 정리한 내용입니다.


시도 1: 상태 변경 타이밍 문제로 추측

문제 추측:

  • 템플릿 불러오기 시
    setWorkflowTitle, setTemplateId 등 여러 setState와
    replaceAll(Context 전체 교체)이 동시에 일어나
    렌더링이 꼬인다고 생각.

as-is 코드:

setWorkflowTitle(template.name || '');
setTemplateId(template.id);
replaceAll(template.template?.nodes || [], template.template?.connections || []);

to-be 코드:

  • 상태 변경을 분리하기 위해 setTimeout, useTransition 등으로 분리
setWorkflowTitle(template.name || '');
setTemplateId(template.id);
setTimeout(() => {
  replaceAll(template.template?.nodes || [], template.template?.connections || []);
}, 0);

또는

const [isPending, startTransition] = useTransition();
startTransition(() => {
  replaceAll(template.template?.nodes || [], template.template?.connections || []);
});

결과:

  • 여전히 동일한 에러 발생

시도 2: replaceAll로 인한 트리 구조 변화 문제로 추측

문제 추측:

  • replaceAll이 실행되면 Context의 노드/커넥션이 완전히 교체됨
  • 이때 기존에 있던 FlowNode 등 하위 컴포넌트가 언마운트/마운트됨
  • 이 과정에서 훅 호출 순서가 꼬인다고 생각

as-is 코드:

  • FlowNode 내부에서 node가 undefined일 때 바로 return
const node = useNodeSelector(nodeId);
if (!node) return null; // <-- 문제의 코드
// ...아래에 여러 훅이 있을 수 있음

to-be 코드:

  • 훅 선언은 항상 최상단에서, 조건부 렌더링은 훅 선언 이후에만!
const node = useNodeSelector(nodeId);
// ...다른 훅들

if (!node) {
  // 훅은 항상 실행, 렌더만 최소화
  return <div style={{display: 'none'}} />;
}

또는,
부모에서 조건부 렌더링:

{nodeIds.map(nodeId => {
  const node = getNode(nodeId);
  if (!node) return null;
  return (
    <FlowNode
      key={nodeId}
      nodeId={nodeId}
      // ...props
    />
  );
})}

결과:

  • 에러가 완전히 해결됨!

시도 3: useNodeSelector의 구독 해제 타이밍/race condition 점검

문제 추측:

  • replaceAll로 노드가 교체되는 순간,
    이미 언마운트된 FlowNode에서 setState가 호출될 수 있다고 생각

as-is 코드:

React.useEffect(() => {
  setNode(getNode(nodeId));
  const unsubscribe = subscribeToNode(nodeId, () => {
    setNode(getNode(nodeId));
  });
  return () => {
    unsubscribe();
  };
}, [nodeId, getNode, subscribeToNode]);

to-be 코드:

  • setState 전에 언마운트 여부 체크(필요시)
React.useEffect(() => {
  let unmounted = false;
  setNode(getNode(nodeId));
  const unsubscribe = subscribeToNode(nodeId, () => {
    if (!unmounted) setNode(getNode(nodeId));
  });
  return () => {
    unmounted = true;
    unsubscribe();
  };
}, [nodeId, getNode, subscribeToNode]);

결과:

  • race condition 방지, 하지만 핵심 원인은 아니었음

결론

  • 훅은 항상 컴포넌트 최상단에서, 조건 없이 실행!
  • 조건부 렌더링은 훅 선언 이후에만!
  • 여러 상태 변경은 setTimeout, useTransition 등으로 분리하면 더 안전!
  • 부모에서 조건부 렌더링을 해주는 것도 좋은 방법!

이런 과정을 통해
"Rendered fewer hooks than expected"
에러를 완벽하게 해결할 수 있었습니다.


실전 팁

  • 훅 선언 전에 return null 하지 마세요!
  • map으로 여러 컴포넌트를 렌더링할 때 key는 항상 고유한 값(nodeId 등)으로!
  • 상태/Context 전체 교체 시에는 트리 구조 변화에 주의하세요!

이 글이 비슷한 문제를 겪는 분들께 도움이 되길 바랍니다.