훅의 규칙 — 왜 if 문 안에서 부르면 안 되나
Article
useState와 useEffect를 쓰면서 이런 경고를 봤을 것이다.
- React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.
규칙은 두 줄이다.
| 규칙 | 무슨 뜻인가 |
|---|---|
| 최상위에서만 부른다 | 조건문 · 반복문 · 중첩 함수 안에서 부르지 않는다 |
| React 함수 안에서만 부른다 | 컴포넌트나 다른 훅 안에서만 부른다 |
외우기는 쉽다. 왜 그런지를 알면 영원히 안 잊는다.
React 는 훅을 이름으로 기억하지 않는다
이런 컴포넌트가 있다고 하자.
function Form() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [agreed, setAgreed] = useState(false);
...
}
React는 name, email, agreed라는 이름을 모른다. 그건 우리가 구조분해로 붙인 변수명일 뿐이다. useState에 넘긴 인자는 초기값 하나뿐이었다.
그럼 세 번의 useState 호출을 어떻게 구별할까.
부른 순서로 구별한다.
- 2번 — useState(false)agreed
- 1번 — useState("")email
- 0번 — useState("")name
Hook List
첫 렌더링에서 순서대로 자리를 만들고, 다음 렌더링에서도 같은 순서로 부를 것이라 믿고 그 자리의 값을 꺼내준다.
순서가 바뀌면
조건문 안에 넣어본다.
function Form({ showEmail }) {
const [name, setName] = useState("");
if (showEmail) {
const [email, setEmail] = useState("");
}
const [agreed, setAgreed] = useState(false);
}
showEmail이 true일 때는 훅이 세 번 불린다. false가 되면 두 번만 불린다.
- 01
0번을 부른다
useState("")→ React 가 0번 자리의 값을 준다.name이다. 정상. - 02
if 를 건너뛴다
useState("")가 실행되지 않는다. - 03
다음 useState 가 1번이 되어버린다
우리는
agreed를 원했는데, React 는 1번 자리를 준다. 그건 `email` 의 값이다.const [agreed] = useState(false); // agreed 에 "" 가 들어온다 - 04
값이 한 칸씩 밀린다
boolean 자리에 문자열이 들어온다. 11강에서 index 를 key 로 썼을 때와 똑같은 종류의 사고다.
11강의 key 문제와 구조가 완전히 같다. 자리 번호로 기억하는데 내용물이 옮겨갔다.
그럼 조건부로 처리하려면
훅은 항상 부르고, 그 안에서 조건을 처리한다.
function Form({ showEmail }) {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [agreed, setAgreed] = useState(false);
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
{showEmail && <input value={email} onChange={e => setEmail(e.target.value)} />}
</form>
);
}
훅은 셋 다 부르고, 화면에 그릴지 말지를 조건으로 정한다. 7강의 조건부 렌더링이다.
useEffect도 같다. 훅 자체를 조건에 넣지 말고, 안에서 조건을 건다.
useEffect(() => {
if (!userId) return;
fetchUser(userId);
}, [userId]);
early return 과 훅
7강에서 early return을 권했는데, 훅과 만나면 함정이 된다.
function Profile({ user }) {
if (!user) return <Empty />;
const [isEditing, setIsEditing] = useState(false);
}
user가 없으면 useState가 안 불린다. 순서가 깨진다.
훅을 전부 부른 뒤에 early return한다.
function Profile({ user }) {
const [isEditing, setIsEditing] = useState(false);
if (!user) return <Empty />;
return <div>…</div>;
}
반복문 안에서도 안 된다
function List({ items }) {
items.forEach(item => {
const [selected, setSelected] = useState(false);
});
}
items의 길이가 바뀌면 훅의 개수가 바뀐다. 같은 이유로 금지된다.
이런 게 필요하다면 컴포넌트를 나눈다. 항목마다 자기 상태를 갖게 한다.
function List({ items }) {
return items.map(item => <Item key={item.id} item={item} />);
}
function Item({ item }) {
const [selected, setSelected] = useState(false);
}
8강에서 본 대로 컴포넌트마다 state가 따로 생긴다.
커스텀 훅 — 로직을 이름으로 묶는다
두 번째 규칙이 "React 함수 안에서만"이었다. 다른 훅 안도 포함된다.
이름이 use로 시작하는 함수를 만들면 그 안에서 훅을 부를 수 있다. 이걸 커스텀 훅이라 한다.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return width;
}
쓰는 쪽은 한 줄이다.
function Header() {
const width = useWindowWidth();
return <p>{width}px</p>;
}
컴포넌트가 아니다. JSX를 반환하지 않고 값을 반환한다. 화면이 아니라 로직을 재사용한다.
컴포넌트
화면을 재사용
- 대문자로 시작
- JSX 를 반환
<Button />으로 쓴다
커스텀 훅
로직을 재사용
use로 시작- 아무 값이나 반환
useWidth()로 부른다
이름을 use로 시작해야 하는 이유는 린트 도구가 알아보게 하기 위해서다. getWindowWidth라고 지으면 린트가 그냥 함수로 보고 훅 규칙을 검사하지 않는다.
언제 커스텀 훅으로 빼나
4강에서 컴포넌트를 나누는 기준을 이야기했다. 같은 감각이다.
- 01
같은 useEffect 를 두 곳에서 쓴다
복사해 붙이고 있다면 훅으로 뺀다.
- 02
이름을 붙일 수 있다
useDebounce,useLocalStorage처럼 이름이 자연스럽게 떠오르면 그건 독립된 로직이다. - 03
컴포넌트가 훅 코드로 뒤덮였다
화면을 그리는 코드보다 로직이 길다면 분리한다. 컴포넌트는 무엇을 그리는지만 보이게.
정리하면
- React는 훅을 이름이 아니라 순서로 기억한다. 이게 모든 규칙의 근거다.
- 그래서 훅은 항상 같은 순서로, 같은 횟수만큼 불려야 한다.
- 조건문·반복문·중첩 함수 안에서 부르지 않는다. 훅을 부른 뒤 안에서 조건을 건다.
- early return은 훅을 전부 부른 뒤에 한다.
- 항목마다 상태가 필요하면 컴포넌트를 나눈다.
use로 시작하는 함수를 만들면 커스텀 훅이다. 화면이 아니라 로직을 재사용한다.- 커스텀 훅은 코드를 공유할 뿐 값을 공유하지 않는다.
다음 강의에서는 렌더링과 무관한 값을 담는 useRef와, 그것으로 DOM을 직접 다루는 법을 본다.