React | 리액트 한달살기 - 3

Lumpen·2024년 12월 25일
0

React

목록 보기
22/26

virtual DOM

인터랙티브한 웹의 경우
레이아웃과 리페인팅에 발생하는 비용이 크다
자식 요소가 많은 경우 더욱 그렇다
싱글페이지 어플리케이션의 경우에는 더더욱 비용이 커진다

이러한 문제를 해결하고자 탄생한 것이 가상 DOM 이다
가상 DOM 은 브라우저에서 관리하는 것이 아니라
React 에서 관리한다
실제 DOM 과 같은 객체를 리액트에서 가지고 있다가 변경점만 찾아 변경 후 실제 DOM 에 적용시킨다

가상 DOM 의 오해는 빠르다는 것이다
항상 빠른 것은 아니고 대부분의 상황에서 충분히 빠르다

리액트는 특히 클라이언트의 자원을 이용해 렌더링 하기 때문에 클라이언트의 성능에 많이 의존해야 하는데
최소한의 DOM 업데이트를 수행하도록 하면 렌더링하는 비용을 아껴 성능에도 이점을 갖게 되지 않을까 싶다

변경점을 찾기 위해서는 state 를 사용한 곳을 표시하고 state 변경 시 해당 위치에서만 변경이 일어나야 한다


이슈

React 는 babel.transform 을 하여
JSX 코드를 jsx() jsxs() 함수로 변환
jsx() 와 jsxs() 함수에서는 각각
createElement() 를 실행하여 virtualDOM 객체를 생성한다

state 를 변경하여 App() 컴포넌트의 리렌더링을 일으킬 때 변경점을 찾기 위해 렌더링 전/후 App() 을 실행해보면 실행된 App() 컴포넌트에서는 항상 state 초깃값만을 반환한다
App() 이 리렌더링 되면서 useState() 가 재실행 될 때 기존의 값을 참조하지 않고 있어 발생하는 문제로 보인다

currentRender 필드 추가로 기존의 값을 저장하여
리렌더링 시에 참조하도록 수정

class React {
  states = new Map()
  stateIndex = 0
  listeners = []
  currentRender = null

  constructor() {
    this.currentComponent = null
  }

  setState = (index, newState) => {
    const states = this.states.get(this.currentComponent)
    if (typeof newState === 'function') {
      states[index] = newState(states[index])
    } else {
      states[index] = newState
    }
    this.listeners.forEach(listener => listener())
  }

  subscribe = listener => {
    this.listeners.push(listener)
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener)
    }
  }

  useState = initialValue => {
    if (!this.states.has(this.currentComponent)) {
      this.states.set(this.currentComponent, [])
    }

    const states = this.states.get(this.currentComponent)

    if (states.length <= this.stateIndex) {
      states[this.stateIndex] = initialValue
    }

    const index = this.stateIndex
    const setState = this.setState.bind(this, index)

    if (!this.currentRender) {
      this.currentRender = states
    }

    this.stateIndex++
    return [this.currentRender[index], setState]
  }

  render = tree => {
    const rootElement = document.createElement(tree.type)
    if (tree.type === 'h1') {
      console.log(tree, 'tree') // + 1
    }

    const { children, onClick, onChange, checked, ...elementProps } = tree.props

    if (onClick) rootElement.addEventListener('click', onClick)
    if (onChange) rootElement.addEventListener('change', e => onChange(e))
    if (checked) rootElement.checked = checked

    Object.keys(elementProps).forEach(key => {
      rootElement.setAttribute(key, elementProps[key])
    })
    if (children) {
      if (Array.isArray(children)) {
        children.map(child => {
          if (typeof child !== 'object') {
            rootElement.appendChild(document.createTextNode(child))
          } else {
            rootElement.appendChild(this.render(child))
          }
        })
      } else {
        if (typeof children !== 'object') {
          rootElement.appendChild(document.createTextNode(children))
        } else {
          rootElement.appendChild(this.render(children))
        }
      }
    }
    return rootElement
  }

  renderComponent = component => {
    this.currentComponent = component
    this.stateIndex = 0
    return this.render(component())
  }
}

const react = new React()

export default react
  • 이벤트 위임 필요

class React {
  states = new Map()
  stateIndex = 0
  listeners = []
  currentRender = null
  currentComponent = null

  prevVDOM = null

