훅의 규칙은 두 가지입니다. 훅은 컴포넌트의 최상위에서만 호출하고, 조건문, 반복문, 중첩 함수 안이나 조기 return 뒤에서는 절대 호출하지 않습니다. 그리고 함수 컴포넌트나 커스텀 훅에서만 호출합니다. 이 규칙이 있는 이유는 React가 호출 순서로 훅을 구별하므로 그 순서가 렌더링마다 같아야 하기 때문입니다.
React는 컴포넌트의 훅 값을 그 컴포넌트에 붙은 목록에 저장합니다. 렌더링할 때마다 목록을 차례로 훑습니다. 첫 번째 훅 호출은 첫 번째 칸을, 두 번째 호출은 두 번째 칸을 받는 식입니다. 이름은 없고 위치만 있습니다. 아래 예제는 그 개념을 일반 자바스크립트 몇 줄로 흉내 낸 장난감 버전이며, React의 실제 코드가 아니므로 안심하고 망가뜨려 볼 수 있습니다.
첫 렌더링은 세 칸을 채웁니다: Ada, Countess, 36. "Render without nickname"을 클릭하면 조건부 호출을 건너뛰므로 age 호출이 1번 칸을 차지하고 Countess를 읽습니다. 실제 React는 어떤 경우에는 이 불일치를 알아채고 오류를 던지지만, 어떤 경우에는 엉뚱한 값을 엉뚱한 훅에 넘기며, 이쪽이 더 나쁩니다. toyUseState('Countess') 호출을 if 위로 옮기면 두 버튼 모두 올바른 나이를 보여 줍니다.
두 규칙을 코드로 보기
function Profile({ user }) {
// Good: always called, always in this order
const [tab, setTab] = useState('posts');
const theme = useContext(ThemeContext);
useEffect(() => {
document.title = user.name;
}, [user.name]);
// Bad: inside a condition, a loop or a nested function
if (user.isAdmin) {
const [log, setLog] = useState([]); // breaks the order
}
}
항상 같은 순서로 도달하는 훅 호출은 본문 어디에 있든 괜찮습니다. 단, 조기 return보다 앞에 있어야 합니다.
조건부로 호출한 훅 고치기
린터는 이것을 React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render.로 보고합니다. 해결 방법은 항상 같습니다. 훅은 매번 호출하고, 조건은 훅 안에 넣으세요.
// Broken: the effect exists on some renders and not others
function Notifications({ user }) {
if (user) {
useEffect(() => {
console.log('subscribe for', user.name);
}, [user]);
}
return null;
}
이펙트는 렌더링마다 선언되고, 그 안의 if가 무언가를 할지 결정합니다. 로그인과 로그아웃을 몇 번 해 보면 Console에 로그인마다 subscribe가, 로그아웃마다 unsubscribe가 표시됩니다.
조기 반환 뒤의 훅
조기 return도 조건입니다. 이 코드는 loading이 처음 false가 될 때 "Rendered more hooks than during the previous render"를 일으킵니다. 그 렌더링이 이전 렌더링에서 한 번도 호출하지 않은 훅에 도달하기 때문입니다. 렌더링이 이전보다 적은 훅을 호출하면 메시지는 "Rendered fewer hooks than expected"입니다.
// Broken: useState is skipped while loading
function Comments({ loading, comments }) {
if (loading) return <p>Loading...</p>;
const [expanded, setExpanded] = useState(false);
return <List items={comments} expanded={expanded} />;
}
모든 훅이 return 위에 있으므로 두 분기 모두 같은 훅을 호출합니다. "Toggle loading"을 클릭하고, 목록을 펼친 다음, loading을 두 번 토글해 보세요. state를 한 번도 건너뛰지 않았으므로 목록은 펼쳐져 있었다는 것을 기억합니다.
반복문 안의 훅
항목 수가 바뀌기 때문에 반복문에서 항목마다 useState를 한 번씩 호출할 수는 없습니다. 각 항목에 자기만의 컴포넌트를 주고 거기서 훅을 호출하세요. 각 인스턴스는 자기만의 칸을 가집니다.
"Invalid hook call"
메시지는 "Invalid hook call. Hooks can only be called inside of the body of a function component."로 시작합니다. 흔한 원인은 세 가지입니다.
- React가 함수 컴포넌트를 렌더링하지 않는 동안 훅이 실행됩니다. 이벤트 핸들러, 이펙트 콜백 안, 클래스 컴포넌트, 모듈 최상위가 그런 곳입니다. 훅을 컴포넌트 본문이나 커스텀 훅으로 옮기고, 커스텀 훅을 컴포넌트에서 호출하세요.
react와react-dom의 버전이 일치하지 않습니다. 둘 다 같은 버전을 설치하세요.- React 사본이 두 개 있습니다. 링크한 로컬 패키지나 자체 React를 번들링한 라이브러리가 두 번째 사본을 가져오며, 한 사본의 훅은 다른 사본의 렌더러와 함께 동작하지 않습니다.
npm ls react를 실행해서 버전이 하나만 나타나는지 확인하세요.
npm ls react
여기 미리보기는 이런 메시지를 출력하지 않는 프로덕션 빌드처럼 실행됩니다. 이벤트 핸들러에서 호출한 훅은 "Cannot read properties of null" 같은 일반 TypeError로 실패하고, React 자체의 오류는 짧은 "Minified React error" 코드로 나옵니다. 전체 문구를 읽으려면 앱을 개발 모드로 실행하세요.
린터가 규칙을 지키게 하기
eslint-plugin-react-hooks는 입력하는 동안 두 규칙을 검사하고, 이펙트의 의존성 배열도 검사합니다. Vite의 React 템플릿과 Next.js의 ESLint 설정에 포함되어 있습니다. flat ESLint 설정에서는 권장 프리셋을 켜세요.
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([reactHooks.configs.flat.recommended]);
핵심 규칙 두 개는 react-hooks/rules-of-hooks(오류)와 react-hooks/exhaustive-deps(경고)입니다. 버전 7부터 이 프리셋은 React Compiler에서 온 규칙도 켭니다. 순수하지 않은 렌더링과 렌더링 중의 ref 읽기를 잡아내는 react-hooks/purity, react-hooks/refs 같은 것들입니다. 규칙을 하나씩 고르려면 플러그인을 plugins: { 'react-hooks': reactHooks }로 등록하고 원하는 규칙을 나열하세요.
플러그인은 이름으로 훅을 찾습니다. 이름이 use 뒤에 대문자로 이어지는 함수는 모두 훅으로 취급되며, 컴포넌트(대문자로 시작하는 이름)와 다른 use 함수만 그것을 호출할 수 있습니다. 그래서 커스텀 훅에는 반드시 이 접두사를 붙여야 합니다.
예외: use
React 19에 추가된 use는 프로미스나 컨텍스트를 읽으며, 다른 모든 훅과 달리 조건문이나 반복문 안에서 호출할 수 있습니다. 그래도 렌더링 중에, 컴포넌트나 훅 안에서 호출해야 합니다.
테마를 바꾸면 테마가 적용된 제목만 색이 바뀝니다. if 안에서 useContext로 같은 일을 해 보면 린터가 경고합니다. use 훅 페이지에서 이것으로 프로미스를 읽는 법을 다룹니다.
자주 묻는 질문
훅의 규칙이란 무엇인가요?
두 가지 규칙입니다. 훅은 함수 컴포넌트나 커스텀 훅의 최상위에서만 호출하고, 조건문, 반복문, 중첩 함수 안이나 조기 반환 뒤에서는 절대 호출하지 마세요. 그리고 훅은 일반 함수나 클래스 컴포넌트가 아니라 함수 컴포넌트나 커스텀 훅에서만 호출하세요.
훅을 조건부로 호출할 수 없는 이유는 무엇인가요?
React는 훅을 이름으로 알지 못합니다. 값을 목록에 저장하고 호출이 일어나는 순서대로 돌려줍니다. 조건 때문에 호출 하나를 건너뛰면, 그 뒤의 모든 훅이 바로 앞 훅의 값을 읽게 됩니다.
"Rendered more hooks than during the previous render"는 무슨 뜻인가요?
어떤 렌더링이 이전 렌더링보다 더 많은 훅을 호출했다는 뜻입니다. 보통 훅이 if 안이나, 이번에는 다르게 실행된 조기 return 뒤에 있기 때문입니다. 모든 훅을 조건 위로 옮기고, 대신 조건을 훅 안에 넣으세요.
"Invalid hook call"은 어떻게 고치나요?
세 가지를 확인하세요. React가 함수 컴포넌트를 렌더링하는 동안 훅이 실행되는지(이벤트 핸들러, 이펙트 콜백, 클래스 컴포넌트, 모듈 최상위가 아닌지), react와 react-dom의 버전이 일치하는지, 그리고 앱에 React 사본이 하나뿐인지(npm ls react로 확인)입니다.
use 훅은 조건부로 호출할 수 있나요?
네. use(React 19)는 유일한 예외로, if 블록과 반복문 안에서 호출할 수 있습니다. 그래도 이벤트 핸들러가 아니라 컴포넌트나 훅 안에서 호출해야 합니다.