Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
aeb9d3f
feat: add 'shallowEquals' and 'deepEquals'
Leehyunji0715 Nov 16, 2025
f0518c5
feat: 'createElement', 'normalizeNode', 'isEmptyValue' 'createTextEle…
Leehyunji0715 Nov 16, 2025
02a2418
feat: 전역컨텍스트 관리
Leehyunji0715 Nov 17, 2025
d52549c
feat: add 'context' and 'setup'
Leehyunji0715 Nov 17, 2025
8ddc127
feat: Phase 3 · DOM 인터페이스 구축
Leehyunji0715 Nov 18, 2025
35eac13
feat: render
Leehyunji0715 Nov 18, 2025
70e0c4f
feat: reconcile
Leehyunji0715 Nov 18, 2025
6fb14df
feat: useState -> enQueue -> clearHooks
Leehyunji0715 Nov 19, 2025
d178d6f
feat: 중첩 컴포넌트의 useState 독립적 동작 (각 컴포넌트별 path index 0부터 가지게 수정)
Leehyunji0715 Nov 19, 2025
6390687
feat: useEffect && and its clean up in reconciler
Leehyunji0715 Nov 19, 2025
8a8e103
feat: 'createElement', 'normalizeNode', 'isEmptyValue' 'createTextEle…
Leehyunji0715 Nov 16, 2025
7c5af06
feat: 전역컨텍스트 관리
Leehyunji0715 Nov 17, 2025
1f2ce0a
feat: add 'context' and 'setup'
Leehyunji0715 Nov 17, 2025
3be42f2
feat: Phase 3 · DOM 인터페이스 구축
Leehyunji0715 Nov 18, 2025
ebd5bd7
feat: render
Leehyunji0715 Nov 18, 2025
6f8cf75
feat: reconcile
Leehyunji0715 Nov 18, 2025
6422f9e
feat: useState -> enQueue -> clearHooks
Leehyunji0715 Nov 19, 2025
508e07c
feat: 중첩 컴포넌트의 useState 독립적 동작 (각 컴포넌트별 path index 0부터 가지게 수정)
Leehyunji0715 Nov 19, 2025
d882ee8
feat: useEffect && and its clean up in reconciler
Leehyunji0715 Nov 19, 2025
4885ef2
feat: useRef 구현
Leehyunji0715 Nov 19, 2025
2a42ed3
feat: useMemo
Leehyunji0715 Nov 19, 2025
936b940
feat: useCallback
Leehyunji0715 Nov 19, 2025
69bedad
feat: useAutoCallback
Leehyunji0715 Nov 19, 2025
51a8b11
feat: useDeepMemo
Leehyunji0715 Nov 19, 2025
bca1df6
feat: memo
Leehyunji0715 Nov 19, 2025
bae9e58
Merge pull request #2 from Leehyunji0715/dev
Leehyunji0715 Nov 19, 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
10 changes: 10 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,14 @@ export default tseslint.config([
...tseslint.configs.recommended,
eslintPluginPrettier,
eslintConfigPrettier,
{
rules: {
"prettier/prettier": [
"error",
{
endOfLine: "auto",
},
],
},
},
]);
1 change: 1 addition & 0 deletions packages/react/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const NodeTypes = {
export type NodeType = ValueOf<typeof NodeTypes>;

export const HookTypes = {
STATE: "state",
EFFECT: "effect",
} as const;

Expand Down
43 changes: 28 additions & 15 deletions packages/react/src/core/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ export const context: Context = {
node: null,
instance: null,
reset({ container, node }) {
// 여기를 구현하세요.
// container, node, instance를 전달받은 값으로 초기화합니다.
this.container = container;
this.node = node;
this.instance = null;
},
},

Expand All @@ -32,36 +33,48 @@ export const context: Context = {
* 모든 훅 관련 상태를 초기화합니다.
*/
clear() {
// 여기를 구현하세요.
// state, cursor, visited, componentStack을 모두 비웁니다.
this.state.clear();
this.cursor.clear();
this.visited.clear();
this.componentStack = [];
},

/**
* 현재 실행 중인 컴포넌트의 고유 경로를 반환합니다.
*/
get currentPath() {
// 여기를 구현하세요.
// componentStack의 마지막 요소를 반환해야 합니다.
// 스택이 비어있으면 '훅은 컴포넌트 내부에서만 호출되어야 한다'는 에러를 발생시켜야 합니다.
return "";
if (this.componentStack.length === 0) {
throw new Error("훅은 컴포넌트 내부에서만 호출되어야 합니다");
}
return this.componentStack[this.componentStack.length - 1];
},

/**
* 현재 컴포넌트에서 다음에 실행될 훅의 인덱스(커서)를 반환합니다.
*/
get currentCursor() {
// 여기를 구현하세요.
// cursor Map에서 현재 경로의 커서를 가져옵니다. 없으면 0을 반환합니다.
return 0;
const path = this.currentPath;
return this.cursor.get(path) || 0;
},

/**
* 현재 컴포넌트의 훅 상태 배열을 반환합니다.
*/
get currentHooks() {
// 여기를 구현하세요.
// state Map에서 현재 경로의 훅 배열을 가져옵니다. 없으면 빈 배열을 반환합니다.
return [];
const path = this.currentPath;
if (!this.state.has(path)) {
this.state.set(path, []);
}
return this.state.get(path)!;
},

/**
* 현재 컴포넌트의 훅 커서를 다음 위치로 이동시킵니다.
*/
moveCursor() {
const path = this.currentPath;
const currentCursor = this.cursor.get(path) || 0;
this.cursor.set(path, currentCursor + 1);
},
},

Expand All @@ -71,4 +84,4 @@ export const context: Context = {
effects: {
queue: [],
},
};
};
155 changes: 145 additions & 10 deletions packages/react/src/core/dom.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,71 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NodeType, NodeTypes } from "./constants";
import { NodeTypes } from "./constants";
import { Instance } from "./types";

