효뚜르팝의 Dev log
[항해플러스] 2주차 회고 - 프레임워크 없이 SPA 만들기 본문
✍️ 1. 문제 (과제, 프로젝트를 진행하면서 부딪혔던 기술적인 문제)
처음으로 React 없이 가상 DOM부터 diff 알고리즘, 이벤트 위임 시스템까지 직접 구현해보는 과제를 하며 전체 렌더링 흐름에 대한 감이 잡히지 않아 막막했다.
특히 createVNode → normalizeVNode → createElement → updateElement까지의 흐름이 머릿속에 명확히 정리되지 않아서 함수 하나하나를 어디에서 왜 호출하는지를 파악하는 데 시간이 오래 걸렸다.
또한 이벤트 시스템을 만들면서 setupEventListeners의 등록 방식과 addEvent, removeEvent의 연결 구조, 그리고 root 기준 이벤트 위임 시 중복 등록 방지를 어떻게 처리할지도 고민이 많았다.
✍️ 2. 시도
- 흐름을 따라가며 디버깅을 반복했고, 각 함수별 역할을 주석과 함께 정리하며 "왜 이 로직이 필요한가?"를 계속 스스로에게 질문하며 파고들었다.
- updateAttributes를 createElement와 updateElement 양쪽에서 공용으로 사용해도 되는지 고민하며, 함수의 책임과 역할을 직접 실험해봤다.
- 이벤트 위임 시 루트 기준으로 등록하는 구조에서 중복 이벤트 등록을 방지하기 위해 WeakMap, WeakSet을 사용해보고, 이벤트가 잘 위임되지 않을 때는 composedPath()를 활용해 직접 DOM 경로를 따라 탐색하는 방식으로 확장했다.
- 상태 관리도 단순히 컴포넌트에서 처리할지, globalStore에 넣는 게 맞는지를 고민했고, 최종적으로는 액션들을 store에 일관되게 모아 관리하는 방식으로 정리했다.
✍️ 3. 해결
- 렌더링 전체 흐름을 명확히 이해하게 된 시점부터는, 각각의 함수들이 왜 필요하고 어떻게 연결되는지에 대한 감이 생겼다.
- 이벤트 위임 시스템도 root → 이벤트 타입 → 엘리먼트 → 핸들러 구조로 재정비하고, composedPath를 통해 bubbling 구조까지 구현했다.
const registeredRoots = new WeakSet();
const eventsMap = new Map();
const rootEventMap = new WeakMap(); // root → Set<eventType>
export function setupEventListeners(root, eventType) {
if (!registeredRoots.has(root)) {
registeredRoots.add(root);
rootEventMap.set(root, new Set());
}
const registeredEvents = rootEventMap.get(root);
if (registeredEvents.has(eventType)) return;
registeredEvents.add(eventType);
root.addEventListener(eventType, (e) => {
const path = e.composedPath?.() || getEventPath(e.target, root);
for (const el of path) {
if (el === root) break;
const elementMap = eventsMap.get(eventType);
const handlers = elementMap?.get(el);
if (handlers) {
handlers.forEach((handler) => handler(e));
break; // 가장 가까운 요소의 핸들러만 실행
}
}
});
}
- 상태 변경도 store 중심으로 관리하면서 컴포넌트에서는 최대한 화면 표현에 집중하게 분리함으로써 흐름이 더 깔끔해졌다.
- 근데 지금 생각해보니 전역으로 관리되는 상태가 아닌 해당 컴포넌트에서만 사용되는 상태라면 그냥 store.setState로 사용하는 게 나을 거 같다는 생각이 든다.
✍️ 4. 알게된 것
- 우리가 일상적으로 사용하는 React나 Vue의 내부에서 어떤 일들이 일어나는지를 몸소 체험해보니, 프레임워크가 제공하는 추상화가 얼마나 고마운지 깨달았다.
- 이벤트 위임은 단순히 루트에 핸들러를 다는 것이 아니라, composedPath()나 parent traversal 같은 디테일까지 고려해야 온전히 위임 구조가 완성된다는 걸 알게 됐다.
- 상태 변경(setState) 후의 렌더링 처리는 단순히 DOM을 다시 그리는 게 아니라, 얼마나 불필요한 재렌더링을 줄이느냐가 핵심이라는 것도 체감했다.
✅ Keep : 현재 만족하고 계속 유지할 부분
- 막혔을 때 스스로 이유를 찾아보려는 자세
- 구조적인 설계를 고민하며, 단순히 동작하는 코드를 넘어 유지보수성과 확장성까지 고려해보려 한 점
- 흐름을 이해하고자 주석을 꼼꼼히 작성하고, 함수의 역할을 직접 손으로 그려가며 정리했던 습관
❗️Problem : 개선이 필요하다고 생각하는 문제점
P1.
처음에 전체 구조를 잡는 데 시간이 너무 오래 걸렸고, 진입 장벽이 높게 느껴졌다.
"무작정 따라 쓰기"보다는 구조도를 먼저 그리고 큰 흐름을 파악하는 방식으로 접근했다면 훨씬 수월했을 것 같다.
P2.
updateAttributes 함수를 createElement와 updateElement 양쪽에서 공용으로 사용하는 방식은
처음에는 효율적으로 느껴졌지만, 시간이 지날수록 함수의 역할이 모호해지고 책임이 섞여 있다는 느낌이 들었다.
속성 초기화와 갱신은 다르게 다뤄야 한다는 점에서, updateAttributes는 updateElement 내부 전용으로 쓰고,
createElement에선 전용 함수 (applyInitialAttributes 등)로 분리하는 게 더 명확한 구조라고 판단했다.
🔧 Try : 문제점을 해결하기 위해 시도해야 할 것
P1.
다음 과제에서는 먼저 전체 흐름과 데이터/이벤트 흐름도를 시각화해서 정리한 뒤, 구조를 잡고 기능 구현에 들어가는 방식을 시도해보고자 한다.
또한 내가 짠 구조를 다른 사람에게 설명해보는 연습을 통해 더 깊이 내 것으로 만들어보고 싶다.
P2.
속성 처리 함수들의 책임을 명확히 나누기 위해 updateAttributes를 updateElement 전용으로 제한하고,
createElement에는 별도의 속성 초기화 함수(applyAttributes 또는 initProps 등)를 도입해서
구조의 명확성과 유지보수성을 높여보려고 한다.
아마 이런 식으로 수정해볼 수 있지 않을까,,,?
🔸 Before: updateAttributes를 공용으로 사용하던 구조
// createElement 안
const el = document.createElement(vNode.type);
updateAttributes(el, vNode.props); // 여기서도 사용
// updateElement 안
updateAttributes(
parentElement.childNodes[index],
newNode.props,
oldNode.props
); // 여기서도 사용
updateAttributes가 초기 설정(create)과 변경(update) 둘 다 맡으면서 책임이 모호해지고,
이벤트 제거 등은 초기 생성 시 불필요한 로직까지 포함되는 문제가 있었음
🔸 After: 역할을 나눠 명확하게 리팩터링한 구조
// props 초기 설정용
function applyInitialAttributes(el, props = {}) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith("on")) {
const eventType = extractEventTypeFromKey(key);
addEvent(el, eventType, value);
} else if (key === "className") {
el.setAttribute("class", value);
} else {
el.setAttribute(key, value);
}
});
}
// props 업데이트 전용
function updateAttributes(el, newProps = {}, oldProps = {}) {
// 속성 제거
Object.keys(oldProps).forEach((key) => {
if (!(key in newProps)) {
if (key.startsWith("on")) {
const eventType = extractEventTypeFromKey(key);
removeEvent(el, eventType, oldProps[key]);
} else {
el.removeAttribute(key);
}
}
});
// 속성 추가/변경
Object.entries(newProps).forEach(([key, value]) => {
if (key === "className") {
el.setAttribute("class", value);
} else if (key.startsWith("on")) {
const eventType = extractEventTypeFromKey(key);
if (oldProps[key] !== value) {
addEvent(el, eventType, value);
}
} else {
if (oldProps[key] !== value) {
el.setAttribute(key, value);
}
}
});
}
createElement()에선 applyInitialAttributes()만 사용
updateElement()에선 updateAttributes()만 사용
→ 두 함수의 책임이 분리돼 유지보수성과 추론 가능성이 올라감!
✍️ 한 줄 회고
프레임워크의 추상화 뒤에 숨겨진 원리를 직접 구현하며, 개념을 ‘이해’에서 ‘체화’로 끌어올린 한 주였다.
'회고록' 카테고리의 다른 글
| [항해플러스] 5주차 회고 - 디자인 패턴과 함수형 프로그래밍 (0) | 2025.04.28 |
|---|---|
| [항해플러스] 3주차 회고 - React, Beyond the Basics (0) | 2025.04.13 |
| [항해 플러스] 1주차 회고 - 바닐라 JS로 SPA 구조 직접 구현해보기 (0) | 2025.03.28 |
| 2024 회고 (3) | 2025.01.05 |
| 사이드프로젝트, Quokka Letter 회고 (5) | 2024.02.27 |