나만의 리액트 라이브러리를 만들어보자

출처 : https://pomb.us/build-your-own-react/

  • 들어가기 전에
    • 결과물? 자바스크립트로 작성된 리액트 코드
    • 어떻게 ? ‘Build your own React’를 보고 따라한다, 더 알고싶은 부분은 공부해서 정리한다
💡 목차 **Step I**: The `createElement` Function **Step II**: The `render` Function **Step III**: Concurrent Mode **Step IV**: Fibers **Step V**: Render and Commit Phases **Step VI**: Reconciliation **Step VII**: Function Components **Step VIII**: Hooks

1. createElement Function

JSX —babel—> React.createElement

React의 element는 React.createElement 함수를 이용해 만들어지지만 가독성이 떨어지기 때문에 JSX 문법을 사용한다. JSX 문법은 babel이 JS로 변환한다.

React.createElement

function createElement(type,props,...children){ // (type,props,[children])
    return {
        type,// tag name
        props:{
            ...props,
            children
        }
    }
}
// text를 처리하기 위한 함수
function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: []
    }
  };
}

💡 뜬금없이 스프레드 연산자!

...children은 children이라는 배열의 요소를 스프레드 한 것. 따라서 type,props,[children]이 됨

2. render Function

ReactDOM.render

  1. DOM node 생성
  2. props 추가
  3. 생성한 node를 container에 추가
function render(element,container){
	const dom = 
				element.type == "TEXT_ELEMENT"
				? document.createTextNode("") // text node
				: document.createElement(element.type) // element type으로 DOM노드 생성하기

  const isProperty = key => key !== 'children'
  // element에 props 추가
	Object.keys(element.props)
			.filter(isProperty)
			.forEach(name => {
				dom[name] = element.props[name]
			})				
	// children을 재귀적으로 
	element.props.children.forEach(child=>
		render(child,dom)	
	)
	container.appendChild(dom) // 생성한 node container에 추가
}

3. Concurrent Mode(동시성 모드)

render 함수에서 재귀호출부분의 경우 트리 끝까지 렌더가 될 때까지 동작을 멈출 수가 없다. 트리가 커지는 경우 긴 시간 동안 메인 스레드가 멈추게 된다. 브라우저가 높은 우선 순위의 작업을 처리하기 위해서 렌더를 멈출 필요가 있다.

function render(element, container) {
... 
 element.props.children.forEach((child) => render(child, dom)); 

  container.appendChild(dom);
}

그래서 작업을 작은 단위로 나누고, 각 단위가 끝나면 브라우저가 렌더링을 중단하도록 한다.

let nextUnitOfWork = null

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

requestIdleCallback : loop를 만들기 위해 requestIdleCallback를 사용한다. 브라우저의 메인 스레드가 idle 상태가 되면 requestIdleCallback을 호출한다.

4. Fibers

각 작업 단위를 구조화 하기 위해 fiber tree 라는 자료 구조가 필요하다.

위와 같은 작업을 수행한다고 할 때 render 함수 내부에서 root fiber를 만들고, 이것을 nextUnitOfWork로 설정한다. 나머지 작업들은 performUnitOfWork function에서 일어난다.

각 fiber는 세가지 일을 한다.

  1. element를 DOM에 추가한다
  2. element의 children에 추가할 fiber를 만든다
  3. 다음 작업 단위를 선택한다.

fiber - children - ling - parent

fiber tree의 목적은 다음 작업단위를 쉽게 찾는데에 있다.

한 fiber의 작업이 끝났을 때, children이 있을 경우에는 children이 없을 때는 sibling이 다음 작업 단위가 된다.

sibling도 없을 경우 parent로 올라가고, root로 갈때까지 render 된다.

5. Render and Commit

지금의 코드에서 새로운 node가 DOM에 추가될 때 브라우저는 렌더 트리가 완성되기 전에 작업을 중단할 수 있다. 이 경우 우리는 미완성된 UI를 보게된다.

이를 위해 DOM을 제거하는 로직 대신, fiber 루트를 추적할 것이다. 이를 wipRoot(work in progress rot)라고 한다.

일단 모든 작업이 끝나고 나면 (더 이상 다음 작업이 없는 경우), 전체 fiber 트리를 돔에 커밋한다.

이 과정은 commitRoot 함수에서 이루어집니다. 여기서 모든 노드를 재귀적으로 dom에 추가한다.

6. Reconciliation(재조정)

node의 update와 delete는 render 함수로 얻은 element를 마지막으로 커밋한 fiber 트리와 비교한다.

이를 위해 커밋을 한 후 마지막 fiber tree를 저장한다. 이를 current root라고 부른다.

그리고 모든 fiber 속성에 alternate를 추가한다. 이것은 이전의 단계에서 커밋한 fiber다.

