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
51 changes: 51 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Deploy to GitHub Pages

on:
push: # push trigger
branches:
- easy
- feature-* # Feature 브랜치도 배포

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: true

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: latest # 여기 새로 추가되었습니다 (2025.11.18)

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build
run: pnpm run build

- name: Setup Pages
uses: actions/configure-pages@v4

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "./dist"

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
2,305 changes: 1,051 additions & 1,254 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions pnpm-workspace.yaml

This file was deleted.

70 changes: 68 additions & 2 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,71 @@
import { addEvent } from "./eventManager";
// import { normalizeVNode } from "./normalizeVNode";

export function createElement(vNode) {}
export function createElement(vNode) {
// 1. vNode가 null, undefined, boolean일 경우 빈 텍스트 노드 반환
if (vNode === null || vNode === undefined || typeof vNode === "boolean") {
return document.createTextNode("");
}

function updateAttributes($el, props) {}
// 2. vNode가 문자열이나 숫자면 텍스트 노드 생성
if (typeof vNode === "string" || typeof vNode === "number") {
return document.createTextNode(vNode);
}

// 함수형 컴포넌트는 정규화(normalize) 과정을 거쳐야 함
if (typeof vNode.type === "function") {
throw new Error(
"Cannot create element from a component. Normalize it first.",
);
}

// 3. vNode가 배열이면 DocumentFragment 생성
if (Array.isArray(vNode)) {
const fragment = document.createDocumentFragment();
vNode.forEach((child) => fragment.appendChild(createElement(child)));
return fragment;
}

// 4. 실제 DOM 요소 생성
const $el = document.createElement(vNode.type);

// 속성 적용
updateAttributes($el, vNode.props);

// 자식 요소 재귀적으로 생성 및 추가
vNode.children
.map(createElement) // 각 자식 vNode에 대해 DOM 요소 생성
.forEach((childElement) => {
$el.appendChild(childElement);
});

return $el;
}

function updateAttributes($el, props) {
if (!props) return;

for (const key in props) {
const value = props[key];

// 이벤트 핸들러 처리 (e.g., onClick)
if (key.startsWith("on")) {
const eventName = key.slice(2).toLowerCase();
addEvent($el, eventName, value);
}
// className 처리
else if (key === "className") {
$el.className = value;
}
// boolean 속성 처리 (e.g., disabled, checked)
else if (typeof value === "boolean") {
if (value) {
$el.setAttribute(key, "");
}
}
// 기타 모든 속성
else {
$el.setAttribute(key, value);
}
}
}
10 changes: 9 additions & 1 deletion src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export function createVNode(type, props, ...children) {
return {};
return {
type: type,
props: props,
children: children
.flat(Infinity)
.filter(
(child) => child !== null && child !== undefined && child !== false,
),
};
}
111 changes: 108 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,110 @@
export function setupEventListeners(root) {}
// 이벤트 핸들러를 저장할 중앙 레지스트리
// WeakMap<Element, Map<EventType, Set<Handler>>>
const eventRegistry = new WeakMap();

export function addEvent(element, eventType, handler) {}
// 지원할 이벤트 타입 목록
const supportedEvents = new Set();

export function removeEvent(element, eventType, handler) {}
/**
* 이벤트 위임을 처리할 최상위 리스너
* @param {Event} e
*/
function dispatchEvent(e) {
const eventType = e.type;
let currentTarget = e.target;

// 이벤트 버블링을 흉내내며 상위 요소로 이동
while (currentTarget) {
// 현재 요소에 대한 이벤트 맵 가져오기
const eventMap = eventRegistry.get(currentTarget);
if (eventMap) {
// 현재 이벤트 타입에 대한 핸들러 Set 가져오기
const handlers = eventMap.get(eventType);
if (handlers) {
// 모든 핸들러 실행
handlers.forEach((handler) => handler(e));
}
}

// 이벤트가 상위로 전파되는 것을 멈추면 루프 종료
if (e.bubbles === false || e.cancelBubble) {
break;
}

// 부모 요소로 이동
currentTarget = currentTarget.parentElement;
}
}

/**
* 루트 요소에 이벤트 리스너를 설정합니다.
* 이 함수는 애플리케이션 초기화 시 한 번만 호출되어야 합니다.
* @param {HTMLElement} root - 이벤트 리스너를 부착할 최상위 요소
*/
export function setupEventListeners(root) {
supportedEvents.forEach((eventType) => {
// 이미 리스너가 등록된 경우 중복 등록 방지
if (
!root.dataset.eventSetup ||
!root.dataset.eventSetup.includes(eventType)
) {
// 버블링 단계에서 처리하도록 수정 (세 번째 인자 제거)
root.addEventListener(eventType, dispatchEvent);
root.dataset.eventSetup = `${root.dataset.eventSetup || ""} ${eventType}`;
}
});
}

