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
107 changes: 95 additions & 12 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,52 +13,132 @@

#### 가상돔을 기반으로 렌더링하기

- [ ] createVNode 함수를 이용하여 vNode를 만든다.
- [ ] normalizeVNode 함수를 이용하여 vNode를 정규화한다.
- [ ] createElement 함수를 이용하여 vNode를 실제 DOM으로 만든다.
- [ ] 결과적으로, JSX를 실제 DOM으로 변환할 수 있도록 만들었다.
- [x] createVNode 함수를 이용하여 vNode를 만든다.
- [x] normalizeVNode 함수를 이용하여 vNode를 정규화한다.
- [x] createElement 함수를 이용하여 vNode를 실제 DOM으로 만든다.
- [x] 결과적으로, JSX를 실제 DOM으로 변환할 수 있도록 만들었다.

#### 이벤트 위임

- [ ] 노드를 생성할 때 이벤트를 직접 등록하는게 아니라 이벤트 위임 방식으로 등록해야 한다
- [ ] 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다
- [ ] 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다
- [x] 노드를 생성할 때 이벤트를 직접 등록하는게 아니라 이벤트 위임 방식으로 등록해야 한다
- [x] 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다
- [x] 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다

### 심화 과제

#### Diff 알고리즘 구현

- [ ] 초기 렌더링이 올바르게 수행되어야 한다
- [ ] diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다
- [ ] 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다
- [ ] 요소의 속성만 변경되었을 때 요소를 재사용해야 한다
- [ ] 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다
- [x] 초기 렌더링이 올바르게 수행되어야 한다
- [x] diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다
- [x] 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다
- [x] 요소의 속성만 변경되었을 때 요소를 재사용해야 한다
- [x] 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다

## 과제 셀프회고

<!-- 과제에 대한 회고를 작성해주세요 -->

### 아하! 모먼트 (A-ha! Moment)

- Virtual DOM의 본질 이해
Virtual DOM이 단순히 "성능 최적화 도구"라고만 생각했는데, 실제로 구현해보니 "DOM 형태를 본뜬 객체 덩어리"라는 것을 체감했습니다. Real DOM의 구조를 JavaScript 객체로 표현하고, 변경사항을 메모리에서 먼저 계산한 뒤 최종 결과만 실제 DOM에 반영한다는 개념이 명확해졌습니다. 브라우저의 렌더링 과정은 HTML을 파싱하여 DOM Tree를 생성하고, CSS를 파싱하여 CSSOM을 만든 후, 이 둘을 결합하여 Render Tree를 구성합니다. 그 후 Layout 단계에서 각 요소의 정확한 위치와 크기를 계산하고, 마지막으로 Painting 단계에서 픽셀을 화면에 그립니다. Virtual DOM은 이 과정에서 변경사항을 일괄 처리하여 불필요한 reflow와 repaint를 최소화합니다.


- JSX 변환 과정에서의 발견
JSX에서 {null}, {undefined}, {false}, {true} 같은 표현식들이 Babel 변환 시 그대로 children 배열에 포함된다는 것을 알게 되었습니다. React에서 false, null, undefined, true는 유효한 children이지만 실제로 렌더링되지 않습니다. 이로 인해 normalizeVNode에서 이런 값들을 적절히 필터링하지 않으면 예상치 못한 동작이 발생할 수 있다는 점을 배웠습니다.
<!--
과제를 진행하며 "아!" 하고 깨달음을 얻었던 순간이 있다면 공유해주세요.
어떤 부분에서 어려움을 겪다가, 어떤 계기로 개념이 명확해졌나요?
-->

### 기술적 성장

#### 기존의 알고 있던 개념 되짚어보기
#### 브라우저의 workflow
- 기존의 알고 있었던 원리였지만 이번 과제를 통해서 더 자세히 개념을 정립할 수 있는 기회가 되었습니다
- Dom Tree 생성 -> Render Tree 생성 -> Layout -> Painting
- Dom에 변화가 생기면 렌더트리를 재생성하고 (모든 요소들의 스타일 재계산) 레이아웃을 만들고 페인팅 하는 과정의 반복
참고 : https://velopert.com/3236

