From daa5e2501db9d7a88aca46308fde44aa80645f90 Mon Sep 17 00:00:00 2001 From: d5ng Date: Wed, 15 Jan 2025 11:40:47 +0900 Subject: [PATCH 01/32] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20add=20readme.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 readme.md diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..28c946e --- /dev/null +++ b/readme.md @@ -0,0 +1,102 @@ +# React Hooks 만들어보면서 이해하기 + +황준일님의 useState Hooks 만들기를 참고하여, 훅 작성 규칙 및 클로저를 어떤 식으로 활용할 수 있을지 이해와 고민하자. 또한, useState뿐 아니라, useCallback, useMemo, useEffect를 어떤 식으로 구현할 수 있을지도 생각해보자. + +> [!NOTE] +> 위 4가지의 hooks을 만들었다면, useRef도 해보자! + +- 구현하면서 느낀 점들을 작성하며 회고하자. +- Hooks의 규칙에 대해 이해하자. +- 클로저에 대해 명확하게 이해하자. +- 메모이제이션을 어떻게 구현할 수 있을지 고민해보자. +- 모르는 것들이나 새롭게 알게 된 것들이 있다면 적극적으로 공유하며 서로의 학습을 확장해 보자. + +[황준일님 Vanilla Javascript로 React UseState Hook 만들기 링크](https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Make-useSate-hook/) + +## 🚀 학습 목표 + +- React Hook을 이해하고 규칙과 연관지어 설명할 수 있다. +- 클로저를 이해하고, 설명할 수 있다 +- React에서 메모이제이션 및 사이드 이펙트를 어떤 방식으로 처리하는지 추론할 수 있다. + +## 📝 Pull Request 주의할 점. + +- **학습과 고민의 흔적 기록** + - 본인이 배우고 고민했던 점들을 최대한 꼼꼼하게 작성해주세요. + - 작성된 내용은 리뷰어가 의도를 이해하고 더 나은 피드백을 제공하는 데 큰 도움이 됩니다. +- **코드리뷰는 존중과 솔직함을 기반으로** + - 서로 상처받지 않도록 배려하며 리뷰를 작성해주세요. + - 하지만, 서로의 발전을 위해 솔직한 피드백을 주고받는 문화를 지향합시다. +- **PR 방식** + - [본인 이름] React Hooks (본인 이름 브랜치에 PR 올려주세요) + - 위와 같은 방식으로 PR 올려주세요. + +## 🤖 GPT의 아티클 요약 + +# React의 `useState`를 바닐라 JS로 구현하기 + +React의 상태 관리 메커니즘인 `useState` 훅을 바닐라 자바스크립트로 구현하는 과정을 다룬 글입니다. React의 상태 관리 및 렌더링 동작 방식을 깊이 있게 이해할 수 있습니다. + +--- + +## 주요 내용 + +### 1. React의 `useState` 이해 + +- **React의 상태 관리 특징**: + + - React에서는 상태(`state`)가 컴포넌트 외부에서 관리됩니다. + - 상태는 컴포넌트 함수가 반복적으로 호출되더라도 초기화되지 않고 유지됩니다. + +- **`useState`의 역할**: + - 상태와 상태 업데이트 함수(`setState`)를 제공합니다. + - 상태 변경 시 컴포넌트를 재렌더링하여 UI를 업데이트합니다. + +--- + +### 2. 바닐라 JS에서 `useState` 구현 + +- **목표**: + + - React의 `useState`와 유사한 기능을 바닐라 자바스크립트로 구현합니다. + +- **핵심 기능**: + - 상태를 외부에서 배열로 관리하여 초기화 및 유지. + - 상태 변경 시 업데이트 함수(`setState`)를 통해 상태를 변경. + - 상태 변경 시 렌더 함수(`render()`) 호출로 컴포넌트를 재렌더링. + +--- + +### 3. 렌더링 관리 + +- 상태 변경 시 컴포넌트를 다시 렌더링하도록 설정합니다. +- 렌더링이 발생할 때 모든 상태 인덱스를 초기화하여 올바른 상태를 참조하도록 합니다. + +--- + +### 4. 최적화 + +- **불필요한 렌더링 방지**: + + - 상태가 변경되지 않으면 렌더링을 생략합니다. + +- **다중 상태 관리**: + + - 상태를 배열로 관리하여 효율성을 높입니다. + +- **컴포넌트 재사용성**: + - 상태 관리 로직과 렌더링 로직을 분리하여 유지보수와 재사용성을 강화합니다. + +--- + +## 학습 포인트 + +1. React의 상태 관리 메커니즘을 바닐라 자바스크립트로 직접 구현하며 학습할 수 있습니다. +2. 상태 저장, 업데이트, 렌더링 동작을 이해하며 React의 내부 동작 원리를 더 잘 알게 됩니다. +3. React 외의 환경에서도 상태 관리를 구현할 수 있는 방법론을 제공합니다. + +--- + +## 결론 + +React의 `useState` 훅을 직접 구현하며, 상태 관리의 기본 개념을 학습할 수 있습니다. 또한 상태와 렌더링 로직을 분리하여 코드의 유지보수성과 재사용성을 높이는 방법을 익힐 수 있습니다. From 235bd577203f191113bf74af3dcaf959722ba9da Mon Sep 17 00:00:00 2001 From: d5ng Date: Wed, 15 Jan 2025 11:42:09 +0900 Subject: [PATCH 02/32] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20readme.md=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 28c946e..5a98026 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ 황준일님의 useState Hooks 만들기를 참고하여, 훅 작성 규칙 및 클로저를 어떤 식으로 활용할 수 있을지 이해와 고민하자. 또한, useState뿐 아니라, useCallback, useMemo, useEffect를 어떤 식으로 구현할 수 있을지도 생각해보자. > [!NOTE] -> 위 4가지의 hooks을 만들었다면, useRef도 해보자! +> useState, useEffect, useCallback, useMemo를 구현했다면, 추가적으로 useRef도 구현해보자! - 구현하면서 느낀 점들을 작성하며 회고하자. - Hooks의 규칙에 대해 이해하자. From 0e55cb0e92b03aedb316f97fe6099f227d608bb8 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:21:21 +0900 Subject: [PATCH 03/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20JSX=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/jsx/index.ts | 3 + src/core/jsx/jsx-runtime.ts | 107 ++++++++++++++++++++++++++++++ src/core/jsx/jsx-runtime.type.ts | 13 ++++ src/core/jsx/jsx-runtime.utils.ts | 9 +++ 4 files changed, 132 insertions(+) create mode 100644 src/core/jsx/index.ts create mode 100644 src/core/jsx/jsx-runtime.ts create mode 100644 src/core/jsx/jsx-runtime.type.ts create mode 100644 src/core/jsx/jsx-runtime.utils.ts diff --git a/src/core/jsx/index.ts b/src/core/jsx/index.ts new file mode 100644 index 0000000..7f7bc12 --- /dev/null +++ b/src/core/jsx/index.ts @@ -0,0 +1,3 @@ +import { createElement, h } from "@/core/jsx/jsx-runtime" + +export { createElement, h } diff --git a/src/core/jsx/jsx-runtime.ts b/src/core/jsx/jsx-runtime.ts new file mode 100644 index 0000000..991e4aa --- /dev/null +++ b/src/core/jsx/jsx-runtime.ts @@ -0,0 +1,107 @@ +import { internals } from "../sharedInternals" +import type { Type, VDOM, Props, VNode } from "./jsx-runtime.type" +import { isNullOrUndefined, isPrimitive } from "./jsx-runtime.utils" + +function h(type: Type, props: Props, ...children: VNode[]): VDOM { + return { type, props: props || {}, children: children.flat() } +} + +function createElement(node: VNode) { + if (isNullOrUndefined(node)) { + return document.createDocumentFragment() + } + + if (isPrimitive(node)) { + return document.createTextNode(String(node)) + } + + const element = document.createElement(node.type) + + setAttribute(element, node.props) + node.children.map(createElement).forEach((child) => element.appendChild(child)) + + return element +} + +function setAttribute(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]) + } + + if (attr === "className") { + element.setAttribute("class", value) + return + } + + element.setAttribute(attr, value) + }) +} + +function updateAttributes(element: HTMLElement, newProps: Props = {}, oldProps: Props = {}) { + for (const [attr, value] of Object.entries(newProps)) { + if (newProps[attr] === oldProps[attr]) continue + + if (attr.startsWith("on") && typeof value === "function") { + const eventType = attr.slice(2).toLowerCase() + element.removeEventListener(eventType, oldProps[attr]) + element.addEventListener(eventType, newProps[attr]) + } + + element.setAttribute(attr, value) + } + + for (const [attr] of Object.entries(oldProps)) { + if (newProps[attr]) continue + element.removeAttribute(attr) + } +} + +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 (diffText(newNode, oldNode)) { + parent.replaceChild(createElement(newNode), parent.childNodes[index]) + return + } + + if (!isVDOM(newNode) || !isVDOM(oldNode)) return + + if (newNode.type !== oldNode.type) { + parent.replaceChild(createElement(newNode), parent.childNodes[index]) + return + } + + if (parent.childNodes[index]) { + updateAttributes(parent.childNodes[index] as HTMLElement, newNode.props, oldNode.props) + } + + const maxLength = Math.max(newNode.children.length, oldNode.children.length) + + for (let i = 0; i < maxLength; i++) { + updateElement(parent.childNodes[index] as HTMLElement, newNode.children[i], oldNode.children[i], i) + } +} + +function isVDOM(node: VNode) { + return typeof node === "object" && node !== null +} + +function diffText(newNode: VNode, oldNode: VNode) { + if (JSON.stringify(newNode) === JSON.stringify(oldNode)) return false + if (typeof newNode === "object" || typeof oldNode === "object") return false + return true +} + +export { h, createElement } diff --git a/src/core/jsx/jsx-runtime.type.ts b/src/core/jsx/jsx-runtime.type.ts new file mode 100644 index 0000000..1dbc188 --- /dev/null +++ b/src/core/jsx/jsx-runtime.type.ts @@ -0,0 +1,13 @@ +export type Type = keyof HTMLElementTagNameMap + +export type Props = Record + +export type VNode = string | number | null | undefined | VDOM + +export type Component = (props?: Props) => VDOM + +export interface VDOM { + type: Type + props: Record + children: VNode[] +} diff --git a/src/core/jsx/jsx-runtime.utils.ts b/src/core/jsx/jsx-runtime.utils.ts new file mode 100644 index 0000000..630293b --- /dev/null +++ b/src/core/jsx/jsx-runtime.utils.ts @@ -0,0 +1,9 @@ +import { VNode } from "./jsx-runtime.type" + +export function isNullOrUndefined(node: VNode) { + return node === null || node === undefined +} + +export function isPrimitive(node: VNode) { + return typeof node === "string" || typeof node === "number" +} From 306d4bd5e708fbf17d5fe932c4ae327ea870a465 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:21:38 +0900 Subject: [PATCH 04/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20Counter=20Component?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Counter.css | 3 +++ src/components/Counter.tsx | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/components/Counter.css create mode 100644 src/components/Counter.tsx diff --git a/src/components/Counter.css b/src/components/Counter.css new file mode 100644 index 0000000..b9a8f4e --- /dev/null +++ b/src/components/Counter.css @@ -0,0 +1,3 @@ +.count-wrapper { + color: red; +} diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx new file mode 100644 index 0000000..ffb7774 --- /dev/null +++ b/src/components/Counter.tsx @@ -0,0 +1,17 @@ +import { useState } from "@/core/hooks" +import "./Counter.css" + +export default function Counter() { + const [count, setCount] = useState(1) + + const handleIncrease = () => setCount(count + 1) + // const handleDecrease = () => setCount(count - 1) + + return ( +
+

