useState를 조건문 안에서 사용하면 안 되는 이유

park.js·2025년 3월 5일
1

FrontEnd Develop log

목록 보기
41/42

"Hook은 컴포넌트의 최상위 레벨에서만 호출해야 한다"

리액트를 사용하다 보면 마주치는 규칙 중 하나는 "Hook은 컴포넌트의 최상위 레벨에서만 호출해야 한다"는 것이다. 특히 useState를 조건문이나 반복문, 중첩된 함수 내부에서 사용하면 안 된다. 이 규칙을 어기면 어떤 문제가 발생하는지, 그리고 왜 이런 규칙이 존재하는지 자세히 알아보자.

리액트 Hooks의 동작 원리

Hook과 순서 의존성

리액트는 내부적으로 컴포넌트의 상태(state)를 관리하기 위해 '순서'에 의존한다. 컴포넌트가 렌더링될 때마다 Hook이 호출되는 순서를 기억하여 각 Hook과 해당 상태를 연결한다.

예를 들어, 다음과 같은 컴포넌트가 있다고 가정해 보자.

function ProfileCard() {
  const [name, setName] = useState('홍길동');       // 첫 번째 Hook
  const [age, setAge] = useState(30);             // 두 번째 Hook
  const [isAdmin, setIsAdmin] = useState(false);  // 세 번째 Hook
  
  // ...
}

리액트는 내부적으로 이 Hook들을 배열 형태로 저장하고 관리한다. 첫 번째 렌더링에서는:

  1. 첫 번째 useState: 'name' 상태를 '홍길동'으로 초기화
  2. 두 번째 useState: 'age' 상태를 30으로 초기화
  3. 세 번째 useState: 'isAdmin' 상태를 false로 초기화

다음 렌더링에서도 같은 순서로 Hook이 호출되기 때문에, 리액트는 각 Hook 호출이 어떤 상태와 연결되어 있는지 정확히 알 수 있다.

조건부 Hook 사용 시 발생하는 문제

이제 useState를 조건문 안에서 사용하면 어떤 일이 발생하는지 살펴보자.

function ProfileCard({ isAdmin }) {
  const [name, setName] = useState('홍길동');       // 항상 첫 번째 Hook
  
  if (isAdmin) {
    const [adminSince, setAdminSince] = useState('2023-01-01');  // 조건부 두 번째 Hook
  }
  
  const [age, setAge] = useState(30);             // 조건에 따라 두 번째 또는 세 번째 Hook
  
  // ...
}

첫 렌더링에서 isAdmintrue인 경우:

  1. 첫 번째 useState: 'name' 상태 초기화
  2. 두 번째 useState: 'adminSince' 상태 초기화
  3. 세 번째 useState: 'age' 상태 초기화

하지만 다음 렌더링에서 isAdminfalse로 바뀌면:

  1. 첫 번째 useState: 'name' 상태 (유지)
  2. 두 번째 useState가 호출되지 않음
  3. 두 번째(원래는 세 번째) useState: 이제 이 Hook은 이전 렌더링의 'adminSince' 상태를 참조하게 된다.

이렇게 되면 리액트는 'age' 상태를 'adminSince' 상태로 오인하게 되며, 이는 버그를 발생시킬 수 있다.

실제 예시로 살펴보기

더 구체적인 예시로 문제를 살펴보자.

function Counter({ showSpecialCounter }) {
  const [count, setCount] = useState(0);
  
  if (showSpecialCounter) {
    const [specialCount, setSpecialCount] = useState(10);
  }
  
  const [step, setStep] = useState(1);
  
  return (
    <div>
      <p>일반 카운터: {count}</p>
      <button onClick={() => setCount(count + step)}>증가</button>
      <p>스텝: {step}</p>
      <button onClick={() => setStep(step + 1)}>스텝 증가</button>
    </div>
  );
}

이 컴포넌트는 다음과 같은 시나리오에서 문제가 발생한다:

  1. 처음에 showSpecialCountertrue인 경우:
    • 첫 번째 Hook: count = 0
    • 두 번째 Hook: specialCount = 10
    • 세 번째 Hook: step = 1
  1. 사용자가 "스텝 증가" 버튼을 클릭하여 step을 2로 변경한다.
  1. 부모 컴포넌트에서 showSpecialCounterfalse로 변경한다.
  1. 다음 렌더링에서:
    • 첫 번째 Hook: count = 0 (유지)
    • 두 번째 Hook: 이제 specialCount의 Hook이 사라지고, 원래 세 번째였던 Hook이 두 번째가 된다.
    • 리액트는 두 번째 Hook에 저장된 값(원래는 specialCount의 10)을 step의 값으로 사용한다.

결과적으로 step은 사용자가 설정한 2가 아닌 10으로 표시되며, "증가" 버튼을 클릭하면 count가 1씩 증가하는 것이 아니라 10씩 증가하게 된다.

리액트 개발자 도구 경고

리액트는 이러한 문제를 방지하기 위해 개발 모드에서 Hook 규칙 위반을 감지하고 경고를 표시한다.

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

이 경고는 우리가 앞서 살펴본 문제를 방지하기 위한 것이다.

조건부 상태를 관리하는 올바른 방법

조건부로 상태를 사용해야 하는 경우, Hook을 컴포넌트 최상위 레벨에 배치하고 조건을 내부 로직에 적용하는 것이 올바른 방법이다.

function ProfileCard({ isAdmin }) {
  const [name, setName] = useState('홍길동');
  const [adminSince, setAdminSince] = useState(isAdmin ? '2023-01-01' : null);
  const [age, setAge] = useState(30);
  
  // adminSince를 조건부로 사용
  const adminSection = isAdmin && (
    <div>
      <h3>관리자 정보</h3>
      <p>관리자 등록일: {adminSince}</p>
    </div>
  );
  
  return (
    <div>
      <h2>{name}, {age}</h2>
      {adminSection}
    </div>
  );
}

또는 조건에 따라 다른 컴포넌트를 렌더링하는 방법도 있다:

function AdminProfile() {
  const [adminSince, setAdminSince] = useState('2023-01-01');
  // ...
  return <div>관리자 프로필...</div>;
}

function UserProfile() {
  // 관리자 상태 없음
  // ...
  return <div>일반 사용자 프로필...</div>;
}

function ProfileContainer({ isAdmin }) {
  return isAdmin ? <AdminProfile /> : <UserProfile />;
}

결론

리액트의 Hook은 호출 순서에 의존하여 상태를 관리하는 특성을 가지고 있다. 따라서 useState와 같은 Hook은 항상 컴포넌트의 최상위 레벨에서, 그리고 렌더링마다 동일한 순서로 호출되어야 한다.

조건문 안에서 Hook을 사용하면 렌더링마다 Hook의 호출 순서가 바뀔 수 있어 의도치 않은 상태 불일치와 버그가 발생할 수 있다. 이를 방지하기 위해서는 Hook을 최상위에 배치하고, 조건부 로직은 Hook 호출 이후에 처리하는 것이 중요하다.

profile
참 되게 살자

0개의 댓글

관련 채용 정보