TypeScript로 React hook 구현해봄

nyoung·2024년 4월 3일
0

React의 setState 작동 원리를 알아보고 싶은 사람들에게 추천한다.

React의 hook은 어떤 식으로 이뤄져 있을까.
React Deep Dive 책을 공부하면서 useState에 대한 설명이 나와있었는데, JS코드로 대략적으로 구현한 부분에서 착안해 여러 블로그를 보면서 React hook을 아주 간략하게 구현해보았다.

먼저, React에서 createRoot 부분을 보자.
React는 index.js 에서 이런식으로 root를 만들어준다.

import { createRoot } from 'react-dom/client';
import App from './App.js';
import './styles.css';

createRoot(document.getElementById('root'))!.render(<App />);

비슷하게, main.ts에서 이런식으로 작성해줬다. 여기에서 $가 앞에 붙은 것은 DOM요소를 나타낸 것이다.

  • main.ts
const $root = document.querySelector<HTMLDivElement>('#app')!
React.createRoot($root).render(Component)

Component는 아래와 같이 구현했다. html요소들로 이뤄진 문자열 리터럴을 반환한다. (원래는 객체이고 React 내에서 요소를 만들어줘야 하지만 hook 구현이 목적이기 때문에 간단히 처리했다)

  • Component.ts
import { React } from './React.ts'

export function Component() {
  const [count, setCount] = React.useState(1)
  const [text, setText] = React.useState('nyoung')

  window.increment = () => {
    setCount(count + 1)
  }
  window.typing = () => {
    setText(text + '.')
  }

  function render() {
    return `<div class="window" style="width: 300px">
        <div class="title-bar">
            <p class="title-bar-text">Component</p>
        </div>
        <div class="window-body" style="font-size:20px">
          <p>hello i am ${text}</p>
          <div>conter is ${count}</div>
          <button> + dot </button>
          <button> + 1 </button>
        </div>
        </div>`
  }

  return render()
}

그럼 이제 제일 중요한 React의 setState 구현부를 보자.
익히 알기로는 useState는 클로저를 이용한다고 했다.

type ComponentType = () => string

interface Global {
  $root: HTMLElement | null
  RootComponent: ComponentType | null
  hooks: any[]
  index: number
}
  • React.ts
export const React = (function () {
  // useState hooks를 사용한 state들을 담을 수 있는 배열, index 초기값 0

  const global: Global = {
    $root: null,
    RootComponent: null,
    hooks: [],
    index: 0,
  }

  function useState<T>(initVal: T): [T, (newVal: T) => void] {
    const { hooks, index } = global

    const state = hooks[index] || initVal

    const setState = (function () {
      const _idx = index
      return function (newVal: T) {
        // state 값이 동일할 때, 렌더링 하지 않는다
        if (hooks[_idx] === newVal) return

        hooks[_idx] = newVal
        render()
      }
    })()

    // 다음 hook을 위해 idx + 1
    global.index += 1
    return [state, setState]
  }

  function render() {
    const { $root, RootComponent } = global
    global.index = 0 // 계속 0으로 초기화 => 실행 될 때 마다 배열 초기화 (stack에 쌓이는 것 방지)
    if (!$root || !RootComponent) return
    $root.innerHTML = RootComponent()
  }

  return { useState, createRoot }
})() // 즉시 실행 함수

일단 render함수는 저장된 root노드(DOM 노드)에 RootComponent의 return값을 innerHTML에 넣고 있다.

render함수가 호출될 때 마다 root의 내용이 갱신된다.
다음으로, useState함수 부분을 확인해보자.

  function useState<T>(initVal: T): [T, (newVal: T) => void] {
    const { hooks, index } = global

    const state = hooks[index] || initVal

    const setState = (function () {
      const _idx = index
      return function (newVal: T) {
        // state 값이 동일할 때, 렌더링 하지 않는다
        if (hooks[_idx] === newVal) return

        hooks[_idx] = newVal
        render()
      }
    })()

    // 다음 hook을 위해 idx + 1
    global.index += 1
    return [state, setState]
  }

useState 함수는 initVal을 받는다. hooks[index]를 조회해서 만약 값이 없다면, initVal를 넣어준다.
그것이 바로 state이다. 그럼 hooks[index]에 값을 넣어주는 동작도 필요할 것이다.
hooks와 index는 global이라는 React내의 객체에서 꺼내오고 있다.

해당 객체이다.
hooks와 index의 초기값은 각각 빈 배열과 0이다.

  const global: Global = {
    $root: null,
    RootComponent: null,
    hooks: [],
    index: 0,
  }

그런데 useState에서 마지막 2줄을 보면, index 값에 +1을 해주고 있다.
즉 useState가 실행될 때 마다, index값은 하나씩 더해지는 것이다.
또한 useStat는 setState라는 내부함수도 리턴하고 있다.

    const setState = (function () {
      const _idx = index
      return function (newVal: T) {
        // state 값이 동일할 때, 렌더링 하지 않는다
        if (hooks[_idx] === newVal) return

        hooks[_idx] = newVal
        render()
      }
    })()

setState는 즉시 실행 함수로, 즉시실행 함수는 함수 호출 즉시 실행되고, 리턴 값이 리턴되는 작동을 한다. 여기에서 즉시 실행 함수를 쓴 이유는 index값을 캡쳐할 수 있기 때문이다.

현재 index값(useState가 호출된 시점의 index값)을 내부 변수 _idx에 저장한다.
그리고 그 값을 setState가 리턴하는 함수 안에서 사용하는 것이다. hooks[_idx]값에 인자로 받은 새로운 값newVal 이 기존 값과 동일하면 그대로 두고,(rendering안함)
다르다면 hooks[_idx] 에 집어넣어주고 render()함수를 호출하는 것이다.

profile
코드는 죄가 없다,,

0개의 댓글

관련 채용 정보