[6팀 이지현] Chapter 1-2. 프레임워크 없이 SPA 만들기 (2) - #55
Conversation
|
고생하셨어용 다음 주차에도 아좌좌좌 |
There was a problem hiding this comment.
이벤트위임에서 사용되는 이벤트들을 저장해두는 저장소 계층에 대해서 궁금합니다
저는 요소를 보여주고 보여주지않는다는 기준으로 제일 상위계층을 element를 담았습니다.
그래서 element > event > handler 순으로 저장했었는데요.
이번 준일님 해설 발제를 들어보니 event type을 최상위 계층에 저장하셨어서 다른 분들은 어떤 계층으로 저장한 지 궁금합니다.
There was a problem hiding this comment.
저도 element > eventType > handler 순으로 관리하고 있어요. 어차피 다 객체 기반이라 두 방식의 큰 차이는 없는 것 같아서 그냥 제가 이해하기 편한대로 작성했어요.
There was a problem hiding this comment.
추측이긴한데 준일님께서는 이벤트 Map 객체에서 이벤트 정보를 모두 관리하고 싶어서 event type을 최상위 계층(key)로 저장하신 것 같아요.
event type을 key로 저장하면 delegatedEvents에 event type을 저장하지 않고, setupEventListeners 에서 이벤트 Map 객체를 순회하면서 event type을 가져올 수 있어서 아마 이렇게 구현하지 않았나 생각이 듭니다. (추측이라 아닐 수도 있습니다...)
// 준일님 코드
const eventMap = new Map<string, WeakMap<Element, Set<EventHandler>>>();
let rootElement: Element | null = null;
export function setupEventListeners(root: Element): void {
rootElement = root;
eventMap.forEach((handlers, eventType) => {
rootElement!.removeEventListener(eventType, handleEvent);
rootElement!.addEventListener(eventType, handleEvent);
});
}|
저는 eventType > element > handler 계층 구조로 사용했는데요! 일단, 이 구조를 사용한건 이벤트 위임의 진입점인 event.type 기준으로 삼는게 저한테는 자연스럽기도하고, 타입별로 한 번에 정리하고 관리하는게 쉽다고 생각했어요! |
|
저도 element > event > handler 순으로 구성을 했는데 라고 설명을 하는데 결론은 '' eventType > element > handler: 라고 공부했습니다 ㅎㅎ |
|
저도 element -> eventType -> handler로 구현을 했는데, |
| } | ||
|
|
||
| if (["checked", "disabled", "selected", "readOnly"].includes(key)) { | ||
| const attrName = key === "readOnly" ? "readonly" : key; |
There was a problem hiding this comment.
어차피 소문자만 가능하다면 요건 어떨까요
| const attrName = key === "readOnly" ? "readonly" : key; | |
| const attrName = key.toLowerCase(); |
| handlers.forEach((handler) => { | ||
| handler(event); | ||
| }); | ||
| break; |
There was a problem hiding this comment.
handler를 실행하고 break를 하게 되면 이벤트버블링이 중간에 멈출 것으로 예상됩니다.
break를 사용하지 않고 이벤트핸들러에서 event.stopPropagation()이 호출되면 이벤트 전파를 멈추도록 구현하는 것도 좋을 것 같아요!
There was a problem hiding this comment.
추측이긴한데 준일님께서는 이벤트 Map 객체에서 이벤트 정보를 모두 관리하고 싶어서 event type을 최상위 계층(key)로 저장하신 것 같아요.
event type을 key로 저장하면 delegatedEvents에 event type을 저장하지 않고, setupEventListeners 에서 이벤트 Map 객체를 순회하면서 event type을 가져올 수 있어서 아마 이렇게 구현하지 않았나 생각이 듭니다. (추측이라 아닐 수도 있습니다...)
// 준일님 코드
const eventMap = new Map<string, WeakMap<Element, Set<EventHandler>>>();
let rootElement: Element | null = null;
export function setupEventListeners(root: Element): void {
rootElement = root;
eventMap.forEach((handlers, eventType) => {
rootElement!.removeEventListener(eventType, handleEvent);
rootElement!.addEventListener(eventType, handleEvent);
});
}| import { addEvent } from "./eventManager"; | ||
|
|
||
| export function createElement(vNode) {} | ||
| export function createElement(vNode) { |
There was a problem hiding this comment.
createFragment, createDOMElement 등 함수를 역할별로 잘 분리하신것 같아요!
| delegatedEvents.forEach((eventType) => { | ||
| root.addEventListener(eventType, (event) => { | ||
| let target = event.target; | ||
| while (target && target !== root) { |
There was a problem hiding this comment.
while 조건문에서 target과 root가 같을때 순회를 멈추게 하는 이유가 무엇인지 궁금합니다.
과제 체크포인트
배포 링크
https://j2h30728.github.io/front_6th_chapter1-2/
기본과제
가상돔을 기반으로 렌더링하기
이벤트 위임
심화 과제
Diff 알고리즘 구현
과제 셀프회고
테스트코드를 따라서 가상돔과 Diff 알고리즘을 구현해보면서 항상 사용하는 리액트 동작에 대해서 좀 더 알게되었습니다.
과제 진행 중 가장 큰 실수는 vNode 객체에 실제 DOM 요소를 저장한 것이었습니다.
커밋
99089b1에서 확인할 수 있듯이vNode.el = element처럼 실제 DOM을 저장했고, 아이러니하게도 이 잘못된 방식이 겉보기에는 정상 작동했습니다.실제 DOM요소가 oldNode.el에 저장되어 있어서, 이를 parentNode로 사용하고 diff 알고리즘을 실행했습니다. 결과적으로 화면은 정상적으로 렌더링되지만, 가상돔의 본질을 놓치고 있었습니다.
과제를 진행하면서 "왜 내가 리얼돔을 객체에 넣었을까?"라는 의문이 생기게 되었습니다. createVNode가 반환하는 { type, props, children } 구조의 순수한 객체, 실제 DOM과의 분리 등에 대해서 다시 한 번 더 되짚어 가며 리팩토링을 진행했습니다.
회고를 작성하면서 돌이켜보니, 당시에는 "작동하는 코드"에만 집중하여 설계 원칙을 깊이 고려하지 못했던 것 같습니다.
이를 통해 표면적인 구현을 넘어 근본적인 이해의 중요성을 깨달았습니다.
기술적 성장
children과 childNodes의 차이
DOM API의 children과 childNodes의 미묘한 차이점을 과제를 구현하면서 경험했습니다.
처음에는 children가져와 사용했지만 텍스트 노드가 존재하지 않기떄문에 diff 알고리즘이 실행되면서 인덱스가 일치하지 않아서 인한 버그를 유발시켰습니다.
실제 화면에서도 제대로 나오지않을 뿐만아니라, 테스트 코드가 실패하는 것을 시작으로 디버깅을 진행했습니다.
API를 제대로 찾아보지 않고 사용했던 점과 그로인해 DOM API 차이점을 알게 되었고, 테스트 코드의 중요성도 덤으로 알게 되었습니다.
이벤트 위임(Event Delegation) 패턴의 실제 구현
학습자료에서 제공한 내용을 토대로, 루트 요소에서 이벤트를 캐치하고 event.target부터 상위로 탐색하며 적절한 핸들러를 찾는 패턴을 구현했습니다. 동적으로 생성되는 DOM 요소들에도 일관된 이벤트 처리가 가능한 구조의 핵심을 이해했습니다.
코드 품질
특히 만족스러운 구현은 WeakMap으로 메모리 누수 방지, Map으로 이벤트 타입 분류, Set으로 핸들러 중복 방지를 했던 부분입니다. 각 계층이 명확한 역할을 담당하여 동작하도록 구현했습니다.
리팩토링이 필요한 부분
기능 구현에 집중했기 때문에 클린 코드에 대해서 깊게 고려하지 못했습니다. 하나의 함수에 여러 역할을 하기도 합니다.
이런 함수들에 대해서 관심사 분리를 진행하고 단일책임원칙을 지키고 싶습니다.
코드 설계 관련 고민과 결정
처음에는 단순히 이벤트 매니저내의 이벤트 타입을 미리 정의한 상수 배열로 관리했습니다.
하지만 이 방식은 새로운 이벤트를 추가할 때마다 코드를 수정해야 하고, 사용하지 않는 이벤트에도 리스너를 등록하는 비효율성이 있었습니다.
이후
Set을 활용한 동적 관리 방식으로 변경했습니다.실제로
addEvent가 호출될 때만 해당 이벤트 타입을delegatedEvents에 추가하여, 필요한 이벤트에만 root 리스너를 등록하겠끔 했습니다.학습 효과 분석
가장 큰 배움이 있었던 부분
removeEvent함수를 처음 구현할 때는 handler 파라미터의 진짜 용도를 깊게 생각하지 못했습니다.단순히 이벤트 타입만 제거하면 된다고 생각했고, 특정 핸들러만 정밀하게 제거해야 하는 상황을 고려하지 못했습니다.
과제의 모든 테스트 코드를 통과시킨 후, 스터디원 영서님의 글을 읽으면서 handler 파라미터가 제공된 진짜 이유를 깨닫게 되었습니다.
같은 요소에 여러 컴포넌트가 동일한 이벤트 타입의 핸들러를 등록했을 때, 특정 컴포넌트가 언마운트되면 해당 핸들러만 정확히 제거해야 메모리 누수를 방지할 수 있다는 것이었습니다.
이를 바탕으로
removeEvent함수를 리팩토링했습니다.handler 참조를 정확히 끊어내고, 빈 Set/Map을 정리하여 메모리 누수를 방지하는 방식으로 개선했습니다.
이런 일련의 경험을 통해 지식공유의 힘을 더 깊이 깨달았습니다. 나 또한 누군가에게 도움이 되는 글을 써보고 싶다는 동기가 생겼습니다.
실무 연결
과제 피드백
리뷰 받고 싶은 내용
이벤트위임에서 사용되는 이벤트들을 저장해두는 저장소의 계층에 대해서 궁금합니다
요소를 보여주고 보여주지않는다는 기준으로 제일 상위계층을 element를 담았습니다.
그래서 element > event > handler 순으로 저장했었는데요. 다른 분들의 코드를 확인하니 각각 다르게 만들수가 있더라구요. 제가 접근을 좀 다르게 했던 것일까요? 멘토님이 생각하시는 최적의 계층설계가 궁금합니다.
현재 구현에는 에러 처리가 전혀 존재하지않는데, vNode 생성 실패나 DOM 조작 에러 시 핸들링하는 함수는 어떤방향으로 구현이 될까요?