  setState = (index, newState) => {
    const states = this.states.get(this.currentComponent)
    if (typeof newState === 'function') {
      states[index] = newState(states[index])
    } else {
      states[index] = newState
    }
    this.listeners.forEach(listener => listener()) // subscribe 함수 호출
  }

  subscribe = listener => {
    this.listeners.push(listener)
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener)
    }
  }

  useState = initialValue => {
    if (!this.states.has(this.currentComponent)) {
      this.states.set(this.currentComponent, [])
    }

    const states = this.states.get(this.currentComponent)

    if (states.length <= this.stateIndex) {
      states[this.stateIndex] = initialValue
    }

    const index = this.stateIndex
    const setState = this.setState.bind(this, index)

    if (!this.currentRender) {
      console.log(this.currentRender, 'this.currentRender')
      this.currentRender = states
    }

    this.stateIndex++
    return [this.currentRender[index], setState]
  }

  diff = (currentTree, prevTree, currentTarget = [], prevTarget = []) => {
    if (currentTree === prevTree) return { isChanged: false }
    const { type: currentType, props: currentProps, childrenNode: currentChildrenNode } = currentTree
    const { type: prevType, props: prevProps, childrenNode: prevChildrenNode } = prevTree

    if (currentType !== prevType) {
      return {
        isChanged: true,
        currentTarget: [{ changeType: 'replace', element: currentTree.element }],
        prevTarget: [{ changeType: 'replace', element: prevTree.element }],
      }
    }

    const { children: currentChildren, ...currentRestProps } = currentProps
    const { children: prevChildren, ...prevRestProps } = prevProps

    if (currentChildren) {
      const isChanged = currentChildren.some((child, index) => {
        if (typeof child === 'object') return false
        if (child !== prevChildren[index]) {
          return true
        }
      })
      if (isChanged) {
        currentTarget.push({ changeType: 'innerHTML', element: currentTree.element })
        prevTarget.push({ changeType: 'innerHTML', element: prevTree.element })
      }
    }

    const currentPropsKeys = Object.keys(currentRestProps)

    const diffProps = currentPropsKeys.filter(key => currentRestProps[key] !== prevRestProps[key])

    diffProps
      .filter(key => !key.startsWith('on'))
      .forEach(key => {
        currentTarget.push({
          changeType: 'propsChange',
          element: currentTree.element,
          prop: { key, value: currentRestProps[key] },
        })
        prevTarget.push({
          changeType: 'propsChange',
          element: prevTree.element,
          prop: { key, value: prevRestProps[key] },
        })
      })

    if (currentChildrenNode) {
      currentChildrenNode.forEach((child, index) => {
        this.diff(child, prevChildrenNode[index], currentTarget, prevTarget)
      })
    }

    if (currentTarget.length > 0) {
      return { isChanged: true, currentTarget, prevTarget }
    }
    return { isChanged: false }
  }

  render = tree => {
    const rootElement = document.createElement(tree.type)

    const virtualDOM = {
      type: tree.type,
      props: tree.props,
      element: rootElement,
      childrenNode: [],
    }

    const { children, onClick, onChange, checked, ...elementProps } = tree.props

    if (onClick) rootElement.addEventListener('click', onClick)
    if (onChange) rootElement.addEventListener('change', e => onChange(e))
    if (checked) rootElement.checked = checked

    Object.keys(elementProps).forEach(key => {
      rootElement.setAttribute(key, elementProps[key])
    })

    if (children) {
      children.map(child => {
        if (typeof child !== 'object') {
          rootElement.appendChild(document.createTextNode(child))
        } else {
          const { rootElement: element, virtualDOM: VDOM } = this.render(child)
          rootElement.appendChild(element)
          virtualDOM.childrenNode.push(VDOM)
        }
      })
    }

    return { rootElement, virtualDOM }
  }

  rerenderComponent = component => {
    this.stateIndex = 0
    return this.diff(component(), this.prevVDOM)
  }

  renderComponent = component => {
    if (!this.currentComponent) this.currentComponent = component
    this.stateIndex = 0
    const { rootElement, virtualDOM } = this.render(component())
    if (!this.prevVDOM) this.prevVDOM = virtualDOM

    const { isChanged, currentTarget, prevTarget } = this.diff(virtualDOM, this.prevVDOM)
    if (isChanged) {
      prevTarget.forEach((elementObj, index) => {
        if (elementObj.changeType === 'innerHTML') {
          elementObj.element.innerHTML = currentTarget[index].element.innerHTML
        }
        if (elementObj.changeType === 'propsChange') {
          elementObj.element.setAttribute(elementObj.prop.key, currentTarget[index].prop.value)
        } else {
          const parentElement = elementObj.element.parentElement
          for (const child of parentElement.children) {
            if (child === elementObj.element) {
              parentElement.replaceChild(currentTarget[index].element, elementObj.element)
              break
            }
          }
        }
      })
    }
    this.prevVDOM = virtualDOM
    return rootElement
  }
}

