본문 바로가기
카테고리 없음

[React] 오픈소스 살펴보기_Hook 객체의 탄생

by 2__50 2023. 12. 6.
공부한 내용을 정리한 글입니다 
내용에 오류가 있거나 더 좋은 의견이 있다면 댓글로 남겨주세요.
배움에 큰 도움이 됩니다. 🖋

 

 

Hook 객체의 탄생


응애 / https://in.pinterest.com/pin/852376666953544799/

 

 

지난 시간에는 개발자가 hook 함수를 호출했을 때 react가 그 hook의 구현체를 찾는 여정을 따라가보았다.
오늘은 대표적인 hook useState()를 통해 hook 객체의 탄생 과정을 살펴보자.

 

// reconciler > ReactFiberHooks.js


// 인자로 받은 initialState는 함수형 컴포넌트에서 해당 상태의 최초값이다.
function mountState(initialState) {
  // 새로운 hook 객체를 생성하고 현재 처리 중인 hook 리스트에 추가한다.
  const hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    // initialState가함수일 경우, 실행한 결과를 새로운 초기 state로 사용한다.
    initialState = initialState()
  }
  hook.memoizedState = hook.baseState = initialState
  ...
  // 생성한 hook 반환
  return hook;
}

 

 

mountState()는 컴포넌트가 처음 마운트 될 때 호출 되는 함수다.
이 함수는 mountWorkInProgressHook() 함수를 통해 hook 객체를 생성하고 초기값을 설정한다.

 

 

// reconciler > ReactFiberHooks.js


function mountWorkInProgressHook(): Hook {
  // 1. 새로운 hook 객체를 생성해 초기값을 설정하고
  const hook: Hook = {
    memoizedState: null, // 컴포넌트에 적용된 마지막 상태 값
    queue: null, // hook이 호출될 때마다 update를 연결 리스트로 queue에 집어넣는다.
    next: null, // 다음 hook을 가리키는 포인터

    baseState: null,
    baseUpdate: null,
  }

// 2. 해당 hook을 현재 처리 중인 hook 리스트에 추가한다.
  if (workInProgressHook === null) {
    // 맨 처음 실행되는 hook인 경우 연결 리스트의 head로 잡아두고
    firstWorkInProgressHook = workInProgressHook = hook
  } else {
    // 두번 째부터는 연결 리스트에 추가한다
    workInProgressHook = workInProgressHook.next = hook
  }
  // 생성한 hook 반환
  return workInProgressHook
}

 

 

mountWorkInProgressHook() 함수는 새로운 hook 객체를 생성해 초기값을 설정하고, 해당 hook을 현재 처리 중인 hook 리스트에 추가하는 역할을 한다.

 

React에서는 Hook을 관리하기 위해 연결 리스트라는 자료 구조를 사용한다. 연결 리스트는 각 노드가 데이터, 그리고 다음 노드를 가리키는 포인터를 가진 구조인데, 배열에 비해 리스트의 탐색 흐름을 쉽게 제어할 수 있고, 노드의 추가/삭제나 리스트 병합이 효율적인 것이 장점이다.

 

 

https://medium.com/hackernoon/the-little-guide-of-linked-list-in-javascript-9daf89b63b54

 

 

[ hook에서 head와 tail 역할을 하는 요소들 ]

firstWorkInProgressHook
- Hook 연결 리스트의 첫 번째 요소(head)다
- 컴포넌트 실행이 끝나면 이 요소는 fiber에 저장되어 컴포넌트와 Hook 리스트를 연결해준다

workInProgressHook
- 현재 처리되고 있는 hook을 나타낸다.
- 동시에 리스트의 마지막 요소(tail) 포인터 역할을 한다


* Fiber는 react의 핵심 알고리즘 "Fiber Reconciliation"에서 사용되는 개념으로, 작업 단위를 나타낸다.
Fiber는 JS 객체로, 컴포넌트와 그 상태, 그에 대한 다양한 정보를 포함하며 이를 통해 react는 컴포넌트 트리의 업데이트를 효과적으로 관리할 수 있다.

 

 

각 hook은 호출한 순서에 따라 연결 리스트에 추가되기에 컴포넌트 내에서 여러 hook의 상태가 독립적으로 관리될 수 있다. 이것은 함수형 컴포넌트가 상태를 유연하게 관리하고, 렌더링 동안 일관된 동작을 하는 데 중요한  한다.

 

 

import React, { useState, useEffect } from "react";

const Profile = () => {
  const [age, setAge] = useState(0); // 연결리스트의 첫 번째 노드
  const [isVisible, setIsVisible] = useState(false);// 두 번째 노드

  useEffect(() => { // 세 번째 노드
    ...
  });

  return (
    <div>
      <p>나이 : {age}세</p>
      {isVisible && (
      	<div>...</div>
      )}
      ...
    </div>
  );
}

export default Profile;

 

 

 

예를 들어, 위의 코드에서 연결리스트는 호출한 순서에 따라  useState(0) -> useState(false) -> useEffect(...) 순으로 생성된다.

 

만약 두번째 hook인 'useState(false)'가 특정 조건일 때는 호출되지 않는다면,

