본문 바로가기
FRONT-END/React

나만의 React 만들기(Build your own react) - STEP 3

by 랄라J 2024. 9. 5.

https://pomb.us/build-your-own-react/ 을 기반으로 학습 내용을 정리한 글입니다.


STEP 3 - Concurrent Mode

이전 step까지의 코드에서 리팩터링이 필요한 부분이 있다. 그 부분을 먼저 수정하고 진행해 보자.
바로 render function 내부에 재귀로 호출하는 부분이다.

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

 

[왜 문제가 될까?🧐]

렌더링을 시작하면 완벽한 element tree를 렌더링 할 때까지 멈추지 않는다. element tree가 크면 메인 스레드를 오랫동안 차단할 수 있다. 그러면 브라우저가 사용자 입력 처리나, 애니메이션을 매끄럽게 유지하는 것과 같은 우선순위가 높은 작업을 해야 하는 경우에도 렌더링이 완료될 때까지 기다려야 한다.

그래서 우리는 작업을 작은 단위로 나누고 각 단위를 마친 후에 수행해야 할 다른 작업이 있으면 브라우저가 렌더링을 중단하도록 할 것이다.

let nextUntilOfWork = null;

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

requestIdleCallback(workLoop);

function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

위 코드는 브라우저의 유휴 시간을 활용해 작업을 점진적으로 처리하는 방식인 협력적 스케줄링을 구현한 것이다.
즉, 브라우저가 주어진 작업을 한 번에 모두 처리하는 것이 아닌 유휴 시간 동안 조금씩 작업을 처리하여 메인 스레드가 계속해서 응답할 수 있게끔 한다.


위 코드를 이해하기 위해 하나씩 조금 뜯어 살펴보자!

nextUntilOfWork는 다음에 처리해야 할 작업을 가리킨다. 

workLoop 함수는 실제 작업 loop를 관리한다.

- deadline 객체는 브라우저가 유휴시간 동안에 이 함수에 얼마나 더 시간을 줄 수 있는지를 의미한다.

- shouldYield는 브라우저가 다른 작업에 리소스를 할당해야 하는지 여부를 결정하는 플래그다.

- while 루프는 nextUntilOfWork가 null이 아니고 shoudYield가 false인 동안 계속 실행된다.
이 루프는 performUnitOfWork에 함수의 현재 작업(nextUntilOfWork)을 전달해 처리하고, 반환된 값은 다음 작업으로 설정된다.

- shouldYield = deadline.timeRemaining() < 1은 브라우저의 유휴 시간이 거의 끝나가는지를 확인하는 조건이다. 남은 시간이 1ms 미만이면 루프를 중단하고 shouldYield를 true로 설정한다.

requestIdleCallback(workLoop) 브라우저가 유휴 상태일 때 workLoop 함수를 호출하도록 예약한다.
-> requestIdleCallback은 deadline 매개변수를 제공해, 브라우저가 다시 제어해야 할 때까지 남은 시간을 확인할 수 있다.

performUnitOfWork 함수는 각 단위 작업을 실제로 수행하는 함수다.
이 함수는 nextUnitOfWork를 입력으로 받아 작업을 처리하고 다음에 수행할 작업을 반환한다.
React에서는 이 부분에서 가상 DOM 트리의 각 노드를 순회하면서 작업을 수행하게 된다.

참고로, React에서는 더 이상 requestIdleCallback을 사용하지 않고, scheduler package를 사용한다.

즉, STEP 3을 완료한 상태의 전체 코드는 아래와 같다.

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === "object" ? child : createTextElement(child)
      ),
    },
  };
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

function render(element, container) {
  const dom =
    element.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);

  const isProperty = (key) => key !== "children";
  Object.keys(element.props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = element.props[name];
    });

  container.appendChild(dom);
}

let nextUntilOfWork = null;

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

requestIdleCallback(workLoop);

function performUnitOfWork(nextUntilOfWork) {
  // TODO
}

const Didact = {
  createElement,
  render,
};

/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
);

const container = document.getElementById("root");
Didact.render(element, container);

여기까지 STEP 3 완료!

반응형

댓글