2026년 2월 14일

React 훅은 왜 조건부로 호출할 수 없을까

조건부 훅 호출 시 마주치는 에러

React 컴포넌트를 작성하다 보면 특정 상황에서만 훅을 실행하고 싶을 때가 있습니다.

export default function App() {
  const [isEditing, setIsEditing] = useState(false);
 
  // ❌ 편집 상태일 때만 자동 저장 훅을 실행하려는 시도
  if (isEditing) {
    useAutoSave("draft");
  }
 
  return <button onClick={() => setIsEditing((e) => !e)}>토글</button>;
}

혹은 데이터가 없을 때 조기 반환(Early Return)을 하고 그 뒤에 훅을 두는 경우도 있습니다.

export default function Profile({ userId }) {
  // ❌ Early Return 뒤에 훅 호출
  if (!userId) return <p>유저를 선택해주세요</p>;
 
  const [user, setUser] = useState(null);
  useEffect(() => {
    /* fetch user... */
  }, [userId]);
 
  return <p>{user?.name}</p>;
}

하지만 이렇게 작성하면 ESLint 규칙(react-hooks/rules-of-hooks)에 의해 다음과 같은 에러가 발생합니다.

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.

React는 왜 훅을 반드시 컴포넌트 최상단에서 일정한 순서로만 호출하도록 제한할까요?
이유를 알기 위해서는 React가 훅의 상태를 메모리에 저장하고 꺼내오는 방식을 살펴볼 필요가 있습니다.


훅의 상태 저장 방식: 호출 순서 기반 슬롯

컴포넌트 안에서 useState나 useEffect를 여러 번 호출할 때, React는 각 훅을 변수 이름으로 식별하지 못합니다.

function Form() {
  const [name, setName] = useState("Mary"); // 1번째 훅
  useEffect(function persistForm() { ... });       // 2번째 훅
  const [surname, setSurname] = useState("Poppins"); // 3번째 훅
}

자바스크립트 언어 특성상, const [name, setName]이라는 로컬 변수명은 함수 내부의 문법일 뿐 함수 실행 시점에 React 런타임에 전달되지 않습니다.

따라서 React가 컴포넌트의 여러 상태를 기억할 수 있는 유일한 기준은 컴포넌트가 렌더링될 때의 호출 순서(인덱스)입니다.

한눈에 보기다이어그램
다이어그램 렌더링 중...
  • 초기 렌더링: 훅이 호출될 때마다 슬롯 노드를 생성하여 Fiber 노드의 연결 리스트에 순서대로 매답니다.
  • 리렌더링: 리스트의 첫 번째 노드부터 포인터를 한 칸씩 이동하며, 이전에 저장해 둔 상태를 순서대로 반환합니다.

이 방식은 별도의 해시맵 검색 없이 포인터 이동만으로 상태를 찾을 수 있어 성능상 가볍습니다. 하지만 매 렌더링마다 훅의 호출 순서와 개수가 달라지지 않아야 한다는 조건이 전제됩니다.


훅의 호출 순서가 바뀌면 발생하는 문제

만약 중간에 있는 훅이 조건문에 의해 실행되지 않으면 어떻게 될까요?

한눈에 보기다이어그램
다이어그램 렌더링 중...

아래 Playground에서 실제로 name 입력값을 지워서 조건문이 건너뛰어지도록 해보세요.

직접 실행해보기코드를 바꿔볼 수 있어요

name이 빈 문자열이 되어 조건문 안의 useEffect가 실행되지 않으면, React는 2번째로 호출된 useState(surname)를 슬롯 #2(원래 Effect가 저장되어 있던 자리)와 매칭하려고 시도합니다.

이로 인해 Effect 데이터 객체를 state 값으로 잘못 읽어오게 되고, 그 뒤에 오는 모든 상태의 인덱스까지 한 칸씩 밀리면서 런타임 크래시가 발생합니다.

Key를 직접 넘기는 방식의 한계

useState('name', 'Mary')처럼 각 훅에 고유한 Key를 넘겨 식별하는 방식도 고려해볼 수 있습니다. 순서가 바뀌어도 Key로 상태를 찾으면 되기 때문입니다.

