jinux127 2022. 6. 26. 23:19

지난 번에 구현하였던 Vritual DOM에 이어 함수형 컴포넌트를 지원하기 위한 작업을 이어서 해보자.

 

7. 함수형 컴포넌트

 

함수형 컴포넌트에서는 두 가지 면에서 차이가 있는데 함수형 컴포넌트에서 만들어진 fiber는 DOM 노드가 없고 자식들을 props에 직접 가져오는 대신 함수를 실행하여 얻는다.  그래서 함수형 컴포넌트의 경우 따로 구현을 해줘야하기 때문에 조건 로직을 구현한다.

 

function performUnitOfWork(fiber) {
  // 함수형 컴포넌트에서 만들어진 fiber는 DOM 노드가 없고 children을 props에서 직접 가져오는 대신 함수를 실행하여 얻는다.
  // 이를 판별하는 조건문을 만든다.
  const isFunctionComponent = fiber.type instanceof Function;
  if (isFunctionComponent) {
    // 함수형 컴포넌트라면
    updateFunctionComponent(fiber);
  } else {
    // 함수형 컴포넌트가 아니라면
    updateHostComponent(fiber);
  }
  ...
}

updateFunctionComponent 에서는 자식 요소를 얻는 함수를 실행하는데 

function updateFunctionComponent(fiber) {
  // 함수형 컴포넌트라면 자식 요소를 얻는다.
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

이렇게 자식을 얻게 되면 리렌더링 작업은 같은 방법으로 수행된다.

 

그 다음으로 필요한 것은 commit 하는 부분이다. 함수형 컴포넌트의 경우는 DOM 노드가 없는 fiber를 가지고 있기 때문에 두 가지를 바꿔야한다. 

  // 함수형 컴포넌트가 적용되면서 DOM 노드가 없는 fiber를 가지기 때문에 이를 구별해야한다.
  // const domParent = fiber.parent.dom;
  let domParentFiber = fiber.parent;
  // DOM 노드의 부모를 찾으려면 DOM 노드를 가진 fiber를 찾을 때까지 fiber 트리의 상단으로 올라간다.
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent;
  }
  const domParent = domParentFiber.dom;

DOM 노드의 부모를 찾으려면 DOM 노드를 가진 fiber를 찾을 때까지 fiber트리의 상단으로 올라가야 한다.

 

그리고 노드를 제거할 때도 동일하게 DOM 노드를 가진 자식을 찾을 때까지 탐색을 수행해야 한다.

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

이 함수를 제거하는 조건에서 호출한다.

 

전체코드

function commitWork(fiber) {
  if (!fiber) {
    return;
  }
  // 함수형 컴포넌트가 적용되면서 DOM 노드가 없는 fiber를 가지기 때문에 이를 구별해야한다.
  // const domParent = fiber.parent.dom;
  let domParentFiber = fiber.parent;
  // DOM 노드의 부모를 찾으려면 DOM 노드를 가진 fiber를 찾을 때까지 fiber 트리의 상단으로 올라간다.
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent;
  }
  const domParent = domParentFiber.dom;

  // fiber의 태그로 구별
  if (fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
    // 부모 fiber 노드에 자식 DOM 노드를 추가
    domParent.appendChild(fiber.dom);
  } else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {
    // 이미 존재하는 DOM 노드를 변경된 props로 갱신
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag === 'DELETION') {
    // 자식을 부모 DOM에 제거
    // domParent.removeChild(fiber.dom);
    // 함수형 컴포넌트가 적용되면서 DOM 노드를 가진 자식을 찾을 때 까지 찾는다.
    commitDeletion(fiber, domParent);
  }

  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

 

 

8. 훅훅훅훅훅

 

우리는 가장 유용하면서 자주 사용하는 훅인 useState훅을 구현할 것 이다. 그러기 앞서 간단한 카운터 컴포넌트를 구현해보자.

import Jeact from './core/Jeact';

