└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # 나만의 React 만들기 2 | https://pomb.us/build-your-own-react/ 3 | 4 | 위 블로그 보며 자신만의 리액트 만들어보기 5 | 6 | 학습 흐름에 맞추어 작성하였다. 7 | 8 | ## 0. preview 9 | 리액트의 기본 예시는 다음과 같다. 10 | ``` js 11 | const element =

Hello

; 12 | const container = document.getElementById("root"); 13 | ReactDom.render(element, container); 14 | ``` 15 | 16 | 이를 순수 자바 스크립트 함수로 바꾸면 다음과 같다. 17 | ```js 18 | const element = { 19 | type: "h1", 20 | props: { 21 | title: "foo", 22 | children: "Hello", 23 | }, 24 | }; 25 | const container = document.getElementById("root"); 26 | ``` 27 | 28 | jsx는 보통 babel을 통해서 js로 트랜스파일링 된다. 29 | 30 | js로 사용하려면 createElement로 대체 한다. 이 element는 type과 props로 이루어져있다.(사실 더 많은 것이 있지만) 31 | 32 | type은 dom의 타입을 나타내며, 함수가 될 수도 있다. 33 | 34 | props는 jsx의 어트리뷰트로 받은 키와 값들이다. 35 | 36 | children은 자식으로 보통은 또 다른 엘리먼트들의 배열이다. 37 | 38 | 리액트의 render는 react element를 실제 javascript dom으로 바꾸는 함수이다. 39 | ```js 40 | const node = document.createElement(element.type); 41 | node["title"] = element.props.title; 42 | 43 | const text = document.createTextNode(""); 44 | text["nodeValue"] = element.props.children; 45 | 46 | node.appendChild(text); 47 | container.appendChild(node); 48 | ``` 49 | 50 | ## 1. createElement 51 | 이제 우리만의 React를 만들어보자. 52 | 53 | 먼저 앞서 보았듯이 type, props, children을 갖는 element를 만드는 함수를 만들자. 54 | ```js 55 | function createElement(type, props, ...children) { 56 | return { 57 | type, 58 | props: { 59 | ...props, 60 | children: children.map((child) => { 61 | typeof child == "object" ? child : createTextElement(child); 62 | }), 63 | }, 64 | }; 65 | } 66 | ``` 67 | 68 | children이 원시값일 때를 처리하는 wrap 함수(createTextElement)를 만든다. 69 | 70 | 참고로, 실제 React는 children에 원시값이나 빈 배열을 할당하지는 않는다. 71 | ```js 72 | function createTextElement(text) { 73 | return { 74 | type: "TEXT_ELEMENT", 75 | props: { 76 | nodeValue: text, 77 | children: [], 78 | }, 79 | }; 80 | } 81 | ``` 82 | 83 | 우리 라이브러리의 이름은 myReact 라고 해두자 84 | ```js 85 | const myReact = { 86 | createElement, 87 | }; 88 | ``` 89 | 90 | element를 생성한다면 다음과 같을 것이다. 91 | ```js 92 | const element = myReact.createElement( 93 | "div", 94 | { id: "foo" }, 95 | myReact.createElement("a", null, "bar"), 96 | myReact.createElement("b") 97 | ); 98 | ``` 99 | 100 | 만약, 우리가 JSX는 계속 쓰고 싶다면, 101 | 102 | babel에게 React의 createElement가 아닌 우리 creataeElement를 쓰라고 다음 커멘트를 달아야한다. 103 | ```js 104 | /** @jsx myReact.createElement */ 105 | const element = ( 106 |
107 | bar 108 | 109 |
110 | ); 111 | ``` 112 | 113 | ## 2. render 114 | 다음은 render 함수를 만들어보자. 115 | 116 | 우선, DOM에 추가하는 코드만 만들고, 차례대로 업데이트, 삭제를 구현할 것이다. 117 | 118 | DOM 노드를 만들고 type을 정하고, container에 추가하는 함수를 만들자. 119 | 120 | 자식들도 재귀적으로 element를 생성해줘야한다. 121 | 122 | Text element에 대한 제외 처리도 해준다. 123 | 124 | 마지막으로, element의 children 외의 props들을 dom 노드에 추가해준다. 125 | ```js 126 | function render(element, container) { 127 | const dom = 128 | element.type == "TEXT_ELEMENT" 129 | ? document.createTextNode("") 130 | : document.createElement(element.type); 131 | 132 | const isPropety = (key) => key !== "children"; 133 | 134 | Object.keys(element.props) 135 | .filter(isPropety) 136 | .forEach((name) => { 137 | dom[name] = element.props[name]; 138 | }); 139 | 140 | element.props.children.forEach((child) => render(child, dom)); 141 | container.appendChild(dom); 142 | } 143 | ``` 144 | 145 | ## 3. concurrent Mode 146 | 다음 코드를 작성하기 하기전에 짚어야 할게 있다. 147 | 148 | 이전 render 함수의 재귀 코드는 많은 양의 트리를 처리할 때는 오랫동안 블록을 하고 있어서 다른 작업을 할 수가 없다. 149 | ```js 150 | Object.keys(element.props) 151 | .filter(isPropety) 152 | .forEach((name) => { 153 | dom[name] = element.props[name]; 154 | }); 155 | ``` 156 | 157 | 그래서, 이 작업을 작은 단위로 쪼개서 브라우저가 다른 작업이 필요하면 멈출 수 있게 하도록 할 것이다. 158 | 159 | 우리는 requestIdleCallback로 루프를 만들건데, requestIdleCallback은 브라우저가 쓰레드가 작업이 없을 때 실행시키도록 하는 함수이다. 160 | 161 | 실제 React는 이를 더 이상 사용하지 않고 스케쥴러를 사용하지만 우리 대체해서 사용하도록한다. (자세한 내용은 concurrent 모드에 대해 알아보면 좋음) 162 | 163 | 또한, requestIdleCallback은 deadline 파라미터를 제공한다. 이를 통해 브라우저가 다신 제어권을 가져갈때까지 얼마나 남았는지 알 수 있다. 164 | 165 | ```js 166 | function workLoop(deadline) { 167 | let shouldYield = false; 168 | while (nextUnitOfWork && !shouldYield) { 169 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork); 170 | shouldYield = deadline.timeRemaining() < 1; 171 | } 172 | requestIdleCallback(workLoop); 173 | } 174 | ``` 175 | 176 | 이 workLoop를 더 완성시켜보자. 177 | 178 | performUnitOfWork는 다음 작업 단위를 리턴하는 함수로, 일단 todo로 남겨놓자. 179 | 180 | ```js 181 | let nextUnitOfWork = null; 182 | 183 | function workLoop(deadline) { 184 | let shouldYield = false; 185 | while (nextUnitOfWork && !shouldYield) { 186 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork); 187 | shouldYield = deadline.timeRemaining() < 1; 188 | } 189 | requestIdleCallback(workLoop); 190 | } 191 | 192 | requestIdleCallback(workLoop); 193 | 194 | function performUnitOfWork(nextUnitOfWork) { 195 | // todo 196 | } 197 | ``` 198 | 199 | ## 4. fibers 200 | 이 실행 단위를 관리하기 위한 자료구조가 바로 fiber이다. 201 | 202 | element 마다 fiber를 가지게 되고, 이 fiber는 하나의 실행 단위이다. 203 | 204 | 다음과 같은 html이 있다고하자 205 | ``` js 206 | Didact.render( 207 |
208 |

209 |

210 | 211 |

212 |

213 |

, 214 | container 215 | ) 216 | ``` 217 | render 함수는 root fiber를 만들고 첫 nextUnitOfWork로 설정할 것이다. 218 | 219 | 이후 작업들은 performUnitOfWork에서 실행될 것이고 다음과 같다. 220 | ``` 221 | 1. Dom에 element 달기 222 | 2. children에 대한 fiber 생성 223 | 3. 다음 실행 단위 설정 224 | ``` 225 | 226 | 이 자료구조의 핵심은 다음 실행 단위를 쉽게 찾는 것이다. 227 | 228 | 그래서, fiber들은 첫 번째 자녀, 바로 옆 형제, 부모와의 링크를 가지고 있고 다음과 같은 규칙을 가진다. 229 | ``` 230 | 자식 있다면, 자식이 다음 실행 단위가 된다. 231 | 자식이 없다면, 형제가 실행 단위가 된다. 232 | 자식, 형제가 없다면, 삼촌(형제의 부모)가 실행 단위가 된다. 233 | ``` 234 | 235 | 이를 코드로 옮겨보자. 우선 기존 render 함수에서 dom을 만드는 부분을 따로 함수화한다. 236 | ```js 237 | function createDom(fiber) { 238 | const dom = 239 | fiber.type == "TEXT_ELEMENT" 240 | ? document.createTextNode("") 241 | : document.createElement(fiber.type); 242 | 243 | const isPropety = (key) => key !== "children"; 244 | 245 | Object.keys(fiber.props) 246 | .filter(isPropety) 247 | .forEach((name) => { 248 | dom[name] = fiber.props[name]; 249 | }); 250 | 251 | return dom; 252 | } 253 | ``` 254 | 255 | render 함수는 이제 nextUnifOfWork를 root의 fiber tree로 설정한다. 256 | ```js 257 | function render(element, container) { 258 | nextUnitOfWork = { 259 | dom: container, 260 | props: { 261 | children: [element], 262 | }, 263 | }; 264 | } 265 | ``` 266 | 267 | 이제, 브라우저가 준비가 되면 workLoop를 호출해 root부터 작업을 시작할 것이다. 268 | 269 | 먼저, 새 node를 만들어 dom에 붙이고, 270 | 271 | 자식들에 대해 fiber를 생성한다. 272 | 첫 자녀는 자녀로 연결하고, 나머지들은 서로 형제로 연결한다. 273 | 274 | 마지막으로는, 다음 실행 단위를 찾고 반환한다. 275 | 276 | ```js 277 | function performUnitOfWork(fiber) { 278 | if (!fiber.dom) { 279 | fiber.dom = createDom(fiber); 280 | } 281 | 282 | if (fiber.parent) { 283 | fiber.parent.dom.appendChild(fiber.dom); 284 | } 285 | 286 | const elements = fiber.props.children; 287 | let index = 0; 288 | let prevSibling = null; 289 | 290 | while (index < elements.lenth) { 291 | const element = elements[index]; 292 | const newFiber = { 293 | type: element.type, 294 | props: element.props, 295 | parent: fiber, 296 | dom: null, 297 | }; 298 | 299 | if (index == 0) { 300 | fiber.child = newFiber; 301 | } else { 302 | prevSibling.sibling = newFiber; 303 | } 304 | 305 | prevSibling = newFiber; 306 | index++; 307 | } 308 | 309 | if (fiber.child) { 310 | return fiber.child; 311 | } 312 | 313 | let nextFiber = fiber; 314 | 315 | while (nextFiber) { 316 | if (nextFiber.sibling) { 317 | return newFiber.sibling; 318 | } 319 | nextFiber = nextFiber.parent; 320 | } 321 | } 322 | ``` 323 | 324 | ## 5. render and commit phases 325 | 여기서 문제가 있다. 326 | 327 | 우리는 각 element에 대하여 dom에 새 node를 달아주고 있는데, 만약 모든 dom을 그리기전에 브라우저가 제어권을 뺏어간다면 rendering이 끝나기전에 불완전한 UI를 보여주고 말 것이다. 328 | 329 | 그래서 performUnitOfWork에서 dom을 매번 추가하는 부분을 지우고, fiber tree의 root를 wiproot라고 이름 짓고 이를 추적할 것이다. 330 | 331 | ```js 332 | // performUnitOfWork 지울부분 333 | if (fiber.parent) { 334 | fiber.parent.dom.appendChild(fiber.dom); 335 | } 336 | ``` 337 | 338 | ``` js 339 | function render(element, container) { 340 | wipRoot = { 341 | dom: container, 342 | props: { 343 | children: [element], 344 | }, 345 | }; 346 | nextUnitOfWork = wipRoot; 347 | } 348 | ``` 349 | 350 | 다음 실행 단위가 없어, 모든 실행 단위가 끝이 나면 비로소 dom에 commit한다. 이를 commitRoot 함수에서 실행한다. 351 | 352 | ``` js 353 | function workLoop(deadline) { 354 | let shouldYield = false; 355 | while (nextUnitOfWork && !shouldYield) { 356 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork); 357 | shouldYield = deadline.timeRemaining() < 1; 358 | } 359 | 360 | if (!nextUnitOfWork && wipRoot){ 361 | commitRoot() 362 | } 363 | 364 | requestIdleCallback(workLoop); 365 | } 366 | ``` 367 | 368 | ```js 369 | function commitRoot() { 370 | commitWork(wipRoot.child); 371 | wipRoot = null; 372 | } 373 | 374 | function commitWork(fiber) { 375 | if (!fiber) { 376 | return; 377 | } 378 | const domParent = fiber.parent.dom; 379 | domParent.appendChild(fiber.dom); 380 | commitWork(fiber.child); 381 | commitWork(fiber.sibling); 382 | } 383 | ``` 384 | ## 6. reconciliation 385 | 지금까지 dom에 추가하는 기능을 구현했다. 이제는 update와 unmount을 구현해보자. 386 | 387 | 우선 마지막 추가한 마지막 fiber tree를 currentRoot로 저장한다. 388 | 389 | 또한, alternate라는 프로퍼티를 fiber에 추가해 이를 기억하게 한다. 390 | ```js 391 | function render(element, container) { 392 | wipRoot = { 393 | dom: container, 394 | props: { 395 | children: [element], 396 | }, 397 | alternate: currentRoot, 398 | }; 399 | 400 | nextUnitOfWork = wipRoot; 401 | } 402 | ``` 403 | 404 | performUnitOfWork에서 자식 fiber를 그리는 부분을 추출해서 reconcileChildren() 함수로 다시 만든다. 405 | 406 | 이제는 자식 fiber를 그리는 동안 이전 oldFiber와 달라진 점이 없는지 체크를 할 것이다. 407 | 408 | 비교를 위해서는 타입을 사용한다. 409 | 410 | 같은 타입이라면 기존 노드를 사용하고 props만 업데이트 해준다. 411 | 412 | 타입이 다르고 새 element가 있다면 새로운 dom 노드를 생성한다. 413 | 414 | 타입이 다르고 기존 fiber만 있다면 예전 node를 지워야한다. 415 | 416 | 이 과정에서 실제 리액트는 key를 가지고 비교를 한다. (element 배열에서 효과적일 것이다.) 417 | 418 | 이를 effectTag라는 새로운 프로퍼티를 추가해 표시해 둘 것이다. 419 | 420 | props만 업데이트 될 경우에는 UPDATE를, 새 dom 노드 추가는 PLACEMENT 표기를 해두어 commit시에 활용한다. 421 | 422 | 삭제는 DELETION list를 따로 만들어 관리한다. 423 | 424 | ```js 425 | function reconcileChildren(wipFiber, elements) { 426 | let index = 0; 427 | let oldFiber = wipFiber.alternate && wipFiber.alternate.child; 428 | let prevSibling = null; 429 | 430 | while (index < elements.lenth || oldFiber != null) { 431 | const element = elements[index]; 432 | let newFiber = null; 433 | const sameType = oldFiber && element && element.type == oldFiber.type; 434 | 435 | if (sameType) { 436 | newFiber = { 437 | type: oldFiber.type, 438 | props: element.props, 439 | dom: oldFiber.dom, 440 | parent: wipFiber, 441 | alternate: oldFiber, 442 | effectTag: "UPDATE", 443 | }; 444 | } 445 | 446 | if (element && !sameType) { 447 | newFiber = { 448 | type: element.type, 449 | props: element.props, 450 | dom: null, 451 | parent: wipFiber, 452 | alternate: null, 453 | effectTag: "PLACEMENT", 454 | }; 455 | } 456 | 457 | if (oldFiber && !sameType) { 458 | oldFiber.effectTag = "DELETION"; 459 | deletions.push(oldFiber); 460 | } 461 | 462 | if (oldFiber) { 463 | oldFiber = oldFiber.sibling; 464 | } 465 | 466 | if (index == 0) { 467 | fiber.child = newFiber; 468 | } else { 469 | prevSibling.sibling = newFiber; 470 | } 471 | 472 | prevSibling = newFiber; 473 | index++; 474 | } 475 | } 476 | ``` 477 | 478 | 이제 commitwork 에서 effectTags에 따라 반영한다. 479 | ``` js 480 | function commitRoot() { 481 | deletions.forEach(commitWork); 482 | commitWork(wipRoot.child); 483 | currentRoot = wipRoot; 484 | wipRoot = null; 485 | } 486 | 487 | function commitWork(fiber) { 488 | if (!fiber) { 489 | return; 490 | } 491 | 492 | const domParent = fiber.parent.dom; 493 | 494 | if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) { 495 | domParent.appendChild(fiber.dom); 496 | } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) { 497 | updateDom(fiber.dom, fiber.alternate.props, fiber.props); 498 | } else if (fiber.effectTag === "DELETION") { 499 | domParent.removeChild(fiber.dom); 500 | } 501 | 502 | commitWork(fiber.child); 503 | commitWork(fiber.sibling); 504 | } 505 | ``` 506 | update tag에 대해서는 기존 propety들을 교체해주는 작업을 해주는 updateDom 함수를 만든다. 507 | 508 | 이벤트 리스터 property는 "on" 으로 시작하는 특수한 케이스로 예외처리해준다. 509 | 510 | ``` js 511 | const isEvent = (key) => key.startsWith("on"); 512 | const isProperty = (key) => key !== "children" && !isEvent(key); 513 | const isNew = (prev, next) => (key) => prev[key] !== next[key]; 514 | const isGone = (prev, next) => (key) => !(key in next); 515 | 516 | function updateDom(dom, prevProps, nextProps) { 517 | // remove old or changed event listeners 518 | Object.keys(prevProps) 519 | .filter(isEvent) 520 | .filter((key) => { 521 | !(key in nextProps) || isNew(prevProps, nextProps)(key); 522 | }) 523 | .forEach((name) => { 524 | const eventType = name.toLowerCase().substring(2); 525 | dom.removeEventListener(eventType, prevProps[name]); 526 | }); 527 | 528 | // remove old properties 529 | Object.keys(prevProps) 530 | .filter(isProperty) 531 | .filter(isGone(prevProps, nextProps)) 532 | .forEach((name) => { 533 | dom[name] = ""; 534 | }); 535 | 536 | // add new properties 537 | Object.keys(nextProps) 538 | .filter(isProperty) 539 | .filter(isNew(prevProps, nextProps)) 540 | .forEach((name) => { 541 | dom[name] = nextProps[name]; 542 | }); 543 | 544 | // add new event listeners 545 | Object.keys(prevProps) 546 | .filter(isEvent) 547 | .filter(isNew(prevProps, nextProps)) 548 | .forEach((name) => { 549 | const eventType = name.toLowerCase().substring(2); 550 | dom.addEventListener(eventType, prevProps[name]); 551 | }); 552 | } 553 | ``` 554 | 555 | ## 7. function components 556 | 다음 추가할 기능은 함수형 컴포넌트이다. 557 | 558 | 함수형 컴포넌트의 차이점은, fiber가 dom node(container)를 갖지 않는다는 점이고, childeren props에서 가져오는 것이 아니라 함수에서 리턴한다는 점이다. 559 | 560 | 우리는 fiber의 타입을 체크해서 함수라면 다른 update 함수를 사용하도록 할 것이다. 561 | 562 | updateHostComponent는 기존 함수와 같고, updateFunctionComponent는 함수로 자식을 가져오도록 할것이다. 563 | 564 | ```js 565 | function performUnitOfWork(fiber) { 566 | const isFunctionComponent = fiber.type instanceof Function; 567 | if (isFunctionComponent) { 568 | updateFunctionComponent(fiber); 569 | } else { 570 | updateHostComponent(fiber); 571 | } 572 | 573 | if (fiber.child) { 574 | return fiber.child; 575 | } 576 | 577 | let nextFiber = fiber; 578 | 579 | while (nextFiber) { 580 | if (nextFiber.sibling) { 581 | return newFiber.sibling; 582 | } 583 | nextFiber = nextFiber.parent; 584 | } 585 | } 586 | 587 | function updateFunctionComponent(fiber) { 588 | const children = [fiber.type(fiber.props)]; 589 | reconcileChildren(fiber, children); 590 | } 591 | 592 | function updateHostComponent(fiber) { 593 | if (!fiber.dom) { 594 | fiber.dom = createDom(fiber); 595 | } 596 | reconcileChildren(fiber, fiber.props.children); 597 | } 598 | ``` 599 | 600 | commitWork에서 바꿔야할 부분이 있다. 부모를 찾는 부분에서는 dom 노드를 가진 fiber를 찾을때까지 찾도록 하는 것이다. 601 | 602 | 삭제하는 부분도 dom 노들를 가진 자식을 찾을때까지 찾도록 해야 한다. 603 | 604 | ```js 605 | function commitWork(fiber) { 606 | if (!fiber) { 607 | return; 608 | } 609 | 610 | let domParentFiber = fiber.parent; 611 | while (!domParentFiber.dom) { 612 | domParentFiber = domParentFiber.parent; 613 | } 614 | const domParent = domParentFiber; 615 | 616 | if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) { 617 | domParent.appendChild(fiber.dom); 618 | } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) { 619 | updateDom(fiber.dom, fiber.alternate.props, fiber.props); 620 | } else if (fiber.effectTag === "DELETION") { 621 | commitDeletion(fiber, domParent); 622 | } 623 | commitWork(fiber.child); 624 | commitWork(fiber.sibling); 625 | } 626 | 627 | function commitDeletion(fiber, domParent) { 628 | if (fiber.dom) { 629 | domParent.removeChild(fiber.dom); 630 | } else { 631 | commitDeletion(fiber.child, domParent); 632 | } 633 | } 634 | ``` 635 | ## 8. hooks 636 | 마지막으로 함수형 컴포넌트에 state를 추가하자. 637 | 638 | 먼저 useState에 필요한 전역변수를 선언한다. 639 | 640 | useState가 한 컴포넌트에서 여러번 호출되었을 때를 위해 hooks 배열도 만든다. 641 | 642 | ```js 643 | let wipFiber = null 644 | let hookIndex = null 645 | 646 | function updateFunctionComponent(fiber) { 647 | wipFiber = fiber 648 | hookIndex = 0 649 | wipFiber.hooks = [] 650 | const children = [fiber.type(fiber.props)]; 651 | reconcileChildren(fiber, children); 652 | } 653 | ``` 654 | 655 | useState가 호출되면, 이전에 호출된 hook이 있는지 확인한다. 656 | 657 | hook이 있다면 이전 hook에서 state를 복사해 온다. 658 | 659 | 새로운 hook을 fiber에 넣어주고 state를 반환한다. 660 | 661 | useState는 상태를 변환시키기 위한 함수를 반환해야한다. 662 | 663 | 그래서 setState를 선언해 action을 받도록 선언해야하고, 이 action을 hook에 큐에 삽입한다. 664 | 665 | 그 다음은 마치 render가 작동하듯이 차근차근 다음 실행단위를 실행시키도록 한다. 666 | 667 | 이 action들은 우리가 다음 랜더링을 할 때 old hook 에 쌓여있던 action들이 실행되며 실행된다. 668 | ```js 669 | function useState(initial) { 670 | const oldHook = 671 | webfiber.alternate && 672 | webFiber.alternate.hooks && 673 | webFiber.alternate.hooks[hookIndex]; 674 | 675 | const hook = { status: oldHook ? oldHook.state : initial, queue: [] }; 676 | 677 | const action = oldHook ? oldHook.queue : [] 678 | action.forEach(action => { 679 | hook.state = action(hook.state) 680 | }) 681 | 682 | const setState = (action) => { 683 | hook.queue.push(action); 684 | wipRoot = { 685 | dom: currnetRoot.Dom, 686 | props: currnetRoot.props, 687 | alternate: currnetRoot, 688 | }; 689 | (nextUnitOfWork = wipRoot), (deletions = []); 690 | }; 691 | 692 | wipFiber.hooks.push(hook); 693 | hookIndex++; 694 | return [hook.state, setState]; 695 | } 696 | ``` 697 | --------------------------------------------------------------------------------