항해 플러스 프론트엔드 6기 2주차, Chapter 1-2. 프레임워크 없이 SPA 만들기 (2)

1주차에서는 프레임워크 없이 SPA를 맨땅에서부터 만들어봤습니다. 그럼 이번 주차 과제는 뭘까요? 간단하게 요약하자면 Virtual DOM과 이벤트 위임 시스템을 직접 구현하고, diff 알고리즘을 통한 렌더링 최적화를 하는 건데요. 평소 단어만 알았던 Virtual...


항해 플러스 프론트엔드 6기 2주차, Chapter 1-2. 프레임워크 없이 SPA 만들기 (2)

이전 블로그 링크: https://velog.io/@chan9yu/hanghae-plus-wil2

(완전 나라고 저거...)

2주차 시작하기

1주차 과제를 무사히 마치고 어느새 2주차 과제까지 마무리를 했습니다!

왜 항상 무언가를 할 때 회사가 바쁠까요? 2주차 월요일부터 외근에 야근까지... (결국 월요일은 불참을 해버린) 정말 힘들었지만 ㅠㅠ 열심히 하는 팀원들을 보면서 겨우겨우 진행할 수 있었습니다.

어쩌다 보니 이번 주차에 우리 팀 교수님을 맡고 계신 지훈님이 서로 과제한 내용을 정리하고 공유해 보자라고 제안해주셔서 내용도 정리하기 시작했습니다.

누군가의 한마디 덕분에 이렇게 되네요~~

매번 정리하고 서로 공유해준 5팀 칭찬해 👍👍

2주차 과제

1주차에서는 프레임워크 없이 SPA를 맨땅에서부터 만들어봤습니다. 그럼 이번 주차 과제는 뭘까요?

간단하게 요약하자면 Virtual DOM이벤트 위임 시스템을 직접 구현하고, diff 알고리즘을 통한 렌더링 최적화를 하는 과제였습니다.

평소 단어만 알았던 Virtual DOM에 대해 자세하게 공부할 수 있었던 기회가 되었던 것 같습니다!

어떻게 구현했을까?

이번 주차에서는 크게 3가지 핵심 기능을 구현해야 했습니다.

createVNode - JSX를 가상 노드로

JSX 문법을 Virtual DOM 객체로 변환하는 함수입니다. Vite 설정을 통해 JSX가 createVNode 함수 호출로 변환되도록 했어요.

export function createVNode(type, props = null, ...children) {  
	const normalizedChildren = children  
		.flat(Infinity)  
		.filter((child) => child !== null && child !== undefined && child !== false);

	return {  
		type,  
		props,  
		children: normalizedChildren  
	};  
}  

여기서 중요한 건 flat(Infinity)를 통한 평탄화 작업인데요. JSX에서 배열이나 조건부 렌더링을 사용할 때 중첩된 배열 구조가 생성되기 때문에 이를 평탄화해주는 작업이 필요했습니다.

normalizeVNode - 일관된 형태로 정규화

다양한 타입의 값들(문자열, 숫자, null, undefined, boolean, 배열, 함수형 컴포넌트 등)을 일관된 VNode 트리로 변환하는 함수입니다.

export function normalizeVNode(vNode) {  
	if (vNode === null || vNode === undefined || typeof vNode === "boolean") {  
		return "";  
	}

	if (typeof vNode === "string" || typeof vNode === "number") {  
		return String(vNode);  
	}

	// ... 나머지 로직  
}  

이 과정을 통해 불필요한 값들은 제거되고, 모든 children이 일관된 형태로 정리됩니다.

createElement - 실제 DOM으로 변환

정규화된 VNode 트리를 실제 DOM 엘리먼트로 변환하는 함수입니다.

export function createElement(vNode) {  
	// 타입별 분기 처리  
	if (typeof vNode === "string" || typeof vNode === "number") {  
		return document.createTextNode(String(vNode));  
	}

	if (Array.isArray(vNode)) {  
		const fragment = document.createDocumentFragment();  
		vNode.forEach((child) => fragment.appendChild(createElement(child)));  
		return fragment;  
	}

	// VNode 객체 처리  
	const element = document.createElement(vNode.type);  
	updateAttributes(element, vNode.props);

	const children = vNode.children ?? [];  
	children.forEach((child) => element.appendChild(createElement(child)));

	return element;  
}  

여기서 updateAttributes 함수는 props를 실제 DOM 속성으로 변환하는 역할을 하는데, className, 이벤트 핸들러, Boolean 속성, style 등 다양한 케이스를 처리합니다.

이벤트 위임 시스템 구현

이번 과제에서 특히 고민을 많이 했던 부분입니다. 동적으로 추가/제거되는 엘리먼트에서도 이벤트가 일관되게 동작하도록 만드는 것이 핵심이었어요.

