리액트 만들어보기

코와->코어·2022년 5월 24일
0

BYOR

목록 보기
1/1
post-thumbnail

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

0. 복습

React
JSX
DOM element

const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)

JSX는 유효한 바닐라 자바스크립트 문법이 아니기 때문에 바벨과 같은 빌드 도구를 사용해 JS 문법으로 바꾼다.
태그 안에 있는 코드를 createElement()에 태그 이름과 props, children을 인자로 넘겨줌으로써 바꾼다.
React.createElement()함수는 인자들로부터 객체 하나를 만드는데, 주요하게 type과 props라는 두 가지의 properties를 살펴보겠다

type은 우리가 만들고 싶어 하는 DOM 노드의 타입을 명시한 문자열로, 태그 이름이다.

props는 또 다른 객체로써, JSX attributes로부터 모든 key와 value들을 가지고 있다. 그 중의 특별한 property로 children이 있는데, 문자열 또는 다른 element들의 배열이다.

ReactDOM.render()는 어떻게 바꿀 수 있을까?
먼저 element의 type을 사용해 노드를 하나 만들고 모든 props를 해당 노드에게 할당해주면 된다
그런 다음, children을 위한 노드들을 만드는데, 여기서는 문자열만 가지므로 textNode를 사용해 노드 하나를 만들겠다.
이때, 이 노드가 {nodeValue: 'hello'}라는 값을 갖고 있는 것처럼 nodeValue를 설정해준 것에 주목

마지막으로 textNode를 h1에 추가하고 h1을 container에 추가한다

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

const container = document.getElementById("root")

const node = document.createElement(element.type)
node["title"] = element.props.title

const text = document.createTextNode("")
text["nodeValue"] = element.props.children

node.appendChild(text)
container.appendChild(node)

1. createElement()

앞에서 봤듯이, element는 type과 props를 가지고 있는 object일 뿐이다. 따라서 createElement()함수가 해야 할 일도 object를 만들기만 하면 된다

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  }
}

예를 들어,

  • createElement("div")는 {type: "div", props: {children:[]}}을 리턴하고 - createElement("div", null, a)는 {type: "div", props: {children: [a]}}를 리턴하고,
  • createElement("div", null, a, b)는 {type: "div", props: {children:[a, b]}}를 리턴한다
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

children 배열은 문자열이나 숫자같은 원시값도 포함해야 한다. 그래서 객체가 아닌 것들을 감싸서 특별한 타입: TEXT_ELEMENT를 만들 것이다.??? 먼말인데....

const Didact = {
  createElement,
}

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

우리 코드에 이름 붙이고 바벨이 리액트 말고 내 꺼 쓰게 하는 주석 추가함

2. render()

ReactDOM.render() 대체하는 함수 만들어보기~~
일단 DOM에 element 추가하는 기능만 넣을것임

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]
    })
  
  element.props.children.forEach(child =>
    render(child, dom)
  )

  container.appendChild(dom)
}

재귀적으로 children에 대해 render 호출해주고~
타입이 textelement인 경우에는 textnode 만들어주기
_isProperty 부분은 element prop을 그 노드에 할당하는 것??? 뭔말이야...

3. Concurrnet 모드

만약에 element tree가 넘 깊다면, 메인 스레드를 계속 블락하고 있을 것임
렌더 함수의 재귀 호출 부분을 최적화해야 함

렌더링 하는 부분 작게 쪼개서 각 부분 완료하면 브라우저가 중간에 끼어들 수 있도록 할 것임

루프를 만들기 위해 requestIdleCallback을 사용할 것임, 메인 스레드가 놀고 있을 때 콜백함수 실행하는 아이임


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에 deadline 인자를 줄 수 있음 브라우저가 끼어들 때까지 텀을 얼마나 줄 것인지 설정하는 것임

먼저 초기 unitOfWork를 설정해주고 그 work 수행하면서 다음 unitOfWork를 리턴해주는 performUnitOfWork함수를 작성할거심

4. Fibers

작업 단위를 정리하기 위해 fiber tree 자료구조가 필요함

각 element당 하나의 fiber를 둘 거고, 각 fiber아 작업 단위가 될 것임

Didact.render(

, container )

만약 위와 같은 element tree가 있다면

먼저 root fiber를 만들고 netUnitOfWork를 만듦
나머지 작업은 performUnitOfWork()함수에서 할 거고, 이 함수에서 각 fiber마다

  • element를 DOM에 추가
  • 그 element의 child를 위해 fiber 생성
  • nextUnitOfWork 선택

이 자료구조의 목표 중 하나는 다음 작업 단위를 쉽게 찾는 것임
그래서 각 fiber가 첫 번째 자식 -> 다음 형제 element -> 부모 element로 연결되어 있는 것

fiber 작업을 끝내면 다음 작업 단위가 될 child가 있게 됨
위의 예시에서, div 작업 끝내면 다음 작업 단위는 h1 fiber가 됨

만약 해당 fiber의 child가 없다면, sibling으로 가고, 얘도 없으면 uncle로 감: uncle은 slbling의 parent임, uncle도 없으면 sibling의 parent 찾거나 root 찾을 때까지 위로 감

performUnitOfWork() 동작 과정

먼저 render 함수에서 nextUnitOfWork를 fiber tree의 root로 설정

브라우저가 준비되면, workLoop()를 호출하고 root부터 작업 시작