#### Virtual Dom
- 리액트의 기존 원리가 되는 가상돔에 대해 개념을 한번 더 짚고 나가는 기회가 되었습니다.
- 뷰에 변화가 있을 시 실제 DOM에 적용되기 전에 가상의 DOM에 먼저 적용시키고 최종적인 결과 값만 실제 DOM에 전달 => 브라우저 내에서 발생하는 연산의 양을 줄이면서 성능 개선 효과를 보임

#### 새로 학습한 개념

#### js로 dom을 생성 시 babel의 역할
- JSX의 문법을 일반 js 함수 호출로 변환 => 코드를 텍스트로 변환하는 컴파일러!
```js

// 변환 전 (JSX)
<div className="box">Hello</div>

// 변환 후 (JavaScript)
createElement('div', { className: 'box' }, 'Hello')

```

####배열의 평탄화 arr.flat(depth)
- depth: 중첩 배열 구조를 평탄화 할 때 사용할 깊이 값이자 생략 시 기본값 1
``` js
const arr = [1, 2, [3, 4, [5, 6]]];
arr.flat(); // [1, 2, 3, 4, [5, 6]]
arr.flat(2); // [1, 2, 3, 4, 5, 6]
const arr2 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr2.flat(Infinity); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
```

#### documentFragment
- 여러 DOM 요소들을 메모리에 일시적으로 저장할 수 있는 객체
- 여러 개의 요소들을 모아두었다가 DOcumentFragment요소를 한번에 HTML DOM 에 추가

=> createElement와의 차이점이 몰까 생각해봤다.
=> 단일 요소를 생성할 때는 createElement, 여러 요소를 한번에 추가할 때는 documentFlagment를 사용해야 불필요한 reflow 과정을 없앨 수 있다.


####기존 지식의 재발견/심화

자료구조 선택의 중요성: 이벤트 핸들러 저장을 위해 처음엔 배열을 고려했지만, 중복된 이벤트 타입 처리가 필요해 Map으로 변경했습니다. 이전 회사에서 사용했던 HashMap 경험이 도움이 되었습니다.

####구현 과정에서의 기술적 도전과 해결
- updateElement의 children 타입 불일치 문제

문제: children의 타입이 다를 경우 교체 로직 누락, DOM 인덱스가 어긋나면서 잘못된 위치에 요소 추가
원인: normalizeVNode에서 빈 문자열이 children 배열에 포함되어 메인 화면에 빈 상품카드가 생성됨
해결: updateElement의 타입 비교 로직을 정규화하고, 동적 인덱스를 추적하는 방식으로 개선


- 이벤트 리스너 중복 등록 문제

문제: 기존 이벤트 리스너를 제거하지 않아 중복 등록 발생
해결: rootEventHandlers.clear()를 사용하여 기존 핸들러 제거 (효율성에 대한 의문은 남음)
<!-- 예시
- 새로 학습한 개념
- 기존 지식의 재발견/심화
- 구현 과정에서의 기술적 도전과 해결
-->

### 코드 품질

####리팩토링이 필요한 부분
- updateElement.js , createElement.js 부분이 조건문 사용이 많아 깔끔해 보이지가 않아서 어떻게 리팩토링을 할 지 고민중입니다.
<!-- 예시
- 특히 만족스러운 구현
- 리팩토링이 필요한 부분
- 코드 설계 관련 고민과 결정
-->

### 학습 효과 분석

####가장 큰 배움이 있었던 부분

Virtual DOM의 동작 원리를 이론이 아닌 실제 구현을 통해 체득했습니다. Virtual DOM은 성능 향상을 위해 변경사항을 JavaScript 엔진의 메모리에서 먼저 계산하고, 최소한의 DOM 조작만 수행하여 비용이 큰 reflow와 repaint를 줄입니다. Stack OverflowTalent500

