React Hooks 규칙: useState를 조건문에서 사용하면 안 되는 이유
React Hooks는 함수형 컴포넌트에서 상태 관리와 생명주기를 다루는 혁신적인 기능입니다. 하지만 Hooks를 올바르게 사용하기 위해서는 몇 가지 중요한 규칙을 지켜야 합니다. 특히 useState를 조건문 안에서 사용하는 것은 심각한 버그를 야기할 수 있습니다.
React Hooks의 내부 동작 원리
Fiber와 Hook 연결 리스트
React는 내부적으로 Fiber라는 자료구조를 사용하여 컴포넌트를 관리합니다. 각 함수형 컴포넌트는 Hook들의 연결 리스트(Linked List)를 가지고 있으며, 이 리스트는 Hook 호출 순서에 따라 구성됩니다.
// React 내부 구조 (단순화된 버전)
function Component() {
// Hook 1: 첫 번째 useState
const [name, setName] = useState('');
// Hook 2: 두 번째 useState
const [age, setAge] = useState(0);
// Hook 3: useEffect
useEffect(() => {
// effect logic
}, []);
}
Hook 호출 순서의 중요성
React는 컴포넌트가 리렌더링될 때마다 Hook 연결 리스트를 순서대로 순회하면서 각 Hook의 상태를 복원합니다. 이때 Hook의 호출 순서가 변경되면 상태와 Hook이 잘못 매핑되어 예측할 수 없는 동작이 발생합니다.
문제 상황: 조건부 Hook 사용
잘못된 예시
function UserProfile({ isLoggedIn }) {
// ❌ 조건문 안에서 useState 사용
if (isLoggedIn) {
const [userInfo, setUserInfo] = useState(null);
}
const [theme, setTheme] = useState('light');
return <div>User Profile</div>;
}
문제가 발생하는 과정
첫 번째 렌더링 (isLoggedIn = true):
Hook 1: userInfo state
Hook 2: theme state
두 번째 렌더링 (isLoggedIn = false):
Hook 1: theme state (잘못된 매핑!)
이때 theme state가 Hook 1 위치로 이동하면서 이전에 userInfo가 가지고 있던 값을 받게 되어 오류가 발생합니다.
실제 에러 메시지
React Hook "useState" is called conditionally.
React Hooks must be called in the exact same order every time.
Hooks의 규칙 (Rules of Hooks)
React에서 정의한 Hooks 사용 규칙은 다음과 같습니다:
1. 최상위에서만 호출
function Component() {
// ✅ 올바른 사용
const [state1, setState1] = useState(initialValue);
const [state2, setState2] = useState(initialValue);
// ❌ 잘못된 사용들
if (condition) {
const [state3, setState3] = useState(initialValue);
}
for (let i = 0; i < count; i++) {
const [state4, setState4] = useState(initialValue);
}
function eventHandler() {
const [state5, setState5] = useState(initialValue);
}
}
2. React 함수에서만 호출
// ✅ 함수형 컴포넌트에서 사용
function MyComponent() {
const [state, setState] = useState(0);
}
// ✅ 커스텀 Hook에서 사용
function useCustomHook() {
const [state, setState] = useState(0);
return [state, setState];
}
// ❌ 일반 JavaScript 함수에서 사용
function regularFunction() {
const [state, setState] = useState(0); // 에러!
}
조건부 로직의 올바른 처리 방법
1. 조건부 상태 초기화
function UserProfile({ isLoggedIn }) {
// ✅ 항상 Hook을 호출하고, 초기값으로 조건 처리
const [userInfo, setUserInfo] = useState(isLoggedIn ? null : undefined);
const [theme, setTheme] = useState('light');
useEffect(() => {
if (isLoggedIn && userInfo === null) {
// 사용자 정보 로드
fetchUserInfo().then(setUserInfo);
}
}, [isLoggedIn, userInfo]);
return <div>User Profile</div>;
}
2. 조건부 컴포넌트 분리
// ✅ 컴포넌트를 분리하여 각각에서 Hook 사용
function LoggedInProfile() {
const [userInfo, setUserInfo] = useState(null);
// 로그인된 사용자 로직
return <div>Logged In Content</div>;
}
function GuestProfile() {
const [guestPrefs, setGuestPrefs] = useState({});
// 게스트 사용자 로직
return <div>Guest Content</div>;
}
function UserProfile({ isLoggedIn }) {
const [theme, setTheme] = useState('light');
return (
<div>
{isLoggedIn ? <LoggedInProfile /> : <GuestProfile />}
</div>
);
}
3. 커스텀 Hook 활용
// ✅ 조건부 로직을 커스텀 Hook으로 분리
function useUserData(isLoggedIn) {
const [userInfo, setUserInfo] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (isLoggedIn) {
setLoading(true);
fetchUserInfo()
.then(setUserInfo)
.finally(() => setLoading(false));
} else {
setUserInfo(null);
}
}, [isLoggedIn]);
return { userInfo, loading };
}
function UserProfile({ isLoggedIn }) {
const { userInfo, loading } = useUserData(isLoggedIn);
const [theme, setTheme] = useState('light');
return <div>User Profile</div>;
}
실무에서 자주 발생하는 실수와 해결법
1. 동적 Hook 개수
// ❌ 배열 길이에 따라 Hook 개수가 변함
function DynamicForm({ fields }) {
const states = fields.map(field => useState('')); // 에러!
return <form>...</form>;
}
// ✅ 단일 객체 상태로 관리
function DynamicForm({ fields }) {
const [formData, setFormData] = useState({});
const updateField = (fieldName, value) => {
setFormData(prev => ({ ...prev, [fieldName]: value }));
};
return <form>...</form>;
}
2. 조건부 useEffect
// ❌ 조건문 안에서 useEffect 사용
function Component({ shouldTrack }) {
if (shouldTrack) {
useEffect(() => {
// 트래킹 로직
}, []);
}
}
// ✅ useEffect 내부에서 조건 처리
function Component({ shouldTrack }) {
useEffect(() => {
if (shouldTrack) {
// 트래킹 로직
}
}, [shouldTrack]);
}
디버깅과 도구
ESLint Plugin 활용
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
React DevTools
React DevTools를 사용하면 Hook의 상태와 순서를 시각적으로 확인할 수 있습니다. 주의할 점은 개발 환경에서 Hook 규칙 위반을 조기에 발견하는 것이 중요하다는 것입니다.
결론
React Hooks의 규칙을 지키는 것은 안정적인 React 애플리케이션을 만드는 기본 조건입니다. 특히 useState를 조건문에서 사용하지 않는 것은 예측 가능한 상태 관리를 위해 반드시 지켜야 할 규칙입니다.
핵심 원칙:
- Hook은 항상 컴포넌트 최상위에서 호출
- 조건부 로직은 Hook 내부에서 처리
- 복잡한 조건부 로직은 컴포넌트 분리나 커스텀 Hook 활용
- ESLint 규칙을 통한 자동 검증
올바른 Hook 사용 패턴을 익히면 React의 강력한 기능을 안전하게 활용할 수 있습니다.