Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
daa5e25
📝 Docs: add readme.md
D5ng Jan 15, 2025
235bd57
📝 Docs: readme.md update
D5ng Jan 15, 2025
0e55cb0
✨ Feat: JSX 추가
D5ng Jan 16, 2025
306d4bd
✨ Feat: Counter Component 추가
D5ng Jan 16, 2025
556dceb
✨ Feat: SharedInternals 훅 내부 객체 추가
D5ng Jan 16, 2025
8469c79
✨ Feat: useState 훅 추가
D5ng Jan 16, 2025
9d6e027
✨ Feat: render 함수 추가
D5ng Jan 16, 2025
c81719c
자잘한 변경사항
D5ng Jan 16, 2025
fa42996
✨ Feat: 나만의 리액트 진입점 추가
D5ng Jan 16, 2025
aa77089
✨ Feat: Counter 컴포넌트 로직 추가
D5ng Jan 16, 2025
783f4d3
🐛 Fix: 노드가 falsy일 때 삭제되거나, 추가되는 버그 해결
D5ng Jan 16, 2025
a980f37
♻️ Refactor: 관심사 분리 및 네이밍 변경
D5ng Jan 16, 2025
e8a4127
🚚 Rename: main => App
D5ng Jan 16, 2025
309e211
🔥 Remove: 불필요한 코드 지우기
D5ng Jan 20, 2025
8f3a2f6
♻️ Refactor: Type 타입 수정
D5ng Jan 20, 2025
7f6f581
✨ Feat: main 페이지 추가
D5ng Jan 20, 2025
ad84a82
📝 Docs: Internals 내부 객체 주석 추가
D5ng Jan 20, 2025
e12fc54
✨ Feat: 렌더링 최적화 및 effect 관련 코드 추가
D5ng Jan 22, 2025
b06dc30
✨ Feat: useEffect 훅 구현
D5ng Jan 22, 2025
37a58e8
✨ Feat: useEffect 관련 필드 추가
D5ng Jan 22, 2025
41db65a
✨ Feat: Counter useEffect 사용
D5ng Jan 22, 2025
0e9e62a
🔥 Remove: 불필요한 코드 지우기
D5ng Jan 22, 2025
cda0f35
♻️ Refactor: react-hooks 파일 분리
D5ng Jan 22, 2025
07907e6
✨ Feat: hook Type 파일 분리
D5ng Jan 23, 2025
1e22b77
✨ Feat: useCallback 구현
D5ng Jan 23, 2025
8136eb4
✨ Feat: useMemo 구현
D5ng Jan 23, 2025
36f7f0c
✨ Feat: useEffect 구현
D5ng Jan 23, 2025
7e33d16
✨ Feat: Counter useEffect 적용해보기
D5ng Jan 23, 2025
5fbd4dc
✨ Feat: Counter 컴포넌트에 hooks 적용해보기
D5ng Jan 23, 2025
2a357e4
🔥 Remove: 죽은 코드 제거
D5ng Jan 24, 2025
6ba90d3
🔧 Chore: jsxInject 수정
D5ng Jan 24, 2025
08c1572
✨ Feat: microtask Queue 활용하여 batch 작업 변경
D5ng Jan 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"semi": false,
"printWidth": 120
}
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/App.ts"></script>
</body>
</html>
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"devDependencies": {
"typescript": "~5.6.2",
"vite": "^6.0.5"
"vite": "^6.0.5",
"vite-tsconfig-paths": "^5.1.4"
}
}
102 changes: 102 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# React Hooks 만들어보면서 이해하기

황준일님의 useState Hooks 만들기를 참고하여, 훅 작성 규칙 및 클로저를 어떤 식으로 활용할 수 있을지 이해와 고민하자. 또한, useState뿐 아니라, useCallback, useMemo, useEffect를 어떤 식으로 구현할 수 있을지도 생각해보자.