{count}

+ {/* */} + +
+ ) +} From 556dceba0e874ebc37be7842e5f7ab947d881730 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:22:01 +0900 Subject: [PATCH 05/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20SharedInternals=20?= =?UTF-8?q?=ED=9B=85=20=EB=82=B4=EB=B6=80=20=EA=B0=9D=EC=B2=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/sharedInternals.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/core/sharedInternals.ts diff --git a/src/core/sharedInternals.ts b/src/core/sharedInternals.ts new file mode 100644 index 0000000..619cd2d --- /dev/null +++ b/src/core/sharedInternals.ts @@ -0,0 +1,20 @@ +import { Component, VNode } from "./jsx/jsx-runtime.type" + +interface Internals { + hooks: any[] + currentHookIndex: number + rootComponent: null | Component + rootElement: null | HTMLElement + currentVDOM: null | VNode +} + +/** + * @todo 이 객체는 코어 객체입니다. 값을 절대로 변경하지 마세요. + */ +export const internals: Internals = { + hooks: [], + currentHookIndex: 0, + rootComponent: null, + rootElement: null, + currentVDOM: null, +} From 8469c79743ca9b07a76df490164001c9278e03ba Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:22:13 +0900 Subject: [PATCH 06/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20useState=20=ED=9B=85?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/hooks.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/core/hooks.ts diff --git a/src/core/hooks.ts b/src/core/hooks.ts new file mode 100644 index 0000000..53e322a --- /dev/null +++ b/src/core/hooks.ts @@ -0,0 +1,20 @@ +import { internals } from "./sharedInternals" +import { reRender } from "./client" + +export const { useState } = (function () { + const useState = (initialState: T) => { + const currentIndex = internals.currentHookIndex + const state = internals.hooks[currentIndex] ?? initialState + + const setState = (newState: T) => { + internals.hooks[currentIndex] = newState + reRender() + } + + internals.currentHookIndex++ + + return [state, setState] + } + + return { useState } +})() From 9d6e027eea2c18e1251e2b40299547691079a89c Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:22:22 +0900 Subject: [PATCH 07/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20render=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/client.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/core/client.ts diff --git a/src/core/client.ts b/src/core/client.ts new file mode 100644 index 0000000..c61ea3f --- /dev/null +++ b/src/core/client.ts @@ -0,0 +1,21 @@ +import { updateElement } from "@/core/jsx/jsx-runtime" +import { internals } from "./sharedInternals" +import { Component } from "./jsx/jsx-runtime.type" + +export const { render, reRender } = (function () { + const render = (element: HTMLElement, component: Component) => { + internals.rootElement = element + internals.rootComponent = component + reRender() + } + + const reRender = () => { + if (!internals.rootElement || !internals.rootComponent) return + const newVDOM = internals.rootComponent() + updateElement(internals.rootElement, newVDOM, internals.currentVDOM) + internals.currentHookIndex = 0 + internals.currentVDOM = newVDOM + } + + return { render, reRender } +})() From c81719c16c0b5807729d3d5678abc72c548a922a Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:22:34 +0900 Subject: [PATCH 08/32] =?UTF-8?q?=EC=9E=90=EC=9E=98=ED=95=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=82=AC=ED=95=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc | 4 ++++ package.json | 3 ++- src/global.d.ts | 5 +++++ tsconfig.json | 10 +++++++++- vite.config.ts | 11 +++++++++++ yarn.lock | 40 +++++++++++++++++++++++++++++++++++++++- 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 .prettierrc create mode 100644 src/global.d.ts create mode 100644 vite.config.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..afa7d06 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "printWidth": 120 +} diff --git a/package.json b/package.json index ca6b4f1..5815fdd 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "devDependencies": { "typescript": "~5.6.2", - "vite": "^6.0.5" + "vite": "^6.0.5", + "vite-tsconfig-paths": "^5.1.4" } } diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..5b9643c --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,5 @@ +declare namespace JSX { + interface IntrinsicElements { + [elemName: string]: any + } +} diff --git a/tsconfig.json b/tsconfig.json index a4883f2..7b15cd7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,15 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + + "jsx": "react-jsx", + "jsxImportSource": "@/core/jsx", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } }, "include": ["src"] } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..923e0f1 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite" +import tsconfigPaths from "vite-tsconfig-paths" + +export default defineConfig({ + plugins: [tsconfigPaths()], + esbuild: { + jsx: "transform", + jsxFactory: "h", + jsxInject: `import { h, createElement } from '@/core/jsx'`, + }, +}) diff --git a/yarn.lock b/yarn.lock index 8e382b7..b0f5f41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -486,7 +486,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.4": version: 4.4.0 resolution: "debug@npm:4.4.0" dependencies: @@ -689,6 +689,13 @@ __metadata: languageName: node linkType: hard +"globrex@npm:^0.1.2": + version: 0.1.2 + resolution: "globrex@npm:0.1.2" + checksum: 10c0/a54c029520cf58bda1d8884f72bd49b4cd74e977883268d931fd83bcbd1a9eb96d57c7dbd4ad80148fb9247467ebfb9b215630b2ed7563b2a8de02e1ff7f89d1 + languageName: node + linkType: hard + "graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -1037,6 +1044,7 @@ __metadata: dependencies: typescript: "npm:~5.6.2" vite: "npm:^6.0.5" + vite-tsconfig-paths: "npm:^5.1.4" languageName: unknown linkType: soft @@ -1274,6 +1282,20 @@ __metadata: languageName: node linkType: hard +"tsconfck@npm:^3.0.3": + version: 3.1.4 + resolution: "tsconfck@npm:3.1.4" + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + bin: + tsconfck: bin/tsconfck.js + checksum: 10c0/5120e91b3388574b449d57d08f45d05d9966cf4b9d6aa1018652c1fff6d7d37b1ed099b07e6ebf6099aa40b8a16968dd337198c55b7274892849112b942861ed + languageName: node + linkType: hard + "typescript@npm:~5.6.2": version: 5.6.3 resolution: "typescript@npm:5.6.3" @@ -1312,6 +1334,22 @@ __metadata: languageName: node linkType: hard +"vite-tsconfig-paths@npm:^5.1.4": + version: 5.1.4 + resolution: "vite-tsconfig-paths@npm:5.1.4" + dependencies: + debug: "npm:^4.1.1" + globrex: "npm:^0.1.2" + tsconfck: "npm:^3.0.3" + peerDependencies: + vite: "*" + peerDependenciesMeta: + vite: + optional: true + checksum: 10c0/6228f23155ea25d92b1e1702284cf8dc52ad3c683c5ca691edd5a4c82d2913e7326d00708cef1cbfde9bb226261df0e0a12e03ef1d43b6a92d8f02b483ef37e3 + languageName: node + linkType: hard + "vite@npm:^6.0.5": version: 6.0.7 resolution: "vite@npm:6.0.7" From fa42996d57392b4047e4d988b45001e093fb6145 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:23:26 +0900 Subject: [PATCH 09/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=82=98=EB=A7=8C?= =?UTF-8?q?=EC=9D=98=20=EB=A6=AC=EC=95=A1=ED=8A=B8=20=EC=A7=84=EC=9E=85?= =?UTF-8?q?=EC=A0=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 63589ae..2b42be6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1 +1,7 @@ -// Your code.. +// import { useState } from "./core/hooks" +import { render } from "./core/client" +import Counter from "./components/Counter" + +const rootElement = document.getElementById("app")! as HTMLDivElement + +render(rootElement, Counter) From aa77089df743b44d651508b7ef1f5501b8215483 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:24:05 +0900 Subject: [PATCH 10/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20Counter=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Counter.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx index ffb7774..948aa1b 100644 --- a/src/components/Counter.tsx +++ b/src/components/Counter.tsx @@ -5,12 +5,12 @@ export default function Counter() { const [count, setCount] = useState(1) const handleIncrease = () => setCount(count + 1) - // const handleDecrease = () => setCount(count - 1) + const handleDecrease = () => setCount(count - 1) return (