const isFunctionHandler = ({ key, value }: { key: string; value: any }) => {
return key.startsWith("on") && typeof value === "function";
};

const setSingleProp = (dom: HTMLElement, { key, value }: { key: string; value: any }) => {
if (key === "children") return;

if (isFunctionHandler({ key, value })) {
const eventType = key.slice(2).toLowerCase();
dom.addEventListener(eventType, value);
return;
}

// className을 class로 변환
if (key === "className") {
dom.setAttribute("class", value);
return;
}

// htmlFor를 for로 변환
if (key === "htmlFor") {
dom.setAttribute("for", value);
return;
}

// style 객체 처리
if (key === "style" && typeof value === "object") {
const cssText = Object.entries(value)
.map(([styleKey, styleValue]) => {
// camelCase를 kebab-case로 변환 (fontSize -> font-size)
const kebabKey = styleKey.replace(/([A-Z])/g, "-$1").toLowerCase();
return `${kebabKey}: ${styleValue}`;
})
.join("; ");
(dom as any).style.cssText = cssText;
return;
}

// 불린 속성 처리
if (typeof value === "boolean") {
if (value) {
dom.setAttribute(key, "");
}
return;
}

// null이나 undefined는 속성 제거
if (value == null) {
dom.removeAttribute(key);
return;
}

// 일반 속성
dom.setAttribute(key, String(value));
};

/**
* DOM 요소에 속성(props)을 설정합니다.
* 이벤트 핸들러, 스타일, className 등 다양한 속성을 처리해야 합니다.
*/
export const setDomProps = (dom: HTMLElement, props: Record<string, any>): void => {
// 여기를 구현하세요.
Object.entries(props).forEach(([key, value]) => {
setSingleProp(dom, { key, value });
});
};

