React 공식 문서 읽기 (3) - Hooks Rule

Chani·2022년 7월 7일
0
post-thumbnail

0. Hook의 규칙

Hook은 함수 컴포넌트에서 사용되는 아주 강력한 도구이지만 규칙에 맞게 사용해야한다.

이번에는 Hook을 사용할때 반드시 준수해야하는 두가지 규칙에 대해 알아보려고 한다.

1. 최상위에서만 Hook을 호출해야한다.

React 공식 문서를 보면 아래와 같이 나와있다.

💡 반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하지 마세요. 대신 early return이 실행되기 전에 항상 React 함수의 최상위(at the top level)에서 Hook을 호출해야 합니다. **이 규칙을 따르면 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장됩니다.** 이러한 점은 React가 useState 와 useEffect 가 여러 번 호출되는 중에도 Hook의 상태를 올바르게 유지할 수 있도록 해줍니다.

React 함수의 최상위에서만 Hook을 호출해야하는 것은 알겠는데, 이 규칙을 따르면 컴포넌트가 렌더링 될 때만 항상 동일한 순서로 Hook이 호출되는 것이 보장된다는 것은 어떤 뜻일까?

리액트에서는 상태가 바뀌면 컴포넌트가 재렌더링이 된다. 재렌더링 되는 컴포넌트는 결국 함수이기 때문에 재실행 되는 함수가 이전 함수 내부 Scope의 상태를 이용한다는것은 실은 말이 안되는 일이다.

그런데 이러한 말이 안되는 일을 Hook이 할 수 있도록 도와주었다.

다음의 예시 코드를 한번 살펴보자.

const App = (): JSX.Element => {
  const [num, setNum] = useState<number>(0);

  useEffect(() => {
    setNum1(num1 + 1);
  }, [num]);

	const [num1, setNum1] = useState<number>(1);

  const add = () => {
    setNum(num + 1);
    setNum1(num1 + 1);
  };

  return (
    <div>
      <div id="strDiv">{"str"}</div>
      <p>{num}</p>
      <p>{num1}</p>
      <button onClick={add}>숫자 늘리기</button>
    </div>
  );
};

위의 코드는 버튼을 클릭 할 때마다 num은 1씩 증가하고, num1은 2씩 증가하는 함수인다.

이전 함수의 상태인 num, num1 이라는 상태를 사용하여 재렌더링을 해주고 있는데 어떻게 이것을 가능하게 할까?

리액트는 Hook을 호출하면 그 상태를 배열에 저장하게 된다.

위의 예시로 들어보면 아래와 같이 배열이 되어있는 것이다.

[
	0,
	()=>setNum1(num1 + 1),
	1,
]

그리고 리액트는 컴포넌트가 렌더링 될때마다 해당 배열을 참조하며(이전 상태를 참조하며) 렌더링을 해주는 것이다.

그렇기 때문에 당연하게도 같은 컴포넌트라면 같은 배열 구조를 가져야 한다.

하지만 아래와 같은 경우, 어떻게 되는지 살펴보자.

const App = (): JSX.Element => {
  const [num, setNum] = useState<number>(0);

	if(num == 0) {
		useEffect(() => {
	    setNum1(num1 + 1);
	  }, [num]);
	}

	const [num1, setNum1] = useState<number>(1);

  const add = () => {
    setNum(num + 1);
    setNum1(num1 + 1);
  };

  return (
    <div>
      <div id="strDiv">{"str"}</div>
      <p>{num}</p>
      <p>{num1}</p>
      <button onClick={add}>숫자 늘리기</button>
    </div>
  );
};

if문 안에 useEffect를 넣어주었다.

초기 상태는 다음과 같을 것이다.

[
	0,
	()=>setNum1(num1 + 1),
	1,
]

버튼을 한번 클릭하면 어떤 상황이 펼쳐지는지 같이 알아보자.

num은 이전 상태 0을 참조하여 1 증가해서 렌더링을 해주면 된다.

하지만 문제는 그 다음에 벌어진다.

num은 더이상 0이 아니기 때문에 useEffect가 실행이 되지 않게 되고 다음에 실행되는 HookuseState 는 1이 아닌 ()⇒setNum1(num + 1)을 참조하게 된다.

()⇒setNum1 함수를 1 증가시켜버려야 하는 그런 끔찍한 일이 발생할 수 있는 것이다.

마지막으로 실제 실행 결과를 살펴보면서 내용을 정리해보자.

처음 실행을 하면 다음과 같이 별다른 오류없이 실행되는 것을 확인 할 수있다.

첫 렌더링이기 때문에 아래 배열이 기준이 되는것이고 앞으로 재렌더링 될 컴포넌트는 이 배열을 따르겠다고 생각하는 것이다.

[
	0,
	()=>setNum1(num1 + 1),
	1,
]

그러나 여기에서 버튼을 누르면 어떻게 될까?

당연하게도 오류가 발생하게 된다.

이전 렌더에서는 useState 이후 useEffect를 실행했는데, 다음에 렌더 될 컴포넌트에서는 useState 다음 useEffect를 건너 뛰고 useState를 실행했다는 오류 메세지를 확인 할 수 있다.

2. 오직 React 함수 내에서만 Hook을 호출해야 한다.

이 말은 실은 생각해보면 당연하다.

Hook이 들어갈 배열은 React가 컴포넌트를 생성하는 그 시기에 생성이 될텐데 일반함수에서 Hook을 호출한다는 것은 훅이 저장될 배열도 없는데 그걸 저장한다는 뜻이므로 말이 안되는 것이다.

그렇다면 Hook이 매우 길어지더라도 하나의 컴포넌트 안에 길게 써야하는 것일까?
Hook을 재사용이 가능하도록 외부로 빼서 관리하는 방법은 없을까?
이 부분을 가능하게 하는것이 Custom Hook 이다.
Custom Hook 에 대한 내용은 다음 포스팅에서 다루어 보도록 하겠다.

profile
프론트엔드에 스며드는 중 🌊

0개의 댓글