// @jsx Jeact.createElement
function Counter() {
  const [state, setState] = Jeact.useState(1);

  return (
    <div>
      <h1>Count: {state}</h1>
      <button onClick={() => setState((state) => state + 1)}>버튼</button>
    </div>
  );
}

const element = <Counter />;
const container = document.getElementById('root');

Jeact.render(element, container);

 

useState를 통해 상태를 변경하면 자동으로 렌더링이 되면서 화면이 바뀐다. 이 말은 useState를 호출할 때 컴포넌트가 업데이트 되는 부분을 호출한다는 말이다.

let wipFiber = null;
let hookIndex = null;

function updateFunctionComponent(fiber) {
  // useState 내부에서 사용하기 위한 전역 변수를 초기화한다.
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = []; // hooks 배열을 추가함으로서 동일한 컴포넌트에서 여러 번 useState함수를 호출할 수 있도록 한다.
  // 함수형 컴포넌트라면 자식 요소를 얻는다.
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

먼저 wipFiber는 현재 작업 중인 fiber, 그 fiber에 hooks 배열을 추가함으로서 동일한 컴포넌트에서 여러 번 useState 함수를 호출할 수 있게 된다. hookIndex는 hook이 이미 존재했던 hook인지 판단할 때 사용하는 인덱스로 이를 사용해 fiber의 alternate를 체크한다.

여기서 alternate는 기존의 fiber이다.

 

이제 useState를 구현해보자.

 

먼저 작업중인 fiber를 설정 한 wipFiber를 가지고 이미 존재한 hook인지 체크한다. 만약 새로운 hook이라면 새로운 훅으로 복사하고 그 인덱스 값을 증가하고 다음 state를 반환한다.

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는 상태를 갱신하는 함수 역시 리턴해야 하는데, 이를 setState라 한다.

setState에서는 render와 비슷하게 nextUnitOfWork에 현재 wipRoot를 다음 작업할 단위로 설정하여 새롭게 렌더 할 수 있도록 한다.

function useState(initial){
  ...	
  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]
}

아직 액션을 실행하지는 않았는데 이를 기존의 훅의 큐에서 모든 액션을 가져온 후에 새로운 훅에 state를 호출는 순으로 하나씩 적용하면 새로운 state를 렌더할 수 있다.

 

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

 

전체 코드

function useState(initial) {
  // 전역변수로 선언된 hookIndex를 사용하여 fiber의 alternate를 체크해 이미 존재하는 hook인지 체크한다.
  // alternate는 이전 커밋 단계에서 DOM에 추가했던 fiber에 대한 링크이다.
  const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
  // hook이 이미 존재하는 hook이라면 기존의 훅으로 사용
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [],
  };

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

  const setState = (action) => {
    hook.queue.push(action);
    // 큐에 넣어주고
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    };
    nextUnitOfWork = wipRoot;
    // 재렌더링하면서 state 변경
    deletions = [];
  };

  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

 

이로써 리액트의 기본적인 기능을 구현해보았다.

 

다만 리액트의 기본적인 기능을 구현했을 뿐이고, 리액트의 아름다운 동작을 완전히 따라하진 못할 것이다. 어찌보면 당연한 것이 페이스북의 능력있는 개발자들이 2주라는 기간이 아닌 더 긴 기간을 통해 개발을 한 것을 내가 짧은 기간에 구현한다는 것은 어불성설이다. 다만 이렇게 리액트의 동작 방식을 발끝이라도 따라감으로써 JavaScript와 React를 사용하는데 도움이 되리라 믿는다. 

 

이제 우리는 Bingact를 적용할 때 우리는 styled-component를 적용하고 싶다.. 이를 적용해보자..

 

 

참고사이트:

https://bluewings.github.io/build-your-own-react/

 

나만의 리액트 라이브러리 만들기

이 글은 Rodrigo Pombo 의 Build your own React 의 한국어 버전입니다. 번역은 나만의 리액트 라이브러리 만들기 를 참고하였으며, 사용을 허락해주신 godori…

bluewings.github.io