‘왜’ 규칙이 필요한지에 대한 공감대가 형성되자, 리액트 팀은 첫 번째 규칙을 명문화하는 작업에 들어갔다. 그것은 훅의 가장 핵심적인 제약을 다루는, 가장 중요한 규칙이었다.
댄이 화이트보드에 큰 글씨로 첫 번째 규칙을 적었다.
규칙 1: 오직 최상위(Top Level)에서만 훅을 호출해야 합니다.
그는 부연 설명을 덧붙였다.
“이는 곧, 반복문(loops), 조건문(conditions), 또는 중첩된 함수(nested functions) 안에서 훅을 호출해서는 안 된다는 의미입니다.”
이 규칙은 댄이 방금 설명했던 ‘호출 순서의 불변성’을 지키기 위한 직접적인 해답이었다. 컴포넌트 함수의 최상위 스코프에 훅을 순서대로 나열함으로써, 개발자는 어떤 렌더링에서도 훅의 호출 순서가 동일하게 유지됨을 보장할 수 있었다.
한 개발자가 질문했다.
“하지만 조건부로 부수 효과를 실행하고 싶을 때가 분명히 있습니다. 예를 들어, 특정 조건이 만족될 때만 API를 호출하고 싶다면요? useEffect
를 if
문 안에 넣을 수 없다면 어떻게 해야 하죠?”
실무에서 흔히 마주칠 수 있는 합리적인 질문이었다.
댄은 미소를 지으며 답했다.
“아주 좋은 질문입니다. 규칙은 우리를 제한하는 것이 아니라, 더 나은 패턴으로 이끌어주기 위해 존재합니다. 해답은 useEffect
의 안쪽에 조건문을 넣는 것입니다.”
그는 예시 코드를 작성했다.
잘못된 코드 (규칙 위반):
function UserProfile({ id, needsFetch }) {
// ERROR: 조건문 안에서 훅을 호출!
if (needsFetch) {
useEffect(() => {
fetchUser(id);
}, [id]);
}
// ...
}
올바른 코드 (규칙 준수):
function UserProfile({ id, needsFetch }) {
// OK: 훅은 항상 최상위에서 호출한다.
useEffect(() => {
// 조건은 훅의 내부에 둔다.
if (needsFetch) {
fetchUser(id);
}
}, [id, needsFetch]); // needsFetch도 의존성에 추가한다.
}
차이는 명확했다.
훅 자체의 호출은 조건 없이 항상 최상위에서 이루어졌다. 조건부 로직은 훅의 ‘내부’로 옮겨졌다. 이렇게 함으로써 useEffect
의 호출 순서는 항상 일정하게 유지되었고, 리액트는 혼란에 빠지지 않았다.
“만약 조건에 따라 아무 일도 하고 싶지 않다면, 그냥 if
문 안에서 return
해버리면 됩니다.”
댄이 덧붙였다. “중요한 것은 훅을 호출하는 행위 자체를 건너뛰지 않는 것입니다.”
세바스티안이 사용했던 은행 창구 비유가 팀원들의 머릿속에 다시 떠올랐다.
“매일 은행에 와서, 기분에 따라 두 번째 창구를 건너뛰고 바로 세 번째 창구로 갈 수는 없습니다. 일단 순서대로 1, 2, 3번 번호표를 모두 뽑고, 2번 창구에 가서는 ‘오늘은 할 일이 없네요’라고 말하고 그냥 지나가야 하는 것과 같군요.”
이 비유는 규칙의 본질을 완벽하게 설명했다.
‘최상위에서만 호출하라.’
이것은 훅의 내부 메커니즘인 ‘배열과 인덱스’가 올바르게 작동하기 위한 최소한의, 그리고 절대적인 약속이었다.
이 규칙을 통해, 개발자는 훅을 사용함으로써 얻는 편리함의 대가로, 코드 구조에 대한 약간의 제약을 받아들여야 했다. 하지만 팀은 이것이 충분히 치를 만한 가치가 있는 거래라고 확신했다. 클래스의 복잡성과 비교하면, 이것은 사소한 불편함에 지나지 않았다.
첫 번째 규칙이 확립되었다.
이제 남은 것은 두 번째 규칙이었다. 훅은 대체 ‘어디에서’ 호출될 수 있는가에 대한, 정체성과 관련된 질문이었다.