실제로 React 팀도 훅을 설계할 때 이 대안을 가장 먼저 검토했습니다.1 하지만 Key 방식에는 치명적인 문제가 있었습니다. 바로 커스텀 훅을 조합할 때 발생하는 이름 충돌과 Key 전달의 번거로움입니다.

1. 커스텀 훅의 Key 충돌과 Key 전달 문제

폼 입력을 다루는 커스텀 훅을 작성한다고 가정해 보겠습니다.

// ❌ Key를 직접 넘겨야 하는 가상의 훅
function useFormInput(key, initialValue) {
  // 내부에서 고정된 Key를 쓰면 같은 훅을 두 번 부를 때 상태가 충돌함
  const [value, setValue] = useState(key, initialValue);
  return { value, onChange: (e) => setValue(e.target.value) };
}
 
function ProfileForm() {
  // ⚠️ 훅을 호출할 때마다 매번 다른 Key를 붙여주어야 함
  const name = useFormInput("user_name", "Mary");
  const surname = useFormInput("user_surname", "Poppins");
}
  • 외부 라이브러리와의 Key 충돌: 내가 만든 훅과 외부 라이브러리(useAuth)가 내부에서 우연히 같은 Key('loading')를 사용하면 한쪽 상태가 덮어씌워집니다.
  • Key 전달(Drilling)의 번거로움: 충돌을 피하려면 모든 커스텀 훅이 상위에서 고유 Key를 인자로 받아 내부 훅들에 계속 넘겨주어야 합니다(key + '_child').

2. 순서 기반 연결 리스트의 이점

호출 순서 기반의 연결 리스트는 이 문제를 해결합니다.

비교 항목Key 기반 방식호출 순서 기반 (현재 React)
커스텀 훅 조합훅마다 고유 Key를 전달해야 함 (충돌 위험)훅을 자유롭게 중첩해서 조합 가능
코드 간결성매번 중복 없는 Key 문자열 관리 필요useState(0)처럼 초기값만 전달
상태 조회 성능매 렌더링마다 해시맵 조회 및 동적 트리 관리포인터만 한 칸 이동 (O(1)O(1))

이러한 호출 순서 규칙 덕분에, 개발자는 Key 충돌이나 전달 과정을 신경 쓰지 않고 커스텀 훅을 자유롭게 중첩하고 조합할 수 있습니다.


실무에서 조건부 로직을 다루는 3가지 패턴

조건에 따라 상태나 사이드 이펙트를 다르게 처리해야 할 때는 다음과 같은 패턴을 활용할 수 있습니다.

1. 컴포넌트 분리 후 조건부 렌더링

가장 권장되는 방식입니다. 해당 상태가 필요한 영역을 별도 컴포넌트로 분리하고, 상위에서 렌더링 여부를 결정합니다.

function Parent() {
  const [isEditing, setIsEditing] = useState(false);
 
  // 컴포넌트 자체를 조건부로 렌더링하여 훅 규칙 준수
  return isEditing ? <AutoSaveEditor /> : <Viewer />;
}

2. 훅은 항상 호출하고 내부에서 조기 반환

훅 자체는 매 렌더링마다 빠짐없이 호출하되, 조건 분기는 훅의 인자나 내부 useEffect 안에서 처리합니다.

function Editor({ isEditing, text }) {
  // 훅은 항상 실행하고, 비활성 상태일 땐 null 전달
  useAutoSave(isEditing ? text : null);
}
 
function useAutoSave(data) {
  useEffect(() => {
    if (!data) return; // 💡 훅 내부에서 조건 분기
    saveDraft(data);
  }, [data]);
}

3. Early Return보다 상단에 훅 선언

컴포넌트에 조기 반환(Early Return) 로직이 있다면, 모든 훅을 그보다 위쪽에 먼저 선언합니다.

function UserProfile({ userId }) {
  // ✅ 모든 훅을 컴포넌트 최상단에 먼저 선언
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
 
  // ✅ 훅 선언이 끝난 뒤에 조기 반환 수행
  if (!userId) return <EmptyState />;
  if (loading) return <Spinner />;
 
  return <div>{user.name}</div>;
}

참고 자료

1

Dan Abramov, Why Do React Hooks Rely on Call Order? — React 팀이 훅 설계 당시 Key 방식, Symbol 방식 등을 검토하고 순서 기반 방식을 선택한 기술적 배경을 설명합니다.

KHLogo