Skip to content
Open
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
Empty file added 404.html
Empty file.
169 changes: 167 additions & 2 deletions src/lib/createElement.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,170 @@
import { addEvent } from "./eventManager";

export function createElement(vNode) {}
// Boolean 속성 목록
const BOOLEAN_PROPS = [
"checked",
"selected",
"disabled",
"readonly",
"readOnly",
"multiple",
"autofocus",
"required",
"autoplay",
"controls",
"loop",
"muted",
"default",
"open",
];

function updateAttributes($el, props) {}
/**
* VNode를 실제 DOM 요소로 변환
* @param {VNode} vNode - 변환할 VNode
* @returns {Node} - 생성된 DOM 요소 또는 TextNode
*/
export function createElement(vNode) {
// Step 1: 원시 값(문자열, 숫자) 처리
if (typeof vNode === "string" || typeof vNode === "number") {
return document.createTextNode(vNode);
}

// Step 2: null, undefined 처리
if (vNode == null) {
return document.createTextNode("");
}

// Step 3: 배열 처리 (map으로 각 항목을 createElement 호출)
if (Array.isArray(vNode)) {
//fragment 사용 - 노드들을 fragment안에 담아놓고 한번에 dom에 추가하기 위함
const fragment = document.createDocumentFragment();
vNode.forEach((node) => {
fragment.appendChild(createElement(node));
});
return fragment;
}

// Step 4: 객체(VNode) 처리
if (typeof vNode === "object") {
const { type, props, children } = vNode;

// Step 4-1: 함수형 컴포넌트 처리 - 오류 발생
// 함수형 컴포넌트는 normalizeVNode로 미리 정규화되어야 함
if (typeof type === "function") {
throw new Error(
`컴포넌트는 반드시 normalizeVNode로 정규화된 후에 createElement를 호출해야 합니다. 받은 컴포넌트: ${type.name}`,
);
}

// Step 4-2: HTML 태그 요소 처리
if (typeof type === "string") {
const $el = document.createElement(type);

// Step 4-3: props 적용 (className, onClick, data-* 등)
if (props) {
updateAttributes($el, props);
}

// Step 4-4: children 추가
if (children && children.length > 0) {
children.forEach((child) => {
// undefined는 무시
if (child != null) {
const childNode = createElement(child);
if (childNode) {
$el.appendChild(childNode);
}
}
});
}

return $el;
}
}

// 예상 밖의 타입
return document.createTextNode("");
}

/**
* DOM 요소에 props(속성)을 적용
* @param {HTMLElement} $el - 대상 DOM 요소
* @param {Object} props - 적용할 속성 객체
*/
function updateAttributes($el, props) {
Object.entries(props).forEach(([key, value]) => {
// Step 1: 이벤트 핸들러 (onClick, onChange 등)
if (key.startsWith("on")) {
const eventType = key.slice(2).toLowerCase(); // onClick -> click
addEvent($el, eventType, value);
return;
}

// Step 2: 특수 속성 무시
if (key === "key" || key === "ref" || key === "children") {
return;
}

// Step 3: className → class로 변환
if (key === "className") {
$el.setAttribute("class", value);
return;
}

// Step 4: style 처리
if (key === "style") {
if (typeof value === "object") {
// 객체 형태: { color: 'red', fontSize: '16px' }
Object.entries(value).forEach(([cssKey, cssValue]) => {
$el.style[cssKey] = cssValue;
});
} else if (typeof value === "string") {
// 문자열 형태: "color: red; font-size: 16px;"
$el.setAttribute("style", value);
}
return;
}

// Step 5: data-* 속성
if (key.startsWith("data-")) {
$el.setAttribute(key, value);
return;
}

// Step 5.5: Boolean 속성 처리
const lowerKey = key.toLowerCase();
if (BOOLEAN_PROPS.includes(key) || BOOLEAN_PROPS.includes(lowerKey)) {
// property로 직접 설정
$el[key] = Boolean(value);

const attrName = key === "readOnly" ? "readonly" : lowerKey;

// disabled, readonly는 attribute도 설정 (true일 때만)
if (
value &&
(key === "disabled" || key === "readonly" || key === "readOnly")
) {
$el.setAttribute(attrName, "");
} else {
// checked, selected는 attribute 설정하지 않음
// false인 경우도 attribute 제거
$el.removeAttribute(attrName);
}
return;
}

// Step 6: 표준 HTML 속성 (id, type, placeholder, disabled 등)
if (value != null && value !== false) {
if (value === true) {
// boolean 속성 (disabled, checked 등)
$el.setAttribute(key, "");
} else {
// 일반 속성
$el.setAttribute(key, value);
}
} else if (value === false) {
// false인 경우 속성 제거
$el.removeAttribute(key);
}
});
}
32 changes: 31 additions & 1 deletion src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
/**
* 배열을 재귀적으로 완전히 평탄화하고 falsy 값 필터링
* @param {Array} arr - 평탄화할 배열
* @returns {Array} - 완전히 평탄화되고 falsy 값이 제거된 배열
*/
function flattenDeep(arr) {
return arr.reduce((flat, item) => {
// null, undefined, boolean(false)은 제외
if (item == null || item === false) {
return flat;
}

// 배열인 경우 재귀적으로 평탄화
if (Array.isArray(item)) {
return flat.concat(flattenDeep(item));
}

// 배열이 아니면 그대로 추가
return flat.concat(item);
}, []);
}