먼저 새 노드를 만들고 DOM에 추가, 부모의 fiber.dom 필드에 새로 만든 DOM 노드 넣기

그리고 각 child에 대해 새 fiber 생성

만든 fiber들을 fiber tree에 추가

child -> sibling -> uncle 순으로 다음 작업 단위 찾기

5. Render와 Commit 단계

불완전하게 렌더링하는 것을 막기 위해 노드 만듥 바로 렌더링하는 것이 아니라 fiber tree의 root를 계속해서 추적하기(wipRoot라고 부를것임)

렌더링이 다 끝나면, 즉 다음 작업 단위가 없으면, 전체 fiber tree를 DOM에 추가

이걸 commitRoot()에서 할 것임. 여기에선 재귀적으로 모든 노드들을 dom에 추가함

function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot = null
}function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

6. 수정

update나 delete는 어떻게 할까?

render()함수에서 받은 element들을 마지막으로 DOM에 commit된 fiber tree와 비교해서 변경사항 찾아냄

그래서 마지막으로 DOM에 추가한 fiber tree를 추적해야 함 이걸 currentRoot라고 할 것임

그리고 모든 fiber에 alternate 필드를 추가할 것임. 이 필드가 이전 커밋 단계에서 DOM에 추가한 예전 fiber를 가리키고 있을 것

performUnitOfWork함수 내부에 recondileChildren()함수 추가. 여기서 예전 fiber를 새 노드로 바꿀 것임

예전 fiber의 children과 수정하고 싶은 노드의 배열을 동시에 순회할 것
만약 배열과 연결 리스트를 순회하는 데 필요한 모든 보일러플레이트들을 무시한다면 제일 중요한 oldFiber와 element를 놓칠 것~~ ???뭔말이냐~~

element는 우리가 DOM에 렌더링하고 싶어하는 노드이고 oldFiber는 우리가 이전에 렌더링한 것임

DOM에 변화가 있는지 확인하기 위해 이 두 개를 비교해야 함

비교하기 위해 type을 사용할 건데

  • 만약 old fiber와 새 element가 같은 type이라면, DOM 노드는 그대로 두고 props만 업데이트
  • 만약 type이 다르고 새 element가 있다면, 새 DOM 노드 만들어야 함
  • 만약 type다른데 old fiber만 있다면, 이 노드 지워야 함

old fiber와 element가 타입이 같으면, old fiber로부터 props와 DOM 노드를 유지하는 새 fiber를 만듦

그리고 fiber에 새 필드 추가할건데, effectTag임

만약 새로운 DOM 노드를 만드는 경우라면, 새 fiber에 PLACEMENT라는 effectTag를 넣어서 만듦

노드 삭제하는 경우에는 삭제할 노드의 effectTag에 DELETION 넣어줌. 나중에 얘는 렌더링 안 해도 된다는 정보를 유지하기 위해 삭제할 노드들의 배열 있어야 함

그리고 DOM에 변화 반영할 때 삭제할 노드들은 뺌

commitWork()에서 effectTag처리하기
만약 fiber가 PLACEMENT를 갖고 있으면 예전처럼 그냥 DOM에 추가하면 됨
만약 DELETION을 갖고 있으면 child 삭제
만약 UPDATE라면 props를 업데이트해줌

updateDOM()에서 갱신 처리할것임
old fiber에 있던 props를 새로운 fiber와 비교해서 없어진 것은 제거하고 바뀌거나 새로 생긴 것들을 넣어줌

갱신해야 하는 특별한 종류의 props는 event listner인데, prop 이름이 on으로 시작하는 것들은 좀 다르게 처리할것임

만약 event handler가 바뀌었으면 그거를 노드에서 삭제하고 새로운 핸들러를 넣어줌

7. 함수형 컴포넌트

다음으로 할 것은 함수형 컴포넌트 지원하는 것

/** @jsx Didact.createElement */
function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)

이 예시를 js로 바꾸면

function App(props) {
  return Didact.createElement(
    "h1",
    null,
    "Hi ",
    props.name
  )
}
const element = Didact.createElement(App, {
  name: "foo",
})

이렇게 될 것

함수형 컴포넌트들은 두 가지 면에서 좀 다른데,

  • 함수형 컴포넌트의 fiber는 DOM 노드를 안 가짐
  • children들이 props에서 오는 것이 아니라 함수 실행해서 얻어짐

그래서 먼저 fiber type이 함수인지 확인하고, 이에 따라 update 함수 달라짐

updateHostComponent()에서는 위에서 했던 그대로 할 것임
updateFunctionComponent()에서는 children 얻기 위해 실행할것임

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

여기서 fiber.type이 App 함수이고, 이걸 실행하면 <h1>을 얻게 됨. child 얻는 방법만 다르고 나머지는 똑같음

이제 commitChange()를 수정할 것임

이제 DOM 없는 fiber가 있으므로 두 가지 바꿔야 함

먼저, DOM노드의 부모를 찾기 위해 DOM 노드가 있는 fiber를 찾을 때까지 fiber tree의 위로 올라가야 함

그리고 노드를 삭제할 때도 DOM 노드의 child를 찾을 때까지 계속 내려가야 함
~~??? 모르겠땅!!!!
~~

8. hooks

마지막!
함수형 컴포넌트가 있으므로 state를 추가해볼까

profile
풀스택 웹개발자👩‍💻✨️

0개의 댓글