/**
Expand All @@ -19,31 +77,73 @@ export const updateDomProps = (
prevProps: Record<string, any> = {},
nextProps: Record<string, any> = {},
): void => {
// 여기를 구현하세요.
for (const [key, value] of Object.entries(nextProps)) {
if (prevProps[key] !== nextProps[key]) {
if (isFunctionHandler({ key, value })) {
const eventType = key.slice(2).toLowerCase();
dom.removeEventListener(eventType, prevProps[key]);
}

setSingleProp(dom, { key, value });
}
}

for (const key of Object.keys(prevProps)) {
if (nextProps[key] === undefined) {
dom.removeAttribute(key);
}
}
};

/**
* 주어진 인스턴스에서 실제 DOM 노드(들)를 재귀적으로 찾아 배열로 반환합니다.
* Fragment나 컴포넌트 인스턴스는 여러 개의 DOM 노드를 가질 수 있습니다.
*/
export const getDomNodes = (instance: Instance | null): (HTMLElement | Text)[] => {
// 여기를 구현하세요.
return [];
if (!instance) {
return [];
}

// HOST나 TEXT 타입이면 직접 DOM 노드를 가지고 있음
if (instance.kind === NodeTypes.HOST || instance.kind === NodeTypes.TEXT) {
return instance.dom ? [instance.dom] : [];
}

// COMPONENT나 FRAGMENT 타입이면 자식들로부터 DOM 노드 수집
const nodes: (HTMLElement | Text)[] = [];
for (const child of instance.children) {
nodes.push(...getDomNodes(child));
}
return nodes;
};

/**
* 주어진 인스턴스에서 첫 번째 실제 DOM 노드를 찾습니다.
*/
export const getFirstDom = (instance: Instance | null): HTMLElement | Text | null => {
// 여기를 구현하세요.
return null;
if (!instance) {
return null;
}

// HOST나 TEXT 타입이면 직접 DOM 노드를 반환
if (instance.kind === NodeTypes.HOST || instance.kind === NodeTypes.TEXT) {
return instance.dom;
}

// COMPONENT나 FRAGMENT 타입이면 자식들로부터 첫 번째 DOM 노드 찾기
return getFirstDomFromChildren(instance.children);
};

/**
* 자식 인스턴스들로부터 첫 번째 실제 DOM 노드를 찾습니다.
*/
export const getFirstDomFromChildren = (children: (Instance | null)[]): HTMLElement | Text | null => {
// 여기를 구현하세요.
for (const child of children) {
const dom = getFirstDom(child);
if (dom) {
return dom;
}
}
return null;
};

Expand All @@ -56,12 +156,47 @@ export const insertInstance = (
instance: Instance | null,
anchor: HTMLElement | Text | null = null,
): void => {
// 여기를 구현하세요.
if (!instance) {
return;
}

// 타입별 최적화된 처리
switch (instance.kind) {
case NodeTypes.HOST:
case NodeTypes.TEXT:
// 직접 DOM이 있는 경우 바로 삽입
if (instance.dom) {
parentDom.insertBefore(instance.dom, anchor);
}
break;

case NodeTypes.COMPONENT:
case NodeTypes.FRAGMENT: {
// 자식들을 재귀적으로 삽입
const domNodes = getDomNodes(instance);
for (const domNode of domNodes) {
if (domNode) {
parentDom.insertBefore(domNode, anchor);
}
}
break;
}
}
};

/**
* 부모 DOM에서 인스턴스에 해당하는 모든 DOM 노드를 제거합니다.
*/
export const removeInstance = (parentDom: HTMLElement, instance: Instance | null): void => {
// 여기를 구현하세요.
if (!instance) {
return;
}

// DOM 노드들 제거
const domNodes = getDomNodes(instance);
for (const domNode of domNodes) {
if (domNode && domNode.parentNode === parentDom) {
parentDom.removeChild(domNode);
}
}
};
Loading