그런 다음 performUnitOfWork에서 새로운 fiber를 생성하는 로직을 추출해 reconcileChildren 함수를 만든다. 예전 fiber의 자식들과 재조정해야하는 element를 비교하기 위에 동시에 순회한다.

element와 oldFiber의 type 비교

  • 타입이 같으면 props만 바꾼다 - update
  • 타입이 다르면 새로운 element이므로 새로운 DOM node를 생성한다 - placement
  • 타입이 다르고 예전 fiber가 있으면 예전 node를 지운다 - deletion

effectTag 속성을 추가한다.

제거하고 싶은 노드를 추적하기 위해 delection 배열을 만든다.

commitRoot에서 delection 배열을 순회해 commitWork를 실행한다. commitWork 함수에서 effectTag를 이용해 append하거나 removeChild하거나 update한다.

갱신을 위해 updateDom 함수를 만든다.

예전 fiber의 props를 새로운 fiber의 props와 비교해 사라진 props는 제거하고, 달라진 props를 설정한다. 이 때 on으로 시작하는 props(event)가 있다면 eventListner를 추가한다.

7. 함수형 컴포넌트

함수형 컴포넌트로 만들어진 fiber에는 DOM node가 없다. 그래서 children을 props로 가져오는 대신 함수를 호출한다.

fiber 타입이 함수인지 체크한다음 updateFunctionComponent 에서 지금까지와 같은 역할을 하게끔 수정한다.

8. Hook

함수형 컴포넌트에 상태를 추가하기.

const Myact = {
  createElement,
  render,
  useState,
}

/** @jsx Didact.createElement */
function Counter() {
  const [state, setState] = Didact.useState(1)
  return (
    <h1 onClick={() => setState(c => c + 1)}>
    Count: {state}
	</h1>
	)
}
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)
let wipFiber = null
let hookIndex = null

function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

함수형 컴포넌트를 호출하기 전에 useState 함수의 내부에서 사용하기 위한 몇몇 전역 변수들을 초기화해야 한다.

먼저 작업중인 fiber를 설정한다. 또한 그 fiber에 hook 배열을 추가함으로서 동일한 컴포넌트에서 여러 번 useState 함수를 호출 할 수 있도록 한다.

function useState(initial) {
  const oldHook =
        wipFiber.alternate &&
        wipFiber.alternate.hooks &&
        wipFiber.alternate.hooks[hookIndex]
  const hook = {
    state: oldHook ? oldHook.state : initial,
  }
  
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state]
}

함수형 컴포넌트가 useState를 호출할 때 이것이 오래된 hook인지를 체크하는데, 이때 훅 인덱스를 사용해 fiber의 alternate를 체크한다.

만약 우리가 가지고 있는 것이 오래된 hook이라면 상태를 초기화하지 않았을 경우 이 훅의 상태를 새로운 훅으로 복사한다.

그리고 새로운 훅을 fiber에 추가한 뒤 훅 인덱스 값을 증가시킨 다음 state를 반환한다.

const hook = {
  state: oldHook ? oldHook.state : initial,
  queue: [],
}
  
    const setState = action => {
      hook.queue.push(action)
      wipRoot = {
        dom: currentRoot.dom,
        props: currentRoot.props,
        alternate: currentRoot,
      }
      nextUnitOfWork = wipRoot
      deletions = []
    }
      
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}

또한 useState는 상태를 갱신하는 함수 역시 리턴해야 하므로, 액션을 받는 setState 함수를 정의한다. 이 액션을 우리가 훅에 추가한 큐에 넣는다.

그리고 렌더 함수에서 했던 것과 비슷한 작업을 하는데, 새로운 작업중(wip)인 루트를 다음 작업할 단위로 설정하여 반복문에서 새로운 렌더 단계를 시작할 수 있도록 한다.

const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
  hook.state = action(hook.state)
})

아직 액션을 실행하지는 않았다. 이는 컴포넌트 렌더링 다음에 수행하는데, 오래된 훅의 큐에서 모든 액션을 가져온 다음 이를 새로운 훅 state에 하나씩 적용하면 갱신된 state를 얻을 수 있게 된다.

새로 알게된 것

  • React의 element는 React.createElement 함수를 이용해 만들어지지만 가독성이 떨어지기 때문에 JSX 문법을 사용한다. JSX 문법은 babel이 JS로 변환한다.
  • react는 작업단위를 구조화하기 위해 fiber tree 구조를 사용한다.
  • virture DOM은 render 함수로 얻은 element 즉, javascript 객체다. 리액트는 이전의 tree와 새로운 tree를 동시에 순회해 비교한다.

좋은 웹페이지 즐겨찾기