첫 번째 렌더링에서는 useState(0) -> useState(false) -> useEffect(...) 순서로 연결리스트가 생성되지만 두 번째 렌더링에서는 조건에 따라 useState(0) -> useEffect(...)로 변경될 수 있다. 그렇게 되면 react는 'useEffect(...)'를  두 번째 hook으로 인식하게 되며 이전 렌더링에서 'useState(false)'에 연결된 상태 정보를 잘못 사용하게 된다.

 

이런 이유로 hook은 항상 컴포넌트의 최상위 레벨에서 호출되어야 하며, 반복문, 조건문, 중첩된 함수 내에서 호출하면 안 되는 것이다. 그렇게 되면 react에서 컴포넌트 상태를 제대로 관리하기 어렵기 때문이다.

 

이제야 내가 그 동안 받은 'useState' is called conditionally 경고장이 이해된다.

 

 

https://bobbyhadz.com/blog/react-hook-usestate-called-conditionally

 



업데이트를 담는 queue


// reconciler > ReactFiberHooks.js

// mountState 함수: 초기 상태와 함께 호출되는 Hook 함수
function mountState(initialState) {
  // queue 객체 생성 및 초기화
  const queue = (hook.queue = {
    last: null, // 마지막 update
    dispatch: null, // push 함수

    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  });

  // dispatch 함수 생성 및 할당
  const dispatch = (queue.dispatch = dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue
  ));

  // 초기 state, dispatch 함수 배열로 반환
  return [hook.memoizedState, dispatch];
}

// basicStateReducer 함수: 기본적인 상태 갱신 로직을 수행하는 리듀서
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

 

 

 

다시 mountState() 함수로 돌아와 이번에는 hook의 업데이트를 살펴볼 것이다.
Hook에는 queue라는 속성이 있는데, queue는 컴포넌트의 상태 업데이트 정보를 저장하는 곳이다.

 

 

const Profile = () => {
  const [name, setName] = React.useState("");
  const [age, setAge] = React.useState(0);

  React.useEffect(() => {
    fetch("/api/user")
      .then((response) => response.json())
      .then((data) => {
        setName(data.name);
        setAge(data.age);
      });
  }, []);

  return (
    <div>
      <h1>User Profile</h1>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  );
}

export default Profile

 

 

이 컴포넌트는 name과 age라는 두 개의 상태를 관리하고 있다. 각 상태는 각각의 useState hook에 의해 관리되며, 호출 순서에 따라 연결 리스트에 노드로 추가된다. 각 hook은 상태 업데이트 정보를 자신의 queue에 저장하고, 컴포넌트가 리렌더링될 때 queue에 저장된 업데이트들이 순서대로 실행되어 최종 상태를 결정한다.

 

반면에, 하나의 setState 함수가 여러 번 호출되면 어떻게 될까?
아래의 또다른 Profile 컴포넌트에서는 setProfile 함수가 두 번 호출된다.

 

 

const Profile = () => {
  const [profile, setProfile] = React.useState({
    name: '',
    age: 0,
  });

  const handleUpdateProfile = () => {
    // setState()가 여러 번 호출되고 있다.
    setProfile((prevProfile) => ({ ...prevProfile, name: '토니' }));
    setProfile((prevProfile) => ({ ...prevProfile, age: '20' }));
  };

  return (
    <div>
      <h1>User Profile</h1>
      <p>Name: {profile.name}</p>
      <button onClick={handleUpdateProfile}>Update Profile</button>
    </div>
  );
}

export default Profile

 

 

 

이 경우 setProfile() 함수가 호출될 때마다 상태 업데이트 정보가 해당 hook 객체의 queue에 연결 리스트 형태로 저장된다. 저장된 업데이트들은 컴포넌트가 리렌더링될 때 순서대로 실행되며 profile 상태를 최종적으로 결정한다. 이 방식으로, react는 여러 번의 상태 업데이트를 효율적으로 관리한다.

 

 

정리하기


React에서 hook 객체는 컴포넌트가 처음 마운트될 때 mountState() 함수를 통해 생성된다.
각 hook은 호출된 순서대로 연결리스트에 추가돼 순서가 보장되고, 각 hook은 자신의 업데이트된 상태를 queue에 저장한다.
hook의 호출 순서가 항상 일정하지 않으면 상태 관리에 문제가 발생할 수 있기 때문에 hook의 호출 순서는 언제나 동일하게 보장되어야 한다.

setState() 함수가 여러 번 호출되면 각 호출은 해당 hook의 queue에 저장되고 이 정보들은 리렌더링 시 순차적으로 실행되며 상태를 최종 결정한다.

 

 

 

참고


https://goidle.github.io/react/in-depth-react-hooks_1/

 

React 톺아보기 - 03. Hooks_1 | Deep Dive Magic Code

모든 설명은 v16.12.0 버전 함수형 컴포넌트와 브라우저 환경을 기준으로 합니다. 버전에 따라 코드는 변경될 수 있으며 클래스 컴포넌트는 설명에서 제외됨을 알려 드립니다. 각 포스트의 주제는

goidle.github.io

 

댓글