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 전체 교체 시에는 트리 구조 변화에 주의하세요!
이 글이 비슷한 문제를 겪는 분들께 도움이 되길 바랍니다.