React hook은 왜 컴포넌트 최상단에서만 동작할까? (부제: react hook 규칙)

민순기·2023년 4월 20일
38
post-thumbnail

이 주제를 포스팅하는 이유

React를 조금만이라도 공부해본 사람이 위 코드를 보게 되면 틀림없이 잘못된 코드라고 말할겁니다.

그 사람에게 저 코드가 왜 잘못됐냐고 물어보면 뭐라고 대답할까요? 장담은 못하겠지만 아마 많은 사람들이 "React에서 그렇게 쓰지 말래" 라고 말할것 같습니다.
같은 사람에게 "왜 React에서 저렇게 쓰지 말래?" 라고 물어보면 이번에도 장담은 못하겠지만 아마도 "오... 그거까지는 모르겠는데...?" 라고 대답할것 같습니다.

저도 최근에 같은 질문을 받은 후 마찬가지의 대답을 했고 그것이 이번 포스팅을 하게 된 이유입니다. 혹시라도 이 글을 보고 계신 여러분께 다시 한번 물어보겠습니다. "왜 React에서 저렇게 쓰지 말라고 할까요?" 이 질문에 대한 대답을 할 수 있는 분은 이글을 보지 않으셔도 좋습니다.


React hook은 왜 컴포넌트 최상단에서 동작해야 할까요?

위 코드를 올바르게 고치면 이렇게 동작할 것입니다. 즉 hook이 조건부로 발생한다면, 조건문 안에서 hook을 호출하는것이 아니라 hook 내부에서 조건에 따른 분기처를 해야 하는것이죠. hook을 사용할때에는 항상 이렇게 컴포넌트의 최상단에서 호출하는 규칙을 지켜야 합니다. React 공식 문서에서는 해당 규칙을 이렇게 설명합니다.

반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하지 마세요. 대신 early return이 실행되기 전에 항상 React 함수의 최상위(at the top level)에서 Hook을 호출해야 합니다. 이 규칙을 따르면 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장됩니다. 이러한 점은 React가 useState 와 useEffect 가 여러 번 호출되는 중에도 Hook의 상태를 올바르게 유지할 수 있도록 해줍니다. 이 점에 대해서 궁금하다면 아래에서 자세히 설명해 드리겠습니다.
https://ko.reactjs.org/docs/hooks-rules.html

설명에서 말한 아래의 내용을 간단하게 설명하면 아래와 같습니다.

  • React가 hook이 호출되는 순서에 의존하고, 그로 인해 어떤 상태가 어떤 useState에서 호출되는지 알 수 있다.
  • hook이 조건문이나 반복문 등에서 호출되면 호출 순서를 건너뛸 수 있다.
  • 호출 순서가 달라지면 어떤 상태가 어떤 useState에서 호출되는지 알 수 없고 이로 인해 버그가 생긴다.

위 내용을 저는 런타임에서 hook의 호출 순서가 동적으로 바뀌면 안된다. 라고 이해했습니다.

사실 여기까지의 내용은 React의 공식 문서만 좀 읽어보면 알 수 있는 내용입니다.
하지만 저는 React에서 hook의 호출 순서를 어떤식으로 관리하고 보장하는가 이 부분이 궁금했습니다. 이걸 알기 위해서는 hook의 실체, 즉 hook이 어떻게 구현되어있는지 그 구현체를 봐야합니다. 이제 한번 그 실체를 들여다 봅시다.


useState는 이렇게 생겼습니다.

위 코드는 react에서 useState를 구현한 부분입니다. mountState라는 함수가 눈에 띄네요. 그럼 mountState는 어떻게 생겼을까요?


mountState는 state와 dispatch 함수를 반환하는 함수입니다. const [state, setState] = useState(); 에서 state와 setState가 각각 hook.memoizedState 그리고 dispatch 함수인거죠.

하지만 우리가 봐야하는 부분은 이곳이 아닌것 같습니다. 이쪽 코드를 한번 봐볼까요?

