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
2 changes: 2 additions & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"pnpm": ">=10"
},
"type": "module",
"homepage": "https://ahnsummer.github.io/front-chapter2-2",
"scripts": {
"dev": "vite",
"build": "vite build",
"deploy:gh-pages": "pnpm run build && gh-pages -d dist",
"lint:fix": "eslint --fix",
"prettier:write": "prettier --write ./src",
"preview": "vite preview"
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/pages/PageWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const close = () => {
export const PageWrapper = ({ headerLeft, children }) => {
const cart = cartStore.getState();
const { cartModal, toast } = uiStore.getState();

const cartSize = cart.items.length;

const cartCount = useMemo(
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/__tests__/basic.mini-react.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ describe("Chapter 2-2 기본과제: MiniReact", () => {
setup(<div style={{ backgroundColor: "red", display: "flex" }}>styled</div>, container);

const div = container.firstElementChild as HTMLElement;

expect(div).not.toBeNull();
expect(div?.style.backgroundColor).toBe("red");
expect(div?.style.display).toBe("flex");
Expand Down
30 changes: 21 additions & 9 deletions packages/react/src/core/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ 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 +34,46 @@ export const context: Context = {
* 모든 훅 관련 상태를 초기화합니다.
*/
clear() {
// 여기를 구현하세요.
// state, cursor, visited, componentStack을 모두 비웁니다.
this.state.clear();
this.cursor.clear();
this.visited.clear();
this.componentStack = [];
},

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

if (!path) throw new Error("훅은 컴포넌트 내부에서만 호출되어야 한다");

return path;
},

/**
* 현재 컴포넌트에서 다음에 실행될 훅의 인덱스(커서)를 반환합니다.
*/
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)!;
},
},

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

const isStyleKey = (
style: CSSStyleDeclaration,
styleKey: string | number | symbol,
): styleKey is keyof CSSStyleDeclaration => {
return styleKey in style;
};

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

const value = props[key];

// 이벤트 핸들러
if (key.startsWith("on")) {
const eventName = key.toLowerCase().replace("on", "");
dom.addEventListener(eventName, value);
return;
}

// 스타일 객체
if (key === "style" && typeof value === "object") {
let styleString = "";

Object.keys(value).forEach((styleKey) => {
if (!isStyleKey(dom.style, styleKey)) return;

styleString += `${styleKey.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${value[styleKey]};`;
});

dom.setAttribute("style", styleString);

return;
}

// className
if (key === "className") {
dom.setAttribute("class", value || "");
return;
}

if (key === "value" && dom instanceof HTMLInputElement) {
dom.value = value;
}

// 일반 속성
if (value === true) {
dom.setAttribute(key, "");
} else if (value === false || value === null) {
dom.removeAttribute(key);
} else {
(dom as Record<string, any>)[key] = value;
dom.setAttribute(key, String(value));
}
});
};

/**
Expand All @@ -19,31 +72,110 @@ export const updateDomProps = (
prevProps: Record<string, any> = {},
nextProps: Record<string, any> = {},
): void => {
// 여기를 구현하세요.
Object.keys(prevProps).forEach((key) => {
if (key === "children") return;
if (key in nextProps) return;

if (key.startsWith("on")) {
const eventName = key.toLowerCase().replace("on", "");
dom.removeEventListener(eventName, prevProps[key]);
return;
}

if (key === "className") {
dom.removeAttribute("class");
return;
}

dom.removeAttribute(key);
});

Object.keys(nextProps).forEach((key) => {
if (key === "children") return;

const prevValue = prevProps[key];
const nextValue = nextProps[key];

if (prevValue === nextValue) return;

if (key.startsWith("on")) {
const eventName = key.toLowerCase().replace("on", "");

if (prevProps) dom.removeEventListener(eventName, prevValue);
if (nextValue) dom.addEventListener(eventName, nextValue);

return;
}

if (key === "style") {
let styleString = "";

Object.keys(nextValue).forEach((styleKey) => {
if (!isStyleKey(dom.style, styleKey)) return;

styleString += `${styleKey.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${nextValue[styleKey]};`;
});

dom.setAttribute("style", styleString);

return;
}

if (key === "className") {
dom.setAttribute("class", nextValue || "");
return;
}

if (key === "value" && dom instanceof HTMLInputElement) {
dom.value = nextValue;
}

if (nextValue === true) {
dom.setAttribute(key, "");
} else if (nextValue === false || nextValue == null) {
dom.removeAttribute(key);
} else {
dom.setAttribute(key, String(nextValue));
}
});
};

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

if (instance.kind === NodeTypes.HOST || instance.kind === NodeTypes.TEXT) {
return instance.dom ? [instance.dom] : [];
}

const nodes: (HTMLElement | Text)[] = [];
instance.children.forEach((child) => nodes.push(...getDomNodes(child))); // 재귀 호출

return nodes;
};

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

if (instance.kind === NodeTypes.HOST || instance.kind === NodeTypes.TEXT) return instance.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 +188,26 @@ export const insertInstance = (
instance: Instance | null,
anchor: HTMLElement | Text | null = null,
): void => {
// 여기를 구현하세요.
if (!instance) return;

const domNodes = getDomNodes(instance);

domNodes.forEach((node) => {
if (anchor) {
parentDom.insertBefore(node, anchor);
} else {
parentDom.appendChild(node);
}
});
};

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

const domNodes = getDomNodes(instance);

domNodes.forEach((node) => parentDom.removeChild(node));
};
Loading
Loading