-
Notifications
You must be signed in to change notification settings - Fork 0
[DongHyun] Virtual DOM #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
D5ng
wants to merge
16
commits into
donghyun
Choose a base branch
from
donghyun_dev
base: donghyun
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
033f5ce
📝 Docs: Readme.md
D5ng 96346ae
✨ Feat: 가상돔 만들기
D5ng 83cfe1c
🔧 Chore: esbuild 옵션 추가(JSX 트랜스파일링)
D5ng c6e17b9
🔧 Chore: tsconfig path 추가
D5ng 1d7e51e
🚚 Rename: 확장자명 변경
D5ng 467dc24
✨ Feat: VDOM을 RealDOM으로 변환 작업
D5ng 595326c
✨ Feat: Fragment 기능 추가
D5ng 01b7462
♻️ Refactor: createElement 함수 리팩토링
D5ng e76ef7b
♻️ Refactor: 널리쉬 연산자를 사용해 props 기본값 지정하기
D5ng ec3b3dc
♻️ Refactor: 헬퍼 함수 주석처리
D5ng f03f1c6
♻️ Refactor: jsx-runtime 파일 단위 리팩토링
D5ng 49432e1
🐛 Fix: 렌더링 최적화 오류 해결
D5ng 7d4d3dc
🐛 Fix: Fragment 버그 수정
D5ng bb01fc4
♻️ Refactor: diffTextDOM 함수 선언문으로 변경
D5ng 0852f4c
♻️ Refactor: 네이밍 변경
D5ng 7029af6
♻️ Refactor: 파일 이름 변경 및 타입 분리
D5ng File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,3 +22,6 @@ dist-ssr | |
| *.njsproj | ||
| *.sln | ||
| *.sw? | ||
|
|
||
| yarn.lock | ||
| install-state.gz | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| # Vanilla Javascript로 Virtual DOM 만들기 | ||
|
|
||
| 황준일님의 Vanilla Javascript로 Virtual DOM 만들기 아티클을 참고하여, 브라우저 렌더링 과정에 대해 이해하고, 성능적인 고민과 Virtual DOM이 해결하려는 문제를 파악하자 | ||
|
|
||
| - 구현하면서 느낀 점들을 작성하며 회고하자. | ||
| - Virtual DOM이 해결하려는 문제를 인지하면서 구현해보자. | ||
| - 모르는 것들이나 새롭게 알게 된 것들이 있다면 적극적으로 공유하며 서로의 학습을 확장해 보자. | ||
|
|
||
| [황준일님 Vanilla Javascript로 Virtual DOM 만들기 링크](https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Virtual-DOM/#_1-virtualdom%E1%84%8B%E1%85%B5-%E1%84%81%E1%85%A9%E1%86%A8-%E1%84%91%E1%85%B5%E1%86%AF%E1%84%8B%E1%85%AD%E1%84%92%E1%85%A1%E1%86%AB%E1%84%80%E1%85%A1) | ||
|
|
||
| ## 🚀 학습 목표 | ||
|
|
||
| 1. 브라우저 렌더링 과정에 대해 명확히 이해하자. | ||
| 2. Virtual DOM이 해결하려는 문제를 파악하고, 어떻게 개선할 수 있는지를 파악해보자. | ||
| 3. Diff 알고리즘이 어떤 방식으로 동작하는 이해하자. | ||
| 4. Virtual DOM을 사용했을 때와 사용하지 않았을 때를 비교해보면서 느껴보자. | ||
|
|
||
| ## 📝 Pull Request 주의할 점. | ||
|
|
||
| - **학습과 고민의 흔적 기록** | ||
| - 본인이 배우고 고민했던 점들을 최대한 꼼꼼하게 작성해주세요. | ||
| - 작성된 내용은 리뷰어가 의도를 이해하고 더 나은 피드백을 제공하는 데 큰 도움이 됩니다. | ||
| - **코드리뷰는 존중과 솔직함을 기반으로** | ||
| - 서로 상처받지 않도록 배려하며 리뷰를 작성해주세요. | ||
| - 하지만, 서로의 발전을 위해 솔직한 피드백을 주고받는 문화를 지향합시다. | ||
| - **PR 방식** | ||
| - [본인 이름] Virtual DOM (본인 이름 브랜치에 PR 올려주세요) | ||
| - 위와 같은 방식으로 PR 올려주세요. | ||
|
|
||
| ## 🤖 GPT의 아티클 요약 | ||
|
|
||
| # 브라우저의 렌더링 과정 | ||
|
|
||
| ## 1. HTML과 CSS 파싱 | ||
|
|
||
| - 브라우저는 HTML과 CSS를 읽어 DOM 트리와 CSSOM 트리를 생성합니다. | ||
| - 이후, DOM과 CSSOM을 결합하여 렌더 트리를 구성합니다. | ||
|
|
||
| ## 2. 레이아웃과 페인트 | ||
|
|
||
| - **레이아웃**: 각 요소의 위치와 크기를 계산합니다. | ||
| - **페인트**: 렌더 트리를 픽셀로 변환하여 화면에 표시합니다. | ||
|
|
||
| ## 3. 성능 문제 | ||
|
|
||
| - DOM이나 CSS에 변화가 생길 경우, 브라우저는 **reflow(레이아웃 다시 계산)**나 **repaint(화면 다시 그림)**를 실행합니다. | ||
| - 이러한 작업은 성능 저하의 주요 원인이 됩니다. | ||
|
|
||
| --- | ||
|
|
||
| # 가상 DOM(Virtual DOM)의 필요성 | ||
|
|
||
| - DOM 조작이 복잡하고 잦아질수록 브라우저의 렌더링 성능이 저하됩니다. | ||
| - **가상 DOM**은 이러한 문제를 해결하기 위해 도입된 개념입니다. | ||
| - 메모리 상에서 DOM 구조를 본뜬 **자바스크립트 객체**를 생성합니다. | ||
| - 변경 사항을 가상 DOM에서 비교(diff)하고, 실제 DOM에 **최소한의 업데이트**만 적용합니다. | ||
|
|
||
| --- | ||
|
|
||
| # 가상 DOM의 기본 원리 | ||
|
|
||
| ## 1. 가상 DOM 생성 | ||
|
|
||
| - 실제 DOM을 대신할 **자바스크립트 객체**를 만듭니다. | ||
| - 각 요소는 객체로 표현되며, 속성, 자식 요소, 이벤트 등을 포함합니다. | ||
|
|
||
| ## 2. 가상 DOM에서 실제 DOM 생성 | ||
|
|
||
| - 가상 DOM 객체를 기반으로 실제 DOM 요소를 동적으로 생성합니다. | ||
| - 이 과정은 초기 렌더링 시에만 수행됩니다. | ||
|
|
||
| ## 3. Diff 알고리즘 | ||
|
|
||
| - 이전 가상 DOM과 새로운 가상 DOM을 비교합니다. | ||
| - 변경된 부분만 찾아 실제 DOM에 업데이트합니다. | ||
| - 이를 통해 불필요한 렌더링 작업을 줄이고 성능을 최적화합니다. | ||
|
|
||
| ## 4. 최소 DOM 조작 | ||
|
|
||
| - 가상 DOM에서 찾은 차이점만 실제 DOM에 반영합니다. | ||
| - 이를 통해 브라우저의 렌더링 부담을 줄이고 효율성을 높입니다. | ||
|
|
||
| --- | ||
|
|
||
| # Diff 알고리즘의 작동 방식 | ||
|
|
||
| ## 1. 요소 비교 | ||
|
|
||
| - 두 가상 DOM 객체를 비교하여 같은 요소인지 판단합니다. | ||
| - 만약 타입이 다르면 해당 요소를 통째로 교체합니다. | ||
|
|
||
| ## 2. 속성 비교 | ||
|
|
||
| - 두 요소의 속성을 비교하여 변경된 속성만 업데이트합니다. | ||
| - 속성이 제거된 경우, 실제 DOM에서도 제거합니다. | ||
|
|
||
| ## 3. 자식 노드 비교 | ||
|
|
||
| - 자식 노드를 순회하며 재귀적으로 diff를 수행합니다. | ||
| - 추가되거나 삭제된 자식 노드만 업데이트합니다. | ||
|
|
||
| --- | ||
|
|
||
| # 가상 DOM의 장점 | ||
|
|
||
| ## 1. 성능 최적화 | ||
|
|
||
| - 변경된 부분만 DOM에 반영하므로, 브라우저의 작업량을 줄일 수 있습니다. | ||
| - 렌더링 과정에서 발생하는 reflow와 repaint를 최소화합니다. | ||
|
|
||
| ## 2. 코드 유지보수성 향상 | ||
|
|
||
| - DOM 조작 코드의 복잡성을 줄이고, 선언형 프로그래밍 방식을 채택할 수 있습니다. | ||
| - React나 Vue와 같은 라이브러리가 이러한 방식을 활용합니다. | ||
|
|
||
| ## 3. 추상화 | ||
|
|
||
| - 개발자는 DOM 조작의 세부 사항을 신경 쓰지 않아도 됩니다. | ||
| - 변경 사항을 선언적으로 표현하면, 가상 DOM이 알아서 처리합니다. | ||
|
|
||
| --- | ||
|
|
||
| # 바닐라 자바스크립트로 구현하는 방법 | ||
|
|
||
| - 가상 DOM을 이해하기 위해 바닐라 자바스크립트로 간단한 버전을 구현할 수 있습니다. | ||
| - 주요 단계는 다음과 같습니다: | ||
| 1. **가상 DOM 구조 정의**: 가상 DOM을 표현하는 데이터 구조를 설계합니다. | ||
| 2. **가상 DOM을 실제 DOM으로 변환**: 가상 DOM 객체를 기반으로 실제 DOM 요소를 생성합니다. | ||
| 3. **Diff 알고리즘 구현**: 이전 상태와 새로운 상태를 비교하여 변경 사항을 찾습니다. | ||
| 4. **DOM 업데이트**: Diff 결과를 바탕으로 최소한의 DOM 조작을 수행합니다. | ||
|
|
||
| --- | ||
|
|
||
| # 컴포넌트와 상태 관리의 통합 | ||
|
|
||
| - 가상 DOM은 **컴포넌트 기반 개발**과 **상태 관리**에도 적합합니다. | ||
| - 각 컴포넌트는 자신의 상태(state)를 관리합니다. | ||
| - 상태 변화가 발생하면 해당 컴포넌트만 다시 렌더링합니다. | ||
| - 이를 통해 애플리케이션의 **구조화**와 **성능 최적화**가 동시에 이루어집니다. | ||
|
|
||
| --- | ||
|
|
||
| # 결론 | ||
|
|
||
| - 가상 DOM은 복잡한 DOM 조작 문제를 해결하기 위한 강력한 도구입니다. | ||
| - React와 Vue 같은 라이브러리가 이를 활용하여 선언형 프로그래밍과 성능 최적화를 제공합니다. | ||
| - 바닐라 자바스크립트로 간단한 가상 DOM을 구현해 보면, 가상 DOM의 내부 원리를 이해하고 DOM 조작에 대한 깊은 통찰을 얻을 수 있습니다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { useState } from './lib/core/hooks'; | ||
|
|
||
| function Wrapper() { | ||
| return ( | ||
| <> | ||
| <p>HelloWorld</p> | ||
| </> | ||
| ); | ||
| } | ||
|
|
||
| export default function App() { | ||
| const [count, setCount] = useState(1); | ||
| const handleIncrease = () => { | ||
| setCount(count + 1); | ||
| }; | ||
|
|
||
| // return ( | ||
| // <div> | ||
| // <p>{count}</p> | ||
| // <button onClick={handleIncrease}>+1</button> | ||
| // </div> | ||
| // ); | ||
|
|
||
| return ( | ||
| <> | ||
| <p>{count}</p> | ||
| <button onClick={handleIncrease}>+1</button> | ||
| <Wrapper /> | ||
| </> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| declare namespace JSX { | ||
| interface IntrinsicElements { | ||
| [elemName: string]: any; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { updateElement } from '@/lib/jsx/jsx-runtime'; | ||
| import { Component } from '../jsx/jsx-runtime.type'; | ||
| import { Internals } from './hooks.type'; | ||
|
|
||
| export const { useState, render } = (function () { | ||
| const INTERNALS: Internals = { | ||
| rootElement: null, | ||
| rootComponent: null, | ||
| currentVDOM: null, | ||
| states: [], | ||
| hookIndex: 0, | ||
| }; | ||
|
|
||
| const render = (rootElement: HTMLElement, component: Component) => { | ||
| INTERNALS.rootElement = rootElement; | ||
| INTERNALS.rootComponent = component; | ||
| _render(); | ||
| }; | ||
|
|
||
| const _render = () => { | ||
| const newVDOM = INTERNALS.rootComponent!(); | ||
| updateElement(INTERNALS.rootElement!, newVDOM, INTERNALS.currentVDOM); | ||
| INTERNALS.hookIndex = 0; | ||
| INTERNALS.currentVDOM = newVDOM; | ||
| }; | ||
|
|
||
| const useState = <T>(initialState: T) => { | ||
| const index = INTERNALS.hookIndex; | ||
| const state = INTERNALS.states[index] ?? initialState; | ||
|
|
||
| const setState = (newState: T) => { | ||
| INTERNALS.states[index] = newState; | ||
| _render(); | ||
| }; | ||
|
|
||
| INTERNALS.hookIndex++; | ||
|
|
||
| return [state, setState]; | ||
| }; | ||
|
|
||
| return { useState, render }; | ||
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { VDOM, Component } from '../jsx/jsx-runtime.type'; | ||
|
|
||
| export interface Internals { | ||
| rootElement: HTMLElement | null; | ||
| rootComponent: Component | null; | ||
| currentVDOM: null | VDOM; | ||
| states: any[]; | ||
| hookIndex: number; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { h } from '@/lib/jsx/jsx-runtime'; | ||
| import { FRAGMENT } from './jsx-runtime.constant'; | ||
|
|
||
| export { h, FRAGMENT }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export const FRAGMENT = 'FRAGMENT' as const; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import { isHTMLElement, isNullOrUndefined, isPrimitive, isVDOM } from './jsx-runtime.util'; | ||
| import { FRAGMENT } from './jsx-runtime.constant'; | ||
| import type { Type, Props, VNode, VDOM } from './jsx-runtime.type'; | ||
|
|
||
| export function h(type: Type, props: Props, ...children: VNode[]): VDOM { | ||
| props = props ?? {}; | ||
| if (typeof type === 'function') return type(props); | ||
| return { type, props, children: children.flat() }; | ||
| } | ||
|
|
||
| export function createElement(node: VNode) { | ||
| if (isNullOrUndefined(node)) { | ||
| return document.createDocumentFragment(); | ||
| } | ||
|
|
||
| if (isPrimitive(node)) { | ||
| return document.createTextNode(String(node)); | ||
| } | ||
|
|
||
| const element = | ||
| node.type === FRAGMENT | ||
| ? document.createDocumentFragment() | ||
| : document.createElement(node.type as keyof HTMLElementTagNameMap); | ||
|
|
||
| if (isHTMLElement(element)) { | ||
| elementSetAttribute(element, node.props); | ||
| } | ||
|
|
||
| node.children.map(createElement).forEach((child) => element.appendChild(child)); | ||
|
|
||
| return element; | ||
| } | ||
|
|
||
| function elementSetAttribute(element: HTMLElement, props: Props = {}) { | ||
| Object.entries(props) | ||
| .filter(([_, value]) => value) | ||
| .forEach(([attr, value]) => { | ||
| if (attr.startsWith('on') && typeof props[attr] === 'function') { | ||
| const eventType = attr.slice(2).toLowerCase(); | ||
| element.addEventListener(eventType, props[attr]); | ||
| } | ||
|
|
||
| element.setAttribute(attr, value); | ||
| }); | ||
| } | ||
|
|
||
| function diffTextVDOM(newVDOM: VNode, currentVDOM: VNode) { | ||
| if (JSON.stringify(newVDOM) === JSON.stringify(currentVDOM)) return false; | ||
| if (typeof newVDOM === 'object' || typeof currentVDOM === 'object') return false; | ||
| return true; | ||
| } | ||
|
|
||
| export function updateElement(parent: HTMLElement, newNode?: VNode, oldNode?: VNode, index: number = 0) { | ||
| if (!newNode && oldNode) { | ||
| parent.removeChild(parent.childNodes[index]); | ||
| return; | ||
| } | ||
|
|
||
| if (newNode && !oldNode) { | ||
| parent.appendChild(createElement(newNode)); | ||
| return; | ||
| } | ||
|
|
||
| if (!parent.childNodes[index]) return; | ||
|
|
||
| if (diffTextVDOM(newNode, oldNode)) { | ||
| parent.replaceChild(createElement(newNode), parent.childNodes[index]); | ||
| return; | ||
| } | ||
|
|
||
| if (!isVDOM(oldNode) || !isVDOM(newNode)) return; | ||
|
|
||
| const maxLength = Math.max(newNode!.children.length, oldNode!.children.length); | ||
|
|
||
| if (newNode.type === FRAGMENT || oldNode.type === FRAGMENT) { | ||
| const fragmentElement = parent.childNodes[index]?.parentNode || parent; | ||
| Array.from({ length: maxLength }).forEach((_, i) => | ||
| updateElement(fragmentElement as HTMLElement, newNode.children[i], oldNode.children[i], i), | ||
| ); | ||
| } | ||
|
|
||
| if (newNode.type !== oldNode.type) { | ||
| parent.replaceChild(createElement(newNode), parent.childNodes[index]); | ||
| return; | ||
| } | ||
|
|
||
| if (parent.childNodes.length) { | ||
| updateAttributes(parent.childNodes[index] as HTMLElement, newNode.props, oldNode.props); | ||
| } | ||
|
|
||
| Array.from({ length: maxLength }).forEach((_, i) => | ||
| updateElement(parent.childNodes[index] as HTMLElement, newNode.children[i], oldNode.children[i], i), | ||
| ); | ||
| } | ||
|
|
||
| function updateAttributes(target: HTMLElement, newProps: Props = {}, oldProps: Props = {}) { | ||
| for (const [attr, value] of Object.entries(newProps)) { | ||
| if (oldProps[attr] === newProps[attr]) continue; | ||
| if (attr.startsWith('on') && typeof newProps[attr] === 'function') { | ||
| const eventType = attr.slice(2).toLowerCase(); | ||
| target.removeEventListener(eventType, oldProps[attr]); | ||
| target.addEventListener(eventType, newProps[attr]); | ||
| } | ||
|
|
||
| target.setAttribute(attr, value); | ||
| } | ||
|
|
||
| for (const attr of Object.keys(oldProps)) { | ||
| if (newProps[attr] !== undefined) continue; | ||
| target.removeAttribute(attr); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { FRAGMENT } from './jsx-runtime.constant'; | ||
|
|
||
| export type Fragment = typeof FRAGMENT; | ||
| export type Type = keyof HTMLElementTagNameMap | Component | Fragment; | ||
| export type Props = Record<string, any>; | ||
| export type VNode = string | number | null | undefined | VDOM; | ||
| export type Component = (props?: Props) => VDOM; | ||
|
|
||
| export interface VDOM { | ||
| type: Type; | ||
| props: Props; | ||
| children: VNode[]; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 주석은 일부러 남겨둔 것인가요??