{count}

- {/* */} +
) From 783f4d36f2f0f259fc1e3a736c872c30dba716c6 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:57:16 +0900 Subject: [PATCH 11/32] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20=EB=85=B8=EB=93=9C?= =?UTF-8?q?=EA=B0=80=20falsy=EC=9D=BC=20=EB=95=8C=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=EB=90=98=EA=B1=B0=EB=82=98,=20=EC=B6=94=EA=B0=80=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/jsx/jsx-runtime.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/jsx/jsx-runtime.ts b/src/core/jsx/jsx-runtime.ts index 991e4aa..a72fb69 100644 --- a/src/core/jsx/jsx-runtime.ts +++ b/src/core/jsx/jsx-runtime.ts @@ -1,4 +1,3 @@ -import { internals } from "../sharedInternals" import type { Type, VDOM, Props, VNode } from "./jsx-runtime.type" import { isNullOrUndefined, isPrimitive } from "./jsx-runtime.utils" @@ -12,6 +11,7 @@ function createElement(node: VNode) { } if (isPrimitive(node)) { + console.log(node) return document.createTextNode(String(node)) } @@ -61,12 +61,12 @@ function updateAttributes(element: HTMLElement, newProps: Props = {}, oldProps: } export function updateElement(parent: HTMLElement, newNode?: VNode, oldNode?: VNode, index: number = 0) { - if (!newNode && oldNode) { + if (isNullOrUndefined(newNode) && oldNode) { parent.removeChild(parent.childNodes[index]) return } - if (newNode && !oldNode) { + if (newNode && isNullOrUndefined(oldNode)) { parent.appendChild(createElement(newNode)) return } From a980f37341f1d9ee01a5804c683c02b363a9b956 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:58:43 +0900 Subject: [PATCH 12/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EA=B4=80=EC=8B=AC=EC=82=AC=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/jsx/jsx-runtime.ts | 14 ++------------ src/core/jsx/jsx-runtime.utils.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/jsx/jsx-runtime.ts b/src/core/jsx/jsx-runtime.ts index a72fb69..eb942c1 100644 --- a/src/core/jsx/jsx-runtime.ts +++ b/src/core/jsx/jsx-runtime.ts @@ -1,5 +1,5 @@ import type { Type, VDOM, Props, VNode } from "./jsx-runtime.type" -import { isNullOrUndefined, isPrimitive } from "./jsx-runtime.utils" +import { isNullOrUndefined, isPrimitive, isVDOM, isDiffText } from "./jsx-runtime.utils" function h(type: Type, props: Props, ...children: VNode[]): VDOM { return { type, props: props || {}, children: children.flat() } @@ -71,7 +71,7 @@ export function updateElement(parent: HTMLElement, newNode?: VNode, oldNode?: VN return } - if (diffText(newNode, oldNode)) { + if (isDiffText(newNode, oldNode)) { parent.replaceChild(createElement(newNode), parent.childNodes[index]) return } @@ -94,14 +94,4 @@ export function updateElement(parent: HTMLElement, newNode?: VNode, oldNode?: VN } } -function isVDOM(node: VNode) { - return typeof node === "object" && node !== null -} - -function diffText(newNode: VNode, oldNode: VNode) { - if (JSON.stringify(newNode) === JSON.stringify(oldNode)) return false - if (typeof newNode === "object" || typeof oldNode === "object") return false - return true -} - export { h, createElement } diff --git a/src/core/jsx/jsx-runtime.utils.ts b/src/core/jsx/jsx-runtime.utils.ts index 630293b..19374d2 100644 --- a/src/core/jsx/jsx-runtime.utils.ts +++ b/src/core/jsx/jsx-runtime.utils.ts @@ -7,3 +7,13 @@ export function isNullOrUndefined(node: VNode) { export function isPrimitive(node: VNode) { return typeof node === "string" || typeof node === "number" } + +export function isVDOM(node: VNode) { + return typeof node === "object" && node !== null +} + +export function isDiffText(newNode: VNode, oldNode: VNode) { + if (JSON.stringify(newNode) === JSON.stringify(oldNode)) return false + if (typeof newNode === "object" || typeof oldNode === "object") return false + return true +} From e8a412721164766d4fe037e1ffff6bc317c5cd14 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 16 Jan 2025 12:59:58 +0900 Subject: [PATCH 13/32] =?UTF-8?q?=F0=9F=9A=9A=20=20Rename:=20main=20=3D>?= =?UTF-8?q?=20App?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- src/{main.ts => App.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{main.ts => App.ts} (100%) diff --git a/index.html b/index.html index e73eb69..6e78c5e 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,6 @@
- + diff --git a/src/main.ts b/src/App.ts similarity index 100% rename from src/main.ts rename to src/App.ts From 309e2116ab1c12f32fa83aacb9cdc5b425149ece Mon Sep 17 00:00:00 2001 From: d5ng Date: Mon, 20 Jan 2025 17:37:51 +0900 Subject: [PATCH 14/32] =?UTF-8?q?=F0=9F=94=A5=20Remove:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A7=80?= =?UTF-8?q?=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/jsx/jsx-runtime.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/jsx/jsx-runtime.ts b/src/core/jsx/jsx-runtime.ts index eb942c1..304a845 100644 --- a/src/core/jsx/jsx-runtime.ts +++ b/src/core/jsx/jsx-runtime.ts @@ -2,6 +2,7 @@ import type { Type, VDOM, Props, VNode } from "./jsx-runtime.type" import { isNullOrUndefined, isPrimitive, isVDOM, isDiffText } from "./jsx-runtime.utils" function h(type: Type, props: Props, ...children: VNode[]): VDOM { + if (typeof type === "function") return type(props) return { type, props: props || {}, children: children.flat() } } @@ -11,11 +12,10 @@ function createElement(node: VNode) { } if (isPrimitive(node)) { - console.log(node) return document.createTextNode(String(node)) } - const element = document.createElement(node.type) + const element = document.createElement(node.type as keyof HTMLElementTagNameMap) setAttribute(element, node.props) node.children.map(createElement).forEach((child) => element.appendChild(child)) @@ -28,6 +28,10 @@ function setAttribute(element: HTMLElement, props: Props) { .filter(([_, value]) => value) .forEach(([attr, value]) => { if (attr.startsWith("on") && typeof props[attr] === "function") { + if (attr === "onChange") { + element.addEventListener("input", props[attr]) + } + const eventType = attr.slice(2).toLowerCase() element.addEventListener(eventType, props[attr]) } From 8f3a2f689254ebff0e80c9168bb25de1691d7202 Mon Sep 17 00:00:00 2001 From: d5ng Date: Mon, 20 Jan 2025 17:38:19 +0900 Subject: [PATCH 15/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20Type=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/jsx/jsx-runtime.type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/jsx/jsx-runtime.type.ts b/src/core/jsx/jsx-runtime.type.ts index 1dbc188..84ce74d 100644 --- a/src/core/jsx/jsx-runtime.type.ts +++ b/src/core/jsx/jsx-runtime.type.ts @@ -1,4 +1,4 @@ -export type Type = keyof HTMLElementTagNameMap +export type Type = keyof HTMLElementTagNameMap | Component export type Props = Record From 7f6f581af1f1791d1685265c636cc976624d9117 Mon Sep 17 00:00:00 2001 From: d5ng Date: Mon, 20 Jan 2025 17:38:38 +0900 Subject: [PATCH 16/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20main=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.ts | 4 ++-- src/pages/main.tsx | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 src/pages/main.tsx diff --git a/src/App.ts b/src/App.ts index 2b42be6..7e9cb3e 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1,7 +1,7 @@ // import { useState } from "./core/hooks" import { render } from "./core/client" -import Counter from "./components/Counter" +import Main from "./pages/main" const rootElement = document.getElementById("app")! as HTMLDivElement -render(rootElement, Counter) +render(rootElement, Main) diff --git a/src/pages/main.tsx b/src/pages/main.tsx new file mode 100644 index 0000000..71439af --- /dev/null +++ b/src/pages/main.tsx @@ -0,0 +1,9 @@ +import Counter from "@/components/Counter" + +export default function Main() { + return ( +
+ +
+ ) +} From ad84a82094e347d48986cd7edab4e7a9e7be5a0e Mon Sep 17 00:00:00 2001 From: d5ng Date: Mon, 20 Jan 2025 17:40:17 +0900 Subject: [PATCH 17/32] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Internals=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=EA=B0=9D=EC=B2=B4=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/sharedInternals.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/sharedInternals.ts b/src/core/sharedInternals.ts index 619cd2d..5cfc5f1 100644 --- a/src/core/sharedInternals.ts +++ b/src/core/sharedInternals.ts @@ -9,7 +9,7 @@ interface Internals { } /** - * @todo 이 객체는 코어 객체입니다. 값을 절대로 변경하지 마세요. + * NOTE - 이 객체는 코어 객체입니다. 값을 절대로 변경하지 마세요. 만약 값을 변경하게되면 제대로 동작하지 않을 수 있습니다. */ export const internals: Internals = { hooks: [], From e12fc547cd1f3ba71a74972ffe76f8051941b6ba Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 00:05:59 +0900 Subject: [PATCH 18/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=8F=20effect=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/client.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/core/client.ts b/src/core/client.ts index c61ea3f..a51bf11 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -2,6 +2,18 @@ import { updateElement } from "@/core/jsx/jsx-runtime" import { internals } from "./sharedInternals" import { Component } from "./jsx/jsx-runtime.type" +function frameRunner(callback: () => void) { + let rafId: ReturnType + + return () => { + if (rafId) { + cancelAnimationFrame(rafId) + } + + rafId = requestAnimationFrame(callback) + } +} + export const { render, reRender } = (function () { const render = (element: HTMLElement, component: Component) => { internals.rootElement = element @@ -9,13 +21,17 @@ export const { render, reRender } = (function () { reRender() } - const reRender = () => { + const reRender = frameRunner(() => { if (!internals.rootElement || !internals.rootComponent) return const newVDOM = internals.rootComponent() + updateElement(internals.rootElement, newVDOM, internals.currentVDOM) internals.currentHookIndex = 0 internals.currentVDOM = newVDOM - } + + internals.effectList.filter((effectHook) => effectHook).forEach((fn) => fn()) + internals.effectList = [] + }) return { render, reRender } })() From b06dc3021a95173d85fcda5c596322151b53da2f Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 00:06:13 +0900 Subject: [PATCH 19/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20useEffect=20=ED=9B=85?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/hooks.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/core/hooks.ts b/src/core/hooks.ts index 53e322a..3b6644a 100644 --- a/src/core/hooks.ts +++ b/src/core/hooks.ts @@ -1,12 +1,19 @@ import { internals } from "./sharedInternals" import { reRender } from "./client" -export const { useState } = (function () { +type UseEffectCallback = (...args: any[]) => void +type UseEffectDep = any[] + +export const { useState, useEffect } = (function () { const useState = (initialState: T) => { const currentIndex = internals.currentHookIndex const state = internals.hooks[currentIndex] ?? initialState const setState = (newState: T) => { + if (Object.is(state, newState)) { + return + } + internals.hooks[currentIndex] = newState reRender() } @@ -16,5 +23,24 @@ export const { useState } = (function () { return [state, setState] } - return { useState } + const useEffect = (callback: UseEffectCallback, dependencies?: UseEffectDep) => { + const currentIndex = internals.currentHookIndex + const oldDependencies = internals.hooks[currentIndex] + let hasChanged = true + + if (oldDependencies) { + hasChanged = dependencies + ? dependencies.some((dependency, index) => !Object.is(dependency, oldDependencies[index])) + : true + } + + if (hasChanged) { + internals.hooks[currentIndex] = dependencies || null + internals.effectList[currentIndex] = callback + } + + internals.currentHookIndex++ + } + + return { useState, useEffect } })() From 37a58e8a9a99d315b26ab75b36fe470328b0a9a9 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 00:06:33 +0900 Subject: [PATCH 20/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20useEffect=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/sharedInternals.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/sharedInternals.ts b/src/core/sharedInternals.ts index 5cfc5f1..cb11850 100644 --- a/src/core/sharedInternals.ts +++ b/src/core/sharedInternals.ts @@ -6,6 +6,7 @@ interface Internals { rootComponent: null | Component rootElement: null | HTMLElement currentVDOM: null | VNode + effectList: (() => void)[] } /** @@ -17,4 +18,5 @@ export const internals: Internals = { rootComponent: null, rootElement: null, currentVDOM: null, + effectList: [], } From 41db65a84226ad53e783fd8012ba3cb56cf7a36f Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 00:06:52 +0900 Subject: [PATCH 21/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20Counter=20useEffect?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Counter.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx index 948aa1b..55e4117 100644 --- a/src/components/Counter.tsx +++ b/src/components/Counter.tsx @@ -1,4 +1,4 @@ -import { useState } from "@/core/hooks" +import { useState, useEffect } from "@/core/hooks" import "./Counter.css" export default function Counter() { @@ -7,6 +7,11 @@ export default function Counter() { const handleIncrease = () => setCount(count + 1) const handleDecrease = () => setCount(count - 1) + useEffect(() => { + const setupCount = () => setCount(100) + setupCount() + }, []) + return (

{count}

From 0e9e62a47642a3b0a115bf2e5326563df244a1d7 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 00:07:11 +0900 Subject: [PATCH 22/32] =?UTF-8?q?=F0=9F=94=A5=20Remove:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A7=80?= =?UTF-8?q?=EC=9A=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.ts b/src/App.ts index 7e9cb3e..02157b5 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1,4 +1,3 @@ -// import { useState } from "./core/hooks" import { render } from "./core/client" import Main from "./pages/main" From cda0f35a34732078a00d1d04369c87aa1de853b6 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 00:21:11 +0900 Subject: [PATCH 23/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20react-ho?= =?UTF-8?q?oks=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Counter.tsx | 2 +- src/core/hooks.ts | 46 ------------------------------------- src/core/hooks/index.ts | 2 ++ src/core/hooks/useEffect.ts | 23 +++++++++++++++++++ src/core/hooks/useState.ts | 20 ++++++++++++++++ 5 files changed, 46 insertions(+), 47 deletions(-) delete mode 100644 src/core/hooks.ts create mode 100644 src/core/hooks/index.ts create mode 100644 src/core/hooks/useEffect.ts create mode 100644 src/core/hooks/useState.ts diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx index 55e4117..7f4ac64 100644 --- a/src/components/Counter.tsx +++ b/src/components/Counter.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "@/core/hooks" +import { useEffect, useState } from "@/core/hooks" import "./Counter.css" export default function Counter() { diff --git a/src/core/hooks.ts b/src/core/hooks.ts deleted file mode 100644 index 3b6644a..0000000 --- a/src/core/hooks.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { internals } from "./sharedInternals" -import { reRender } from "./client" - -type UseEffectCallback = (...args: any[]) => void -type UseEffectDep = any[] - -export const { useState, useEffect } = (function () { - const useState = (initialState: T) => { - const currentIndex = internals.currentHookIndex - const state = internals.hooks[currentIndex] ?? initialState - - const setState = (newState: T) => { - if (Object.is(state, newState)) { - return - } - - internals.hooks[currentIndex] = newState - reRender() - } - - internals.currentHookIndex++ - - return [state, setState] - } - - const useEffect = (callback: UseEffectCallback, dependencies?: UseEffectDep) => { - const currentIndex = internals.currentHookIndex - const oldDependencies = internals.hooks[currentIndex] - let hasChanged = true - - if (oldDependencies) { - hasChanged = dependencies - ? dependencies.some((dependency, index) => !Object.is(dependency, oldDependencies[index])) - : true - } - - if (hasChanged) { - internals.hooks[currentIndex] = dependencies || null - internals.effectList[currentIndex] = callback - } - - internals.currentHookIndex++ - } - - return { useState, useEffect } -})() diff --git a/src/core/hooks/index.ts b/src/core/hooks/index.ts new file mode 100644 index 0000000..2d03a26 --- /dev/null +++ b/src/core/hooks/index.ts @@ -0,0 +1,2 @@ +export { useState } from "./useState" +export { useEffect } from "./useEffect" diff --git a/src/core/hooks/useEffect.ts b/src/core/hooks/useEffect.ts new file mode 100644 index 0000000..860b51d --- /dev/null +++ b/src/core/hooks/useEffect.ts @@ -0,0 +1,23 @@ +import { internals } from "../sharedInternals" + +type UseEffectCallback = (...args: any[]) => void +type UseEffectDep = any[] + +export function useEffect(callback: UseEffectCallback, dependencies?: UseEffectDep) { + const currentIndex = internals.currentHookIndex + const oldDependencies = internals.hooks[currentIndex] + let hasChanged = true + + if (oldDependencies) { + hasChanged = dependencies + ? dependencies.some((dependency, index) => !Object.is(dependency, oldDependencies[index])) + : true + } + + if (hasChanged) { + internals.hooks[currentIndex] = dependencies || null + internals.effectList[currentIndex] = callback + } + + internals.currentHookIndex++ +} diff --git a/src/core/hooks/useState.ts b/src/core/hooks/useState.ts new file mode 100644 index 0000000..2c55375 --- /dev/null +++ b/src/core/hooks/useState.ts @@ -0,0 +1,20 @@ +import { reRender } from "../client" +import { internals } from "../sharedInternals" + +export function useState(initialState: T) { + const currentIndex = internals.currentHookIndex + const state = internals.hooks[currentIndex] ?? initialState + + const setState = (newState: T) => { + if (Object.is(state, newState)) { + return + } + + internals.hooks[currentIndex] = newState + reRender() + } + + internals.currentHookIndex++ + + return [state, setState] +} From 07907e67ca7aa4f430b65c8cc1ee91cdb609bcc5 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 14:48:15 +0900 Subject: [PATCH 24/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20hook=20Type=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/hooks/hooks.type.ts | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/core/hooks/hooks.type.ts diff --git a/src/core/hooks/hooks.type.ts b/src/core/hooks/hooks.type.ts new file mode 100644 index 0000000..8a68a68 --- /dev/null +++ b/src/core/hooks/hooks.type.ts @@ -0,0 +1,2 @@ +export type Dependencies = any[] +export type Callback = (...args: any[]) => void From 1e22b7781449f58b927da6c5d4c39b0a8543f4f6 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 14:48:27 +0900 Subject: [PATCH 25/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20useCallback=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/hooks/useCallback.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/core/hooks/useCallback.ts diff --git a/src/core/hooks/useCallback.ts b/src/core/hooks/useCallback.ts new file mode 100644 index 0000000..accf2c8 --- /dev/null +++ b/src/core/hooks/useCallback.ts @@ -0,0 +1,25 @@ +import { internals } from "../sharedInternals" +import { Callback, Dependencies } from "./hooks.type" + +export function useCallback(callback: Callback, dependencies?: Dependencies) { + const currentIndex = internals.currentHookIndex + const [oldValue, oldDependencies] = internals.hooks[currentIndex] || [] + + let cachedFunction: Callback = oldValue + let hasChanged = true + + if (oldDependencies) { + hasChanged = dependencies + ? dependencies.some((dependency, index) => !Object.is(dependency, oldDependencies[index])) + : true + } + + if (hasChanged) { + cachedFunction = callback + internals.hooks[currentIndex] = [callback, dependencies] + } + + internals.currentHookIndex++ + + return cachedFunction +} From 8136eb41a52f1fd285f694cb2d71bd4d905abe31 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 14:48:35 +0900 Subject: [PATCH 26/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20useMemo=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/hooks/useMemo.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/core/hooks/useMemo.ts diff --git a/src/core/hooks/useMemo.ts b/src/core/hooks/useMemo.ts new file mode 100644 index 0000000..698e967 --- /dev/null +++ b/src/core/hooks/useMemo.ts @@ -0,0 +1,25 @@ +import { internals } from "../sharedInternals" +import { Callback, Dependencies } from "./hooks.type" + +export function useMemo(callback: Callback, dependencies?: Dependencies) { + const currentIndex = internals.currentHookIndex + const [oldValue, oldDependencies] = internals.hooks[currentIndex] || [] + + let hasChanged = true + let memoValue = oldValue || null + + if (oldDependencies) { + hasChanged = dependencies + ? dependencies.some((dependency, index) => !Object.is(dependency, oldDependencies[index])) + : true + } + + if (hasChanged) { + memoValue = callback() + internals.hooks[currentIndex] = [memoValue, dependencies] + } + + internals.currentHookIndex++ + + return memoValue +} From 36f7f0c8eb5a3bb84ac2cd461dcafd0b91ff2ec9 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 14:48:44 +0900 Subject: [PATCH 27/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20useEffect=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/hooks/useEffect.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/core/hooks/useEffect.ts b/src/core/hooks/useEffect.ts index 860b51d..c8f9284 100644 --- a/src/core/hooks/useEffect.ts +++ b/src/core/hooks/useEffect.ts @@ -1,9 +1,7 @@ import { internals } from "../sharedInternals" +import { Callback, Dependencies } from "./hooks.type" -type UseEffectCallback = (...args: any[]) => void -type UseEffectDep = any[] - -export function useEffect(callback: UseEffectCallback, dependencies?: UseEffectDep) { +export function useEffect(callback: Callback, dependencies?: Dependencies) { const currentIndex = internals.currentHookIndex const oldDependencies = internals.hooks[currentIndex] let hasChanged = true From 7e33d16dedc1f57001ae7607c9a1a13be568935e Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 14:49:11 +0900 Subject: [PATCH 28/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20Counter=20useEffect?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9=ED=95=B4=EB=B3=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Counter.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx index 7f4ac64..86a456c 100644 --- a/src/components/Counter.tsx +++ b/src/components/Counter.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "@/core/hooks" import "./Counter.css" +import { useCallback } from "@/core/hooks/useCallback" export default function Counter() { const [count, setCount] = useState(1) @@ -20,3 +21,5 @@ export default function Counter() {
) } + +// const handleIncrease = useCallback(() => setCount(count + 1), []) From 5fbd4dc10382f14ae0e7a48b0766efed96987624 Mon Sep 17 00:00:00 2001 From: d5ng Date: Thu, 23 Jan 2025 14:50:12 +0900 Subject: [PATCH 29/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20Counter=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=20hooks=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=EB=B3=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Counter.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx index 86a456c..e22af61 100644 --- a/src/components/Counter.tsx +++ b/src/components/Counter.tsx @@ -5,8 +5,8 @@ import { useCallback } from "@/core/hooks/useCallback" export default function Counter() { const [count, setCount] = useState(1) - const handleIncrease = () => setCount(count + 1) - const handleDecrease = () => setCount(count - 1) + const handleIncrease = useCallback(() => setCount(count + 1), [count]) + const handleDecrease = useCallback(() => setCount(count - 1), [count]) useEffect(() => { const setupCount = () => setCount(100) From 2a357e47d44eeb28bc215e78c5b4abbef3240c2e Mon Sep 17 00:00:00 2001 From: d5ng Date: Fri, 24 Jan 2025 19:46:46 +0900 Subject: [PATCH 30/32] =?UTF-8?q?=F0=9F=94=A5=20Remove:=20=EC=A3=BD?= =?UTF-8?q?=EC=9D=80=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Counter.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx index e22af61..82d0b3a 100644 --- a/src/components/Counter.tsx +++ b/src/components/Counter.tsx @@ -21,5 +21,3 @@ export default function Counter() { ) } - -// const handleIncrease = useCallback(() => setCount(count + 1), []) From 6ba90d3df7942bc24aae295f17f4edddfd22f845 Mon Sep 17 00:00:00 2001 From: d5ng Date: Fri, 24 Jan 2025 19:47:09 +0900 Subject: [PATCH 31/32] =?UTF-8?q?=F0=9F=94=A7=20Chore:=20jsxInject=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 923e0f1..5e29256 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,6 @@ export default defineConfig({ esbuild: { jsx: "transform", jsxFactory: "h", - jsxInject: `import { h, createElement } from '@/core/jsx'`, + jsxInject: `import { h } from '@/core/jsx'`, }, }) From 08c15725bd6d2e43072af9d36b93f37994a5d47b Mon Sep 17 00:00:00 2001 From: d5ng Date: Sat, 25 Jan 2025 17:10:58 +0900 Subject: [PATCH 32/32] =?UTF-8?q?=E2=9C=A8=20Feat:=20microtask=20Queue=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=98=EC=97=AC=20batch=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Counter.tsx | 4 ++-- src/core/client.ts | 7 +++++-- src/core/hooks/useState.ts | 22 ++++++++++++++++++++-- src/core/sharedInternals.ts | 4 ++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx index 82d0b3a..3a763c7 100644 --- a/src/components/Counter.tsx +++ b/src/components/Counter.tsx @@ -5,8 +5,8 @@ import { useCallback } from "@/core/hooks/useCallback" export default function Counter() { const [count, setCount] = useState(1) - const handleIncrease = useCallback(() => setCount(count + 1), [count]) - const handleDecrease = useCallback(() => setCount(count - 1), [count]) + const handleIncrease = () => setCount(count + 1) + const handleDecrease = () => setCount(count - 1) useEffect(() => { const setupCount = () => setCount(100) diff --git a/src/core/client.ts b/src/core/client.ts index a51bf11..8ec3892 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -2,6 +2,9 @@ import { updateElement } from "@/core/jsx/jsx-runtime" import { internals } from "./sharedInternals" import { Component } from "./jsx/jsx-runtime.type" +/** + * Note: 현재 microtaskQueue 활용으로 `frameRunner` 함수를 사용하지 않음 + */ function frameRunner(callback: () => void) { let rafId: ReturnType @@ -21,7 +24,7 @@ export const { render, reRender } = (function () { reRender() } - const reRender = frameRunner(() => { + const reRender = () => { if (!internals.rootElement || !internals.rootComponent) return const newVDOM = internals.rootComponent() @@ -31,7 +34,7 @@ export const { render, reRender } = (function () { internals.effectList.filter((effectHook) => effectHook).forEach((fn) => fn()) internals.effectList = [] - }) + } return { render, reRender } })() diff --git a/src/core/hooks/useState.ts b/src/core/hooks/useState.ts index 2c55375..490fd9c 100644 --- a/src/core/hooks/useState.ts +++ b/src/core/hooks/useState.ts @@ -10,11 +10,29 @@ export function useState(initialState: T) { return } - internals.hooks[currentIndex] = newState - reRender() + const addBatchQueue = () => { + internals.hooks[currentIndex] = newState + } + + internals.batchQueue.push(addBatchQueue) + scheduleBatch() } internals.currentHookIndex++ return [state, setState] } + +function flushBatchQueue() { + internals.batchQueue.forEach((fn) => fn()) + internals.batchQueue = [] + reRender() + internals.isBatching = false +} + +function scheduleBatch() { + if (!internals.isBatching) { + internals.isBatching = true + queueMicrotask(flushBatchQueue) + } +} diff --git a/src/core/sharedInternals.ts b/src/core/sharedInternals.ts index cb11850..c1d2a9a 100644 --- a/src/core/sharedInternals.ts +++ b/src/core/sharedInternals.ts @@ -7,6 +7,8 @@ interface Internals { rootElement: null | HTMLElement currentVDOM: null | VNode effectList: (() => void)[] + isBatching: boolean + batchQueue: (() => void)[] } /** @@ -19,4 +21,6 @@ export const internals: Internals = { rootElement: null, currentVDOM: null, effectList: [], + isBatching: false, + batchQueue: [], }