####추가 학습이 필요한 영역

Diffing 알고리즘의 최적화: 현재 구현은 기본적인 비교만 수행하는데, React의 Fiber 아키텍처처럼 더 효율적인 업데이트 방식에 대해 학습이 필요합니다.
이벤트 핸들러 관리: 현재 방식보다 메모리 효율적이고 성능이 좋은 패턴을 연구하고 싶습니다.

<!-- 예시
- 가장 큰 배움이 있었던 부분
- 추가 학습이 필요한 영역
Expand All @@ -73,6 +153,9 @@

## 리뷰 받고 싶은 내용

- updateElement.js 파일의 updateElement 함수의 조건문들이 너무 남발되어서 가독성이 떨어지는데 다른 조건문을 써도 같은 결과가 나올 것 같은데 어떠한 코드 구조로 작성해야 더 가독성이 좋은 코드가 되는지 궁금합니다.

- eventManaget.jsdml setupEventListeners작성 중 기존에 있던 이벤트 리스너를 제거를 안해서 중복 등록하는 경우가 발생하여 rootEventHandlers.clear()를 통해 제거를 하였는데 변경된 내용만 선택적으로 업데이트 하는 방식이 괜찮은 것 같은데 또 이러면 코드 내용이 길어지니 과연 효율적인 방식인가?? 하는 의문이 생겼습니다.
<!--
피드백 받고 싶은 내용을 구체적으로 남겨주세요
모호한 요청은 피드백을 남기기 어렵습니다.
Expand Down
26 changes: 26 additions & 0 deletions 404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>페이지를 찾을 수 없습니다 - 상품 쇼핑몰</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/src/styles.css" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280",
},
},
},
};
</script>
</head>
<body class="bg-gray-50">
<div id="root"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"test:e2e:report": "npx playwright show-report",
"test:generate": "playwright codegen localhost:5173",
"prepare": "husky",
"gh-pages": "pnpm run build && gh-pages -d dist"
"predeploy": "pnpm run build",
"deploy": "gh-pages -d dist"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
Expand All @@ -31,6 +32,7 @@
]
},
"devDependencies": {
"homepage" : "https://toeam.github.io/front_7th_chapter2-2/",
"@babel/core": "latest",
"@babel/plugin-transform-react-jsx": "latest",
"@eslint/js": "^9.16.0",
Expand Down
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";

export function createElement(vNode) {}
export function createElement(vNode) {
// 1. vNode가 null, undefined, boolean 일 경우, 빈 텍스트 노드를 반환합니다.
if (vNode === null || vNode === undefined || typeof vNode === "boolean") {
return document.createTextNode("");
}
// 2. vNode가 문자열이나 숫자면 텍스트 노드를 생성하여 반환합니다.
else if (typeof vNode === "string" || typeof vNode === "number") {
return document.createTextNode(vNode);
}
// 3. vNode가 배열이면 DocumentFragment를 생성하고 각 자식에 대해 createElement를 재귀 호출하여 추가합니다.
else if (Array.isArray(vNode)) {
const fragment = document.createDocumentFragment();
vNode.forEach((child) => {
fragment.appendChild(createElement(child));
});
return fragment;
}
// 4. 위 경우가 아니면 실제 DOM 요소를 생성합니다:
// - vNode.type에 해당하는 요소를 생성
// - vNode.props의 속성들을 적용 (이벤트 리스너, className, 일반 속성 등 처리)
// - vNode.children의 각 자식에 대해 createElement를 재귀 호출하여 추가
else {
const $el = document.createElement(vNode.type);

function updateAttributes($el, props) {}
// 속성 설정
updateAttributes($el, vNode.props);

vNode.children.forEach((child) => {
$el.appendChild(createElement(child));
});
return $el;
}
}
function updateAttributes($el, props) {
if (!props) return;

Object.entries(props)
.filter(([, value]) => value)
.forEach(([attr, value]) => {
// 이벤트 핸들러 처리
if (attr.startsWith("on")) {
const eventType = attr.slice(2).toLowerCase(); // "onClick" → "click"
addEvent($el, eventType, value);
}
// className 처리
else if (attr === "className") {
$el.className = value;
$el.setAttribute("class", value);
}
// boolean props 처리
else if (attr === "checked" || attr === "selected") {
$el[attr] = value;
} else if (attr === "disabled") {
$el.disabled = value;
if (value) {
$el.setAttribute("disabled", "");
}
} else if (attr === "readOnly") {
$el.readOnly = value;
if (value) {
$el.setAttribute("readonly", "");
}
}
// 일반 속성
else {
$el.setAttribute(attr, value);
}
});
}
9 changes: 8 additions & 1 deletion src/lib/createVNode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export function createVNode(type, props, ...children) {
return {};
console.log(type, props, children);
return {
type,
props,
children: children
.flat(Infinity)
.filter((child) => child != null && child !== false && child !== true),
};
}
88 changes: 85 additions & 3 deletions src/lib/eventManager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,87 @@
export function setupEventListeners(root) {}
const eventListeners = new Map();
const rootEventHandlers = new Map(); // root에 등록된 이벤트 핸들러 추적

export function addEvent(element, eventType, handler) {}
// root요소에 이벤트 리스너를 등록
export function setupEventListeners(root) {
// 기존 이벤트 리스너 제거
rootEventHandlers.forEach((handler, eventType) => {
root.removeEventListener(eventType, handler);
});
rootEventHandlers.clear();

export function removeEvent(element, eventType, handler) {}
// DOM에 존재하지 않는 요소의 이벤트 리스너 정리
const elementsToRemove = [];
eventListeners.forEach((listeners, element) => {
// 요소가 DOM에 존재하지 않거나 root의 하위 요소가 아니면 제거
if (!root.contains(element) && element !== root) {
elementsToRemove.push(element);
}
});
elementsToRemove.forEach((element) => {
eventListeners.delete(element);
});

// eventType 가져오기, 중복된 이벤트 타입은 제거
const eventTypes = new Set();
eventListeners.forEach((listeners) => {
listeners.forEach(({ eventType }) => {
eventTypes.add(eventType);
});
});

// eventType 마다 이벤트 리스너 등록
eventTypes.forEach((eventType) => {
const handler = (event) => {
const $target = event.target;
eventListeners.forEach((listeners, element) => {
// 이벤트 위임: target이 element이거나 element의 하위 요소인지 확인
if (element.contains($target) || $target === element) {
listeners
.filter((listener) => listener.eventType === eventType)
.forEach((listener) => listener.handler(event));
}
});
};
root.addEventListener(eventType, handler);
rootEventHandlers.set(eventType, handler); // 핸들러 저장
});
}

export function addEvent(element, eventType, handler) {
// element가 dom 요소를 가져옴
// 전역변수를 만들어서 이벤트 타입을 저장 왜 배열이 아닌 맵으로? 배열은 순서가 있어서 중복된 이벤트 타입을 처리할 수 없음

// map에 element 가 있는지 확인
// 있으면 기존 배열에 추가
// 없으면 새 배열 만들고 추가
if (eventListeners.has(element)) {
eventListeners.get(element).push({
eventType: eventType,
handler: handler,
});
} else {
eventListeners.set(element, [
{
eventType,
handler,
},
]);
}
}

export function removeEvent(element, eventType, handler) {
// 등록 이벤트 핸들러가 있는지 확인
const listeners = eventListeners.get(element);
if (listeners) {
const filteredListeners = listeners.filter(
({ eventType: type, handler: fn }) =>
type !== eventType || fn !== handler,
);

if (filteredListeners.length === 0) {
eventListeners.delete(element);
} else {
eventListeners.set(element, filteredListeners);
}
}
}
Loading
Loading