> [!NOTE]
> useState, useEffect, useCallback, useMemo를 구현했다면, 추가적으로 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` 훅을 직접 구현하며, 상태 관리의 기본 개념을 학습할 수 있습니다. 또한 상태와 렌더링 로직을 분리하여 코드의 유지보수성과 재사용성을 높이는 방법을 익힐 수 있습니다.
6 changes: 6 additions & 0 deletions src/App.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { render } from "./core/client"
import Main from "./pages/main"

const rootElement = document.getElementById("app")! as HTMLDivElement

render(rootElement, Main)
3 changes: 3 additions & 0 deletions src/components/Counter.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.count-wrapper {
color: red;
}
23 changes: 23 additions & 0 deletions src/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect, useState } from "@/core/hooks"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구현한 hook을 다른 파일로 분리한 게 좋아보여요! 저도 리팩토링 때 분리해볼게요

@D5ng D5ng Feb 3, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

분리를 했지만, internals 객체를 클로저 안에서 관리하는게 아니다보니, 어느곳에서 접근 가능하다는게 조금 별로인거 같네요..

import "./Counter.css"
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)

useEffect(() => {
const setupCount = () => setCount(100)
setupCount()
}, [])

return (
<div className="count-wrapper">
<p className="count">{count}</p>
<button onClick={handleDecrease}>-1</button>
<button onClick={handleIncrease}>+1</button>
</div>
)
}
40 changes: 40 additions & 0 deletions src/core/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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<typeof requestAnimationFrame>

return () => {
if (rafId) {
cancelAnimationFrame(rafId)
}

rafId = requestAnimationFrame(callback)
}
}

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

internals.effectList.filter((effectHook) => effectHook).forEach((fn) => fn())
internals.effectList = []
}

return { render, reRender }
})()
2 changes: 2 additions & 0 deletions src/core/hooks/hooks.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type Dependencies = any[]
export type Callback = (...args: any[]) => void
2 changes: 2 additions & 0 deletions src/core/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useState } from "./useState"
export { useEffect } from "./useEffect"
25 changes: 25 additions & 0 deletions src/core/hooks/useCallback.ts
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 21 additions & 0 deletions src/core/hooks/useEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { internals } from "../sharedInternals"
import { Callback, Dependencies } from "./hooks.type"

export function useEffect(callback: Callback, dependencies?: Dependencies) {
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클린업 함수 추가해봅시다~~ 저도 해야돼요

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

참고로 conflict 발생하는 거 제가 작업물을 main에 push해서 되돌리느라 readme.md 수정해서 그런겁니다ㅜㅜ 참고 부탁드려요!

internals.currentHookIndex++
}
25 changes: 25 additions & 0 deletions src/core/hooks/useMemo.ts
Original file line number Diff line number Diff line change
@@ -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
}
38 changes: 38 additions & 0 deletions src/core/hooks/useState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { reRender } from "../client"
import { internals } from "../sharedInternals"

export function useState<T>(initialState: T) {
const currentIndex = internals.currentHookIndex
const state = internals.hooks[currentIndex] ?? initialState

const setState = (newState: T) => {
if (Object.is(state, newState)) {
return
}

const addBatchQueue = () => {
internals.hooks[currentIndex] = newState
}

internals.batchQueue.push(addBatchQueue)
scheduleBatch()
}

internals.currentHookIndex++

return [state, setState]
}

function flushBatchQueue() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아티클과 다른 방식으로 배치 알고리즘을 적용했군요...!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React에서는 마이크로 태스크 큐를 사용하는거로 알고있어서 저도 똑같이 해봤습니다!

internals.batchQueue.forEach((fn) => fn())
internals.batchQueue = []
reRender()
internals.isBatching = false
}

function scheduleBatch() {
if (!internals.isBatching) {
internals.isBatching = true
queueMicrotask(flushBatchQueue)
}
}
3 changes: 3 additions & 0 deletions src/core/jsx/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createElement, h } from "@/core/jsx/jsx-runtime"

export { createElement, h }
Loading