-
Notifications
You must be signed in to change notification settings - Fork 0
[DongHyun] React Hooks #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
base: main
Are you sure you want to change the base?
Changes from all commits
daa5e25
235bd57
0e55cb0
306d4bd
556dceb
8469c79
9d6e027
c81719c
fa42996
aa77089
783f4d3
a980f37
e8a4127
309e211
8f3a2f6
7f6f581
ad84a82
e12fc54
b06dc30
37a58e8
41db65a
0e9e62a
cda0f35
07907e6
1e22b77
8136eb4
36f7f0c
7e33d16
5fbd4dc
2a357e4
6ba90d3
08c1572
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "semi": false, | ||
| "printWidth": 120 | ||
| } |
| 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` 훅을 직접 구현하며, 상태 관리의 기본 개념을 학습할 수 있습니다. 또한 상태와 렌더링 로직을 분리하여 코드의 유지보수성과 재사용성을 높이는 방법을 익힐 수 있습니다. |
| 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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .count-wrapper { | ||
| color: red; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { useEffect, useState } from "@/core/hooks" | ||
| 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> | ||
| ) | ||
| } | ||
| 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 } | ||
| })() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export type Dependencies = any[] | ||
| export type Callback = (...args: any[]) => void |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { useState } from "./useState" | ||
| export { useEffect } from "./useEffect" |
| 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 | ||
| } |
| 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 | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 클린업 함수 추가해봅시다~~ 저도 해야돼요
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 참고로 conflict 발생하는 거 제가 작업물을 main에 push해서 되돌리느라 readme.md 수정해서 그런겁니다ㅜㅜ 참고 부탁드려요! |
||
| internals.currentHookIndex++ | ||
| } | ||
| 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 | ||
| } |
| 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() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아티클과 다른 방식으로 배치 알고리즘을 적용했군요...!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
| } | ||
| 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 } |
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.
구현한 hook을 다른 파일로 분리한 게 좋아보여요! 저도 리팩토링 때 분리해볼게요
Uh oh!
There was an error while loading. Please reload this page.
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.
분리를 했지만, internals 객체를 클로저 안에서 관리하는게 아니다보니, 어느곳에서 접근 가능하다는게 조금 별로인거 같네요..