const react = new React()

export default react

diff 함수를 구현하여
기존 virtualDOM 과 비교하여 변경점을 찾고
찾은 변경점을 업데이트 renderComponent 내에서
업데이트 하도록 구현 하였으나
diff 함수의 실행이 render 내부로 옮겨져야 할 것 같기도 한데 다음 기회에..

SyntheticEvent

SyntheticEvent(합성 이벤트)는 리액트의 이벤트 객체로 이벤트 핸들러는 이벤트 객체를 받게 된다
DOM 이벤트와 같이 표준을 준수하지만 브라우저별 불일치를 해소하기 위해 만들어졌다

이벤트 위임

HTML 의 이벤트 전파를 이용하여
이벤트 핸들러를 한 곳에 등록하여 관리하는 것

장점

  • 동적인 엘리먼트에 대한 이벤트 처리가 수월하다
  • 상위 엘리먼트에서만 이벤트 리스너를 관리하기 때문에 하위 엘리먼트는 자유롭게 추가 삭제할 수 있다
  • 이벤트 핸들러 관리가 쉽다
  • 메모리 사용량이 줄어든다
  • 메모리 누수 가능성도 줄어든다

단점

  • 이벤트 위임을 사용하려면 이벤트가 반드시 버블링 되어야 한다
  • 모든 하위 컨테이너에서 발생하는 이벤트에 응답해야 하므로 CPU 작업 부하가 늘어날 수 있다

useEffect


effectIndex = 0
effectListeners = []
useEffect = (effect, deps) => {
    if (!deps) {
      effect()
      return
    }

    if (!this.effectListeners[this.effectIndex]) {
      this.effectListeners.push({ effect, deps })

      effect()
    } else {
      const { effect: prevEffect, deps: prevDeps } = this.effectListeners[this.effectIndex]

      if (prevDeps.some((dep, index) => dep !== deps[index])) {
        this.effectListeners[this.effectIndex] = { effect, deps }
        prevEffect()
      }
    }
    this.effectIndex++
  }

useEffect 는 동기적으로 실행되어 실행 순서를 보장한다
의존성 배열에 값을 주지 않으면 렌더링 시마다 실행,
의존성 배열의 값이 비어있으면 첫 렌더링 시 한 번,
의존성 배열의 값이 있을 때, 첫 렌더링 시와 내부 값 변경 시 한 번 실행된다

hooks

리액트의 hooks 를 최상위에 작성해야 하는 이유

  • 리액트의 규칙 준수: 리액트는 hooks를 언제 어떻게 호출할지를 추적하는 내부 규칙이 있습니다. 이를 따르지 않으면 버그가 발생할 가능성이 높아집니다. 최상위에서 hooks를 호출하면 이런 규칙을 잘 지킬 수 있게 됩니다.
  1. 컴포넌트의 일관성 유지: hooks를 조건문이나 반복문 안에서 사용하면 컴포넌트가 매 렌더링 시 마다 다른 순서로 hooks를 호출하게 될 수 있습니다. 이는 리액트가 hooks의 호출 순서를 추적할 때 혼란을 초래하고, 예상치 못한 동작을 유발할 수 있습니다.
    hooks가 호출되는 순서가 항상 같아야 하기 때문입니다. 리액트는 hooks의 호출 순서에 의존하여 내부적으로 상태와 효과를 관리합니다. 만약 호출 순서가 바뀌면 리액트는 올바르게 상태를 추적할 수 없게 됩니다.
  1. 코드 가독성 및 유지보수성 향상: hooks를 최상위에 작성하면 코드의 가독성과 유지보수성이 향상됩니다. 컴포넌트의 논리 구조가 더 명확하게 보이며, 나중에 코드를 수정하거나 다른 개발자가 코드를 이해하기 쉬워집니다.
profile
떠돌이 생활을 하는. 실업자, 부랑 생활을 하는

0개의 댓글