export function setupEventListeners(rootElement) {  
	eventMap.forEach((handlers, eventType) => {  
		rootElement.addEventListener(eventType, (event) => {  
			let currentTarget = event.target;

			// 이벤트가 발생한 요소부터 루트까지 버블링하면서 핸들러 찾기  
			while (currentTarget && currentTarget !== rootElement) {  
				const elementHandlers = handlers.get(currentTarget);  
				if (elementHandlers) elementHandlers.forEach((handler) => handler(event));

				// 부모 요소로 이동하여 버블링 효과 구현  
				currentTarget = currentTarget.parentElement;  
			}  
		});  
	});  
}  

루트 요소에 이벤트를 한 번만 등록하고, 버블링을 통해 실제 이벤트가 발생한 요소를 찾아 핸들러를 실행하는 방식으로 구현했습니다.

Diff 알고리즘 구현

심화 과제로 diff 알고리즘을 구현해서 변경된 부분만 업데이트하도록 만들었어요. 이 부분이 가장 복잡했는데, 새로운 요소 추가, 불필요한 요소 제거, 속성 변경 등 다양한 케이스를 처리해야 했습니다.

다만 조건문이 꽤 복잡해졌는데...

if (  
	typeof newNode !== typeof oldNode ||  
	(typeof newNode === "string" && typeof oldNode !== "string") ||  
	(typeof newNode !== "string" && typeof oldNode === "string") ||  
	(isVNode(newNode) && isVNode(oldNode) && (newNode.type !== oldNode.type || newNode.props?.key !== oldNode.props?.key))  
) {  
	// ... 나머지 로직  
}  

이런 식으로 조건문이 복잡해지는 경우가 있어서 리팩토링이 필요할 것 같다는 생각이 들었습니다.

그래서 결과는..?

이번에는 기본 과제와 심화 과제까지 모두 통과할 수 있었습니다!

  • 기본과제: Virtual DOM 기반 렌더링 구현 완료
  • 심화과제: 이벤트 위임 시스템과 Diff 알고리즘을 통한 최적화 완료

배포 링크

2주차 KPT 회고

Keep

팀 스터디

이번 주차에서 가장 좋았던 건 팀원들과 함께 정리하고 공유한 점입니다. 지훈님의 제안으로 시작된 스터디 덕분에 혼자서는 놓칠 수 있었던 부분들을 다시 한번 정리할 수 있었습니다.

깊이 있는 학습 경험

Virtual DOM이 무엇인지, 왜 쓰는지에 대해 진짜 깊이 고민할 수 있었던 시간이였습니다. 단순히 React를 사용하는 것이 아니라 "왜 이런 구조가 필요한지", "어떻게 동작하는지"를 직접 구현하면서 이해할 수 있었습니다.

문서화 습관 형성

과제를 진행하면서 GitHub Issues로 내용을 정리해둔 게 정말 도움이 되었습니다. 나중에 다시 보거나 다른 사람에게 설명할 때 훨씬 수월했어요.

정리한 내용들

Problem

복잡한 조건문 처리

diff 알고리즘을 구현하면서 조건문이 너무 복잡해진 부분이 있습니다. 가독성 면에서 개선이 필요할 것 같습니다.

함수 크기 문제

updateElement 함수가 너무 많은 역할을 하고 있다는 생각이 들어요. 함수 분리를 시도해봤지만 오히려 가독성이 떨어지는 것 같아서 고민이 됩니다.

시간 부족의 아쉬움

여전히 회사 업무와 병행하다 보니 더 깊이 있게 공부하지 못한 아쉬움이 있습니다... key 기반 리스트 diff, memoization, 비동기 렌더링 같은 고급 최적화 기법들은 더 공부해보고 싶습니다.

Try

클린 코드 적용

다음 주차에서는 처음부터 함수를 작게 만들고, 의도가 명확한 이름을 사용하려고 합니다. 특히 복잡한 조건문을 별도 함수로 분리해서 가독성을 높여보고 싶습니다.

React Deep-dive

React의 Fiber 아키텍처나 Concurrent 모드 같은 고급 주제들도 틈틈이 공부해보려고 합니다. 이번 과제를 통해 기초를 다졌으니 좀 더 심화된 내용도 이해할 수 있을 것 같아요.

마무리

1주차보다는 비교적 순탄했던 2주차였지만, 오히려 더 많은 걸 얻어간 과제였던 것 같아요.

Virtual DOM의 동작 원리를 직접 구현해보면서 React의 철학과 고민을 조금이나마 이해할 수 있었습니다. 그리고 무엇보다 5팀 분들이 서로 도와주고 열심히 하는 모습을 보면서 덩달아 저까지 자극받고 열심히 하게 되었어요.

3주차도 화이팅! 💪


(코어타임 지키겠습니다 교수님 ㅠㅠ 😭)

댓글