//이 함수가 jsx를 VNode로 변환(jsx 주석을 통해 명시적 지정)
export function createVNode(type, props, ...children) {
return {};
if (type === "input") {
console.log("type", type, props);
}
return {
type,
props,
children: flattenDeep(children),
};
}
160 changes: 157 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,159 @@
export function setupEventListeners(root) {}
// 요소별 이벤트 핸들러를 저장하는 WeakMap
const eventHandlers = new Map();

export function addEvent(element, eventType, handler) {}
/**
* 요소에 이벤트 핸들러 등록
* @param {HTMLElement} element - 대상 요소
* @param {string} eventType - 이벤트 타입 (click, change, input 등)
* @param {Function} handler - 이벤트 핸들러 함수
*/
export function addEvent(element, eventType, handler) {
const normalizedType = eventType.startsWith("on")
? eventType.slice(2).toLowerCase()
: eventType.toLowerCase();

export function removeEvent(element, eventType, handler) {}
if (normalizedType === "keydown") {
// ✅ keydown만 로깅
console.log(`[addEvent] type: ${normalizedType}, element:`, element);
}

if (!eventHandlers.has(element)) {
eventHandlers.set(element, {});
}

const handlers = eventHandlers.get(element);

if (!handlers[normalizedType]) {
handlers[normalizedType] = [];
}

if (!handlers[normalizedType].includes(handler)) {
handlers[normalizedType].push(handler);
if (normalizedType === "keydown") {
// ✅ keydown만 로깅
console.log(
`[addEvent] keydown 핸들러 등록 완료:`,
handlers[normalizedType],
);
}
}
}

/**
* 요소에서 이벤트 핸들러 제거
* @param {HTMLElement} element - 대상 요소
* @param {string} eventType - 이벤트 타입
* @param {Function} handler - 제거할 이벤트 핸들러
*/

export function removeEvent(element, eventType, handler) {
// Step 1: 이벤트 타입 정규화
const normalizedType = eventType.startsWith("on")
? eventType.slice(2).toLowerCase()
: eventType.toLowerCase();

// Step 2: 해당 요소의 핸들러 조회
if (!eventHandlers.has(element)) {
return;
}

const handlers = eventHandlers.get(element);
if (!handlers[normalizedType]) {
return;
}

// ✅ Step 3: handler가 전달된 경우 특정 핸들러만 제거
if (handler) {
handlers[normalizedType] = handlers[normalizedType].filter(
(h) => h !== handler,
);
} else {
// handler가 없으면 모든 핸들러 제거
handlers[normalizedType] = [];
}

// Step 4: 핸들러 배열이 비어있으면 타입 자체를 삭제
if (handlers[normalizedType].length === 0) {
delete handlers[normalizedType];
}

// Step 5: 모든 이벤트 타입이 비어있으면 요소 자체를 삭제
if (Object.keys(handlers).length === 0) {
eventHandlers.delete(element);
}
}

/**
* 루트 요소에 이벤트 위임(Event Delegation) 설정
* container의 모든 자식 요소에서 발생하는 이벤트를 위임 방식으로 처리
* @param {HTMLElement} root - 루트 요소
*/
export function setupEventListeners(root) {
if (!root) {
return;
}

console.log(`[setupEventListeners] 시작, root:`, root);

const eventTypes = [
"click",
"change",
"input",
"mouseover",
"focus",
"keydown",
];

eventTypes.forEach((eventType) => {
if (root._delegatedListeners && root._delegatedListeners[eventType]) {
if (eventType === "keydown") {
// ✅ keydown만 로깅
console.log(`[setupEventListeners] ${eventType} 이미 등록됨`);
}
return;
}

const delegatedListener = (event) => {
if (eventType === "keydown") {
// ✅ keydown만 로깅
console.log(
`[delegatedListener] ${eventType} 이벤트 발생:`,
event.target,
eventHandlers,
);
}

let target = event.target;
while (target && target !== root) {
if (eventHandlers.has(target)) {
const handlers = eventHandlers.get(target);
if (handlers[eventType]) {
if (eventType === "keydown") {
// ✅ keydown만 로깅
console.log(
`[delegatedListener] keydown 핸들러 실행:`,
handlers[eventType].length,
);
}
handlers[eventType].forEach((handler) => {
handler(event);
});
}
}
target = target.parentNode;
}
};

root.addEventListener(eventType, delegatedListener, false);

if (eventType === "keydown") {
// ✅ keydown만 로깅
console.log(`[setupEventListeners] ${eventType} 위임 리스너 등록 완료`);
}

if (!root._delegatedListeners) {
root._delegatedListeners = {};
}
root._delegatedListeners[eventType] = delegatedListener;
});
}
Loading