아래에서 이 hook이라는 녀석의 프라퍼티를 바꾸는 코드도 있습니다. 그럼 이 hook이라는 녀석이 어떤 놈인지 확인해봐야 할 차례인것 같습니다.
mountWorkInProgressHook이라는 이름에서 왠지 현재 프로세스를 진행중인 hook에 관련된 함수라는 느낌이 옵니다.


위 코드는 함수형 컴포넌트에서 hook 객체를 생성하는 함수입니다. 그러니까 특정 hook이 처음 만들어질때를 의미하는거죠.
hook객체를 업데이트하는 함수도 있습니다. updateWorkInProgressHook 함수가 바로 그 역할을 담당합니다.
(제가 가져온 코드 사진에서는 updateWorkInProgressHook이 실행되는 부분이 빠져있지만 react에서 useState를 선언한 다른 코드 부분에는 있습니다. 이 점 양해 부탁드립니다.)

두 함수 모두 반환하는 값은 workInProgressHook이라는 Hook 객체입니다. 다만 과정의 차이가 있습니다.

mountWorkInProgressHook 함수는 workInProgressHook에 초기값을 할당하는 과정이 있습니다.

updateWorkInProgressHook함수는 workInProgressHookworkInProgressHook.next에 다음에 실행될 실행중인 hook을 할당하는 과정이 있습니다. 아래 사진은 그 과정을 간단하게 나타낸 코드입니다.

이를 통해 알 수 있는 점은 React의 Hook들은 서로 연결되어 있는 구조를 띄고 있다는 점입니다. 예를 들어 useState 다음에 useEffect를 실행할 경우 useState의 hook 객체에서는 다음에 실행될 useEffect hook 객체를 가지고 있습니다. React는 이 구조를 통해 hook의 호출 순서를 보장합니다.

이러한 구조로 인해 런타임에서 호출 순서가 동적으로 바뀌는 경우. 즉 조건문이나 반복문 안에서 hook을 호출한 경우 런타임 에러가 발생하는 것입니다.


마무리

이번 포스팅에서는 React hook의 호출 순서에 대한 규칙과 왜 그 규칙을 지켜야 하는지 알아봤습니다.
일 핑계로 포스팅을 안올리다가 오랜만에 포스팅을 작성하는데, 공부하고 글을 작성하면서 너무 재미있었네요.
종종 재밌는 주제로 포스팅을 해야겠다는 생각이 많이드는 포스팅이었습니다.

글에 틀리거나 부족한 내용이 있다면 피드백은 언제든지 환영입니다~~!

참고 문헌
https://dev.to/wuz/linked-lists-in-the-wild-react-hooks-3ep8

profile
2년차 FE 개발자 민순기입니다.

6개의 댓글

comment-user-thumbnail
2023년 4월 24일

오랜만에 영양가있는 게시물을 보네요! 앞으로도 이런식의 글 많이 써주셨으면 좋겠습니다.
잘 보고 갑니다.

1개의 답글
comment-user-thumbnail
2023년 4월 28일

hooks는 useXXXX를 호출하는 순서대로 배열에 추가됩니다.
그리고 다음번 component build에서는 다시 만들지 않고, 만들어진 순서대로 그 element를 재사용하는 방식입니다. 그래서 처음 만들때 조건부로 만들면 다음 build에서 순서가 꼬일수 있고.. 그럼 엉뚱한 element를 이용하게 되어 망하게 되는거죠. 그래서 이론적으로는 새로 추가는 할 수는 있어요.

원문이긴 한데 유명한 post입니다.
https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e

1개의 답글
comment-user-thumbnail
2023년 4월 30일

잘봤어요. 그냥 무의식적으로 사용하고 있었는데 좋은 글이었습니다.

답글 달기
comment-user-thumbnail
2023년 9월 4일

머시따

답글 달기