/**
* 요소에 이벤트 핸들러를 등록합니다.
* @param {HTMLElement} element - 이벤트를 등록할 요소
* @param {string} eventType - 이벤트 타입 (e.g., 'click')
* @param {Function} handler - 실행할 이벤트 핸들러
*/
export function addEvent(element, eventType, handler) {
// 지원하는 이벤트 목록에 추가
supportedEvents.add(eventType);

// 요소에 대한 이벤트 맵 가져오기 (없으면 생성)
let eventMap = eventRegistry.get(element);
if (!eventMap) {
eventMap = new Map();
eventRegistry.set(element, eventMap);
}

// 이벤트 타입에 대한 핸들러 Set 가져오기 (없으면 생성)
let handlers = eventMap.get(eventType);
if (!handlers) {
handlers = new Set();
eventMap.set(eventType, handlers);
}

// 핸들러 추가
handlers.add(handler);
}

/**
* 요소에서 이벤트 핸들러를 제거합니다.
* @param {HTMLElement} element - 이벤트가 등록된 요소
* @param {string} eventType - 이벤트 타입
* @param {Function} handler - 제거할 이벤트 핸들러
*/
export function removeEvent(element, eventType, handler) {
const eventMap = eventRegistry.get(element);
if (!eventMap) return;

const handlers = eventMap.get(eventType);
if (!handlers) return;

handlers.delete(handler);

// 핸들러 Set이 비면 Map에서 제거
if (handlers.size === 0) {
eventMap.delete(eventType);
}

// 이벤트 Map이 비면 WeakMap에서 제거
if (eventMap.size === 0) {
eventRegistry.delete(element);
}
}
44 changes: 43 additions & 1 deletion src/lib/normalizeVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
export function normalizeVNode(vNode) {
return vNode;
// 1. 최상위 레벨에서 직접 호출된 경우: null, undefined, boolean을 빈 문자열로 변환 (첫 번째 테스트 케이스 충족)
if (vNode === null || vNode === undefined || typeof vNode === "boolean") {
return "";
}

// 2. 숫자나 문자열은 텍스트 노드로 변환
if (typeof vNode === "string" || typeof vNode === "number") {
return String(vNode);
}

// 3. vNode가 배열인 경우, 각 항목을 정규화
if (Array.isArray(vNode)) {
return vNode.map(normalizeVNode).flat();
}

// vNode 객체가 아니면 그대로 반환
if (!vNode || !vNode.type) {
return vNode;
}

// 4. 함수형 컴포넌트 처리
if (typeof vNode.type === "function") {
const props = { ...(vNode.props || {}), children: vNode.children };
const renderedVNode = vNode.type(props);
return normalizeVNode(renderedVNode);
}

// 5. 네이티브 엘리먼트 처리 (e.g. type: 'div')
// 여기서 두 번째 테스트 케이스를 충족시킵니다.
const normalizedChildren = vNode.children
.flat()
// 먼저 Falsy 값들을 자식 배열에서 완전히 제거합니다.
.filter(
(child) =>
child !== null && child !== undefined && typeof child !== "boolean",
)
// 그 후에 남은 유효한 자식들만 재귀적으로 정규화합니다.
.map(normalizeVNode);

return {
...vNode,
children: normalizedChildren,
};
}
20 changes: 17 additions & 3 deletions src/lib/renderElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,21 @@ import { normalizeVNode } from "./normalizeVNode";
import { updateElement } from "./updateElement";

export function renderElement(vNode, container) {
// 최초 렌더링시에는 createElement로 DOM을 생성하고
// 이후에는 updateElement로 기존 DOM을 업데이트한다.
// 렌더링이 완료되면 container에 이벤트를 등록한다.
const normalizedVNode = normalizeVNode(vNode);

// 첫 렌더링인지 확인
if (!container.firstChild) {
// 첫 렌더링: 새 요소 생성
const element = createElement(normalizedVNode);
container.appendChild(element);
setupEventListeners(container);
} else {
// 업데이트: 기존 DOM을 새 VNode로 업데이트
updateElement(container.firstChild, normalizedVNode, container.__lastVNode);
}

// 다음 업데이트를 위해 VNode 저장
container.__lastVNode = normalizedVNode;

return container.firstChild;
}
Loading