Skip to content

Commit

Permalink
rednerElement테스트 완료
Browse files Browse the repository at this point in the history
  • Loading branch information
Azamwa committed Oct 3, 2024
1 parent 3afaf73 commit 4573531
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 46 deletions.
16 changes: 13 additions & 3 deletions src/lib/createElement__v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,22 @@ export function createElement__v2(vNode) {

for (let name in vNode.props) {
let propsName = name;
if (events.includes(name) && typeof vNode.props[name] === 'function') {
let propsValue = vNode.props[name];
if (events.includes(name) && typeof propsValue === 'function') {
const eventName = name.slice(2).toLocaleLowerCase();
const eventFn = vNode.props[name];
const eventFn = propsValue;
$el.addEventListener(eventName, eventFn);
continue;
}
if (name === 'className') {
propsName = 'class';
} else if (name === 'style' && typeof propsValue === 'object') {
const styleProps = Object.entries(propsValue)
.map(([key, value]) => `${camelToKebab(key)}: ${value}`)
.join(';');
propsValue = styleProps;
}
$el.setAttribute(propsName, vNode.props[name]);
$el.setAttribute(propsName, propsValue);
}

vNode.children.forEach(child => {
Expand All @@ -45,3 +51,7 @@ export function createElement__v2(vNode) {

return $el;
}

function camelToKebab(str) {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
}
103 changes: 61 additions & 42 deletions src/lib/renderElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@
import { addEvent, removeEvent, setupEventListeners } from './eventManager';
import { createElement__v2 } from './createElement__v2.js';

// TODO: processVNode 함수 구현
function processVNode(vNode) {
// vNode를 처리하여 렌더링 가능한 형태로 변환합니다.
// - null, undefined, boolean 값 처리
// - 문자열과 숫자를 문자열로 변환
// - 함수형 컴포넌트 처리 <---- 이게 제일 중요합니다.
// - 자식 요소들에 대해 재귀적으로 processVNode 호출
if ([null, undefined].includes(vNode) || typeof vNode === 'boolean') {
return '';
}
Expand All @@ -26,56 +20,81 @@ function processVNode(vNode) {
}

function updateAttributes(vNode, newProps, oldProps) {
// DOM 요소의 속성을 업데이트합니다.
// - 이전 props에서 제거된 속성 처리
// - 새로운 props의 속성 추가 또는 업데이트
// - 이벤트 리스너, className, style 등 특별한 경우 처리
// <이벤트 리스너 처리>
// - TODO: 'on'으로 시작하는 속성을 이벤트 리스너로 처리
// - 주의: 직접 addEventListener를 사용하지 않고, eventManager의 addEvent와 removeEvent 함수를 사용하세요.
// - 이는 이벤트 위임을 통해 효율적으로 이벤트를 관리하기 위함입니다.
for (let name in oldProps) {
if (!Object.hasOwn(newProps, name)) {
vNode.removeAttribute(name);
}
if (!Object.hasOwn(newProps, 'class')) {
vNode.removeAttribute('class');
}
}
for (let name in newProps) {
if (!Object.hasOwn(oldProps, name) || oldProps[name] !== newProps[name]) {
if (name === 'className') {
vNode.setAttribute('class', newProps[name]);
} else if (name === 'style') {
const styleValue = Object.entries(newProps[name])
.map(([key, value]) => `${camelToKebab(key)}:${value}`)
.join(';');
vNode.setAttribute(name, styleValue);
} else {
vNode.setAttribute(name, newProps[name]);
}
}
}
}

function updateElement(parent, newNode, oldNode) {
// 1. 노드 제거 (newNode가 없고 oldNode가 있는 경우)
// TODO: oldNode만 존재하는 경우, 해당 노드를 DOM에서 제거
// 2. 새 노드 추가 (newNode가 있고 oldNode가 없는 경우)
// TODO: newNode만 존재하는 경우, 새 노드를 생성하여 DOM에 추가
// 3. 텍스트 노드 업데이트
// TODO: newNode와 oldNode가 둘 다 문자열 또는 숫자인 경우
// TODO: 내용이 다르면 텍스트 노드 업데이트
// 4. 노드 교체 (newNode와 oldNode의 타입이 다른 경우)
// TODO: 타입이 다른 경우, 이전 노드를 제거하고 새 노드로 교체
// console.log(oldNode);
// console.log('--------------');
// console.log(newNode);
// 5. 같은 타입의 노드 업데이트
// 5-1. 속성 업데이트
// TODO: updateAttributes 함수를 호출하여 속성 업데이트
// 5-2. 자식 노드 재귀적 업데이트
// TODO: newNode와 oldNode의 자식 노드들을 비교하며 재귀적으로 updateElement 호출
// HINT: 최대 자식 수를 기준으로 루프를 돌며 업데이트
// 5-3. 불필요한 자식 노드 제거
// TODO: oldNode의 자식 수가 더 많은 경우, 남은 자식 노드들을 제거
function updateElement(parent, newNode, oldNode, key) {
if (!newNode && oldNode) {
parent.removeChild(parent.childNodes[key]);
return;
}

if (newNode && !oldNode) {
parent.appendChild(createElement__v2(newNode));
return;
}

if (typeof newNode === 'string' && typeof oldNode === 'string' && newNode !== oldNode) {
parent.innerHTML = newNode;
return;
}

if (newNode.type !== oldNode.type) {
parent.replaceChild(createElement__v2(newNode), parent.childNodes[key]);
return;
}

if (JSON.stringify(newNode.props) !== JSON.stringify(oldNode.props)) {
updateAttributes(parent.childNodes[key], newNode.props, oldNode.props);
}

if (newNode.children && oldNode.children) {
const maxLength = Math.max(newNode.children.length, oldNode.children.length);
for (let i = 0; i < maxLength; i++) {
updateElement(parent.childNodes[key], newNode.children[i], oldNode.children[i], i);
}
}
}

let vNodeMap = new Map();

export function renderElement(vNode, $root) {
// 최상위 수준의 렌더링 함수입니다.
// - processVNode를 실행해서 함수 컴포넌트로 정의된 vNode를 전부 해체
// - 이전 vNode와 새로운 vNode를 비교하여 업데이트
// - 최초 렌더링 시에는 createElement__v2로 실행
// - 리렌더링일 때에는 updateElement로 실행
// 이벤트 위임 설정
// TODO: 렌더링이 완료된 후 setupEventListeners 함수를 호출하세요.
// 이는 루트 컨테이너에 이벤트 위임을 설정하여 모든 하위 요소의 이벤트를 효율적으로 관리합니다.
vNodeMap.set('newNode', processVNode(vNode));

if (!vNodeMap.has('oldNode')) {
if ($root.childNodes.length === 0) {
vNodeMap.set('oldNode', processVNode(vNode));
$root.appendChild(createElement__v2(vNodeMap.get('oldNode')));
$root.appendChild(createElement__v2(vNodeMap.get('newNode')));
return;
}
updateElement($root, vNodeMap.get('newNode'), vNodeMap.get('oldNode'));
updateElement($root, vNodeMap.get('newNode'), vNodeMap.get('oldNode'), 0);
vNodeMap.set('oldNode', vNodeMap.get('newNode'));
}

function camelToKebab(str) {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
}
1 change: 0 additions & 1 deletion src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ function render() {

try {
renderElement(<App targetPage={router.getTarget()} />, $root);
renderElement(<App targetPage={router.getTarget()} />, $root);
} catch (error) {
if (error instanceof ForbiddenError) {
router.push('/');
Expand Down

0 comments on commit 4573531

Please sign in to comment.