Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ dist-ssr
*.sln
*.sw?

.yarn
.yarn
yarn.lock
13 changes: 13 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"bracketSameLine": true,
"jsxBracketSameLine": true,
"jsxSingleQuote": false,
"printWidth": 120,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"endOfLine": "auto"
}
8 changes: 8 additions & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
declare global {
interface Window {
increment: () => void;
decrement: () => void;
}
}

export {};
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,4 @@ React의 상태 관리 메커니즘인 `useState` 훅을 바닐라 자바스크

## 결론

React의 `useState` 훅을 직접 구현하며, 상태 관리의 기본 개념을 학습할 수 있습니다. 또한 상태와 렌더링 로직을 분리하여 코드의 유지보수성과 재사용성을 높이는 방법을 익힐 수 있습니다.
React의 `useState` 훅을 직접 구현하며, 상태 관리의 기본 개념을 학습할 수 있습니다. 또한 상태와 렌더링 로직을 분리하여 코드의 유지보수성과 재사용성을 높이는 방법을 익힐 수 있습니다.
9 changes: 9 additions & 0 deletions src/App.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import CounterAndMeow from './components/CounterAndMeow';

const App = () => `
<div>
${CounterAndMeow()}
</div>
`;

export default App;
26 changes: 26 additions & 0 deletions src/components/CounterAndMeow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import MyReact from '../core/MyReact';

const { useState, render } = MyReact();

export default function CounterAndMeow() {
const [count, setCount] = useState(1);
const [cat, setCat] = useState('야옹!');

function countMeow(newCount: number) {
setCount(newCount);
setCat('야옹! '.repeat(newCount));
}

window.increment = () => countMeow(count + 1);
window.decrement = () => {
if (count > 0) countMeow(count - 1);
};

return `
<div>
<p>고양이가 ${count}번 울어서 ${cat} </p>
<button onclick="increment()">증가</button>
<button onclick="decrement()">감소</button>
</div>
`;
}
140 changes: 140 additions & 0 deletions src/core/MyReact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import debounceFrame from '../utils/debounceFrame';

interface Options {
renderCount: number;
componentStateMap: Map<any, { states: any[]; currentStateKey: number }>;
root: Element | null;
rootComponent: any;
}

export default function MyReact() {
const options: Options = {
renderCount: 0,
componentStateMap: new Map(),
root: null,
rootComponent: null,
};

function getComponentState(component: any) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

getComponentState 라는 네이밍도 좋고, Map을 사용한 것도 좋아보이네요.

하나 궁금한건 component를 매개변수로 받는데, 이 컴포넌트는 결국 루트 컴포넌트 아닌가요 ????

루트 컴포넌트가 아닌 커스텀 컴포넌트들도 받는건지 궁금합니다

if (!options.componentStateMap.has(component)) {
options.componentStateMap.set(component, { states: [], currentStateKey: 0 });
}
return options.componentStateMap.get(component)!;
}

function useState(initState: any) {
const component = options.rootComponent;
const componentState = getComponentState(component);
const { currentStateKey: key, states } = componentState;

if (states.length === key) states.push(initState);

const state = states[key];
const setState = (newState: any) => {
states[key] = newState;
_render();
};

componentState.currentStateKey += 1;
return [state, setState];
}

const _render = debounceFrame(() => {
const { root, rootComponent } = options;
if (!root || !rootComponent) return;
root.innerHTML = rootComponent();

options.componentStateMap.forEach(state => (state.currentStateKey = 0));

options.renderCount += 1;
});

function useEffect<T extends (...arg: any[]) => any>(callback: T, dependencies: any[]) {
const component = options.rootComponent;
const componentState = getComponentState(component);
const { currentStateKey: key, states } = componentState;

const oldDependencies = states[key];
let hasChanged = true;

if (oldDependencies) {
hasChanged = dependencies.some((dep, index) => !Object.is(dep, oldDependencies[index]));
}
if (hasChanged) {
callback();
states[key] = dependencies;
}

componentState.currentStateKey += 1;
}

function useCallback<T extends (...arg: any[]) => any>(callback: T, dependencies: any[]) {
const component = options.rootComponent;
const componentState = getComponentState(component);
const { currentStateKey: key, states } = componentState;

const oldDependencies = states[key]?.dependencies;
let hasChanged = true;

if (oldDependencies) {
hasChanged =
dependencies.length !== oldDependencies.length ||
dependencies.some((dep, index) => !Object.is(dep, oldDependencies[index]));
}

if (hasChanged) {
states[key] = { callback, dependencies };
}

componentState.currentStateKey += 1;
return states[key].callback;
}

function useMemo<T>(callback: () => T, dependencies: any[]) {
const component = options.rootComponent;
const componentState = getComponentState(component);
const { currentStateKey: key, states } = componentState;

const [oldDependencies, oldMemoValue] = states[key] || [];
let hasChanged = true;
let memoValue = oldMemoValue;

if (oldDependencies) {
hasChanged = dependencies.some((dep, index) => !Object.is(dep, oldDependencies[index]));
}

if (hasChanged) {
memoValue = callback();
states[key] = [dependencies, memoValue];
}

componentState.currentStateKey += 1;
return memoValue;
}

function render(rootComponent: any, root: Element | null) {
options.root = root;
options.rootComponent = rootComponent;
_render();
}

return { useState, useEffect, useCallback, useMemo, render };
}

// state를 외부에서 관리하지 않아 계속 초기화 문제 발생
// function useCallback<T extends (...arg: any[]) => any>(fn: T, dependencies: any[]) {
// let cachedFn: T | null = null;
// let cachedDependencies: any[] | null = null;

// const hasChanged =
// cachedDependencies === null ||
// dependencies.length !== cachedDependencies.length ||
// dependencies.some((dependency, index) => !Object.is(dependency, cachedDependencies[index]));

// if (hasChanged) {
// cachedFn = fn;
// cachedDependencies = dependencies;
// }

// return cachedFn || fn;
// }
6 changes: 5 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
// Your code..
import MyReact from './core/MyReact';
import App from './App';

const { render } = MyReact();
render(App, document.querySelector('#app'));
7 changes: 7 additions & 0 deletions src/utils/debounceFrame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function debounceFrame(callback: FrameRequestCallback) {
let nextFrameCallback = -1;
return () => {
cancelAnimationFrame(nextFrameCallback);
nextFrameCallback = requestAnimationFrame(callback);
};
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
"include": ["src", "global.d.ts"]
}
Loading