https://solbijae.github.io/front_5th_chapter1-2/index.hash.html#/
- createVNode 함수를 이용하여 vNode를 만든다.
- normalizeVNode 함수를 이용하여 vNode를 정규화한다.
- createElement 함수를 이용하여 vNode를 실제 DOM으로 만든다.
- 결과적으로, JSX를 실제 DOM으로 변환할 수 있도록 만들었다.
- 노드를 생성할 때 이벤트를 직접 등록하는게 아니라 이벤트 위임 방식으로 등록해야 한다
- 동적으로 추가된 요소에도 이벤트가 정상적으로 작동해야 한다
- 이벤트 핸들러가 제거되면 더 이상 호출되지 않아야 한다
- 초기 렌더링이 올바르게 수행되어야 한다
- diff 알고리즘을 통해 변경된 부분만 업데이트해야 한다
- 새로운 요소를 추가하고 불필요한 요소를 제거해야 한다
- 요소의 속성만 변경되었을 때 요소를 재사용해야 한다
- 요소의 타입이 변경되었을 때 새로운 요소를 생성해야 한다
- 비사용자는 포스트 작성 폼이 보이지 않는다
- 비사용자는 포스트에 좋아요를 클릭할 경우, 경고 메세지가 발생한다.
- 사용자는 포스트 작성 폼이 보인다.
- 사용자는 포스트를 추가할 수 있다.
- 사용자는 포스트에 좋아요를 클릭할 경우, 좋아요가 토글된다.
❗[테스트 코드 오류잡기] '"/" 경로로 접근하면 홈 페이지가 렌더링된다’
appendChildren을 사용했었지만, replaceChildren로 변경하였습니다.
-
기존 노드를 효율적으로 제거하고 새로 렌더링 replaceChildren을 사용하면 container 내부의 기존 모든 자식을 제거한 후 새로운 요소를 삽입할 수 있습니다. 이를 통해 불필요한 appendChild 호출을 방지하고, 기존 노드를 하나씩 삭제하는 로직 없이 한 번의 호출로 변경할 수 있어 코드가 간결해집니다.
-
메모리 관리 및 성능 최적화 기존 DOM 노드를 유지하면서 appendChild를 사용할 경우, 필요 없는 노드가 남아 있을 가능성이 있으며, 이를 수동으로 정리해야 하는 추가적인 코드가 필요할 수 있습니다. 반면, replaceChildren은 자동으로 기존 내용을 정리하므로 메모리 누수를 방지하고 불필요한 DOM 조작을 줄이는 효과가 있습니다.
-
불필요한 이벤트 리스너 제거 이벤트 리스너가 등록된 요소를 직접 변경하는 경우, 기존 요소에 남아 있는 이벤트 리스너가 예기치 않은 동작을 일으킬 수 있습니다. replaceChildren을 사용하면 기존 요소가 완전히 제거되므로, 불필요한 이벤트 리스너가 남아 있지 않도록 할 수 있습니다.
-
일관된 상태 유지 UI를 갱신할 때, 기존의 DOM을 조작하는 방식보다 전체 컨테이너 내용을 교체하는 것이 명확한 상태 관리를 가능하게 합니다. 이는 가상 DOM 기반 렌더링을 구현할 때 불필요한 비교 연산을 줄이는 데도 도움이 됩니다.
✅ 결론 replaceChildren을 사용하면 기존 요소를 깔끔하게 제거하고 새로운 요소로 교체할 수 있어 코드를 간결하게 유지하면서도 성능과 유지보수성을 향상시킬 수 있습니다. 특히 초기 렌더링 시 불필요한 DOM 조작을 줄이고, 이벤트 리스너 관리 측면에서도 유리합니다.
❗[테스트 코드 오류잡기] null, undefined, boolean 값은 빈 문자열로 변환되어야 한다
처음에는 boolean 타입을 왜 걸러야하는지, 어떤 경우가 boolean 타입이 되는지 이해하지 못했습니다. boolean 타입이 될 수 있는 경우는 아래와 같다는걸 알게되었습니다.
❗[테스트 코드 오류 잡기] Virtual DOM과 이벤트 관리
- 처음에는 변경되는 vNode에 대해서만 가상돔을 신경 썼는데, 오류 내용을 보니 updateElement에 previousVNode를
<div class="bg-gray-100 min-h-screen flex justify-center">...</div>
이렇게 리얼돔 형태로 보내주고 있었습니다. - 생각해보니 비교를 하려면 previousVNode도 가상돔의 형태여야 하기 때문에 렌더링이 종료된 후
previousVNode = normalizedVNode;
를 통해 previousVNode를 정규화된 노드로 업데이트할 수 있도록 하였습니다.
- 이번 과제는 어떻게 만들어야겠다는게 머릿속에 잘 그려지지 않아서 사전 학습자료와 AI툴을 이용해서 최대한 빠르게 개발 → 나만의 학습 가이드를 만들겠다는 계획을 가지고 임했습니다.
- 처음에는 AI 툴을 사용해서 개발을 하기 때문에 배우는게 적지 않을까 고민되었는데 과제를 완료하고, 학습 가이드를 만들다보니 심도 깊게 과제를 고민해볼 수 있었습니다.
- 🆕 새로 만들어본 나만의 학습 가이드
- 리액트를 공부할 때 가장 기본이 되지만, 생각해보지 못했던 VirtualDOM에 대해 자세히 알게된 점이 가장 좋았습니다. 직접 구현을 해보니 리액트에서 당연하게 생각했던 부분을 실제로 구현해 사용하려면 복잡하고, 고려해야할 부분이 많다는 것을 알게 되었습니다.
- 개요
- Virtual DOM은 실제 DOM의 '가벼운' 복사본이며, 객체로 관리되어 개발자 경험이 향상됨.
- 직접 DOM 조작이 가장 빠르지만, Virtual DOM을 활용하면 성능 이슈를 방지 가능.
- 선언형 UI를 지원하여 전체적인 구조 이해가 쉬워지고, HTML 조작을 간소화함.
- 브라우저 종속성이 없으며, JSX를 사용하여 가독성을 개선함.
- 동작 원리
- Virtual DOM은 메모리 상에서 JavaScript 객체로 표현됨.
- 상태 변경 시 새로운 Virtual DOM을 생성하여 이전 버전과 비교(diffing).
- 변경된 부분만 실제 DOM에 반영(Reconciliation).
- 이를 통해 성능 최적화 및 불필요한 DOM 조작 방지.
- Virtual DOM이 만능일까?
- 작은 변경이 많을 경우 오히려 오버헤드가 발생할 수 있음.
- 복잡한 애니메이션 처리에는 한계가 있음.
- Virtual DOM보다 특정 상황에서는 직접 DOM 조작이 더 빠를 수 있음.
- 이번에는 내용을 이해하고 소화하기 벅찼습니다. 그래서 평소와는 다르게 코드에 주석을 많이 활용해 내용의 흐름을 놓치지 않도록 노력했습니다.
질문 1) oldComponent 재사용 로직을 더 최적화할 방법이 있을까요? 현재 oldNode.type과 newNode.type을 비교하여 동일한 경우에만 oldComponent를 재사용하고 있습니다. 이 방식에서는 다음과 같은 고민이 있습니다.
- 상태(state) 및 내부 데이터 유지
- 기존 VNode를 재사용하더라도, 새로운 nextVNode를 생성하는 과정에서 내부 상태가 유지되지 않을 가능성이 있습니다.
- props 변화가 크지 않은 경우에도 불필요한 재생성이 발생할 수 있습니다.
- 불필요한 연산 방지
- 현재 방식에서는 항상 newNode.type({...})을 호출하여 새로운 VNode를 생성하고 있습니다.
- props가 변경되지 않았다면 기존 VNode를 그대로 유지하는 방식이 가능할지 궁금합니다.
- 비교 방식 개선
- type이 같은 경우에도 props를 비교하여 변동이 없으면 기존 VNode를 유지할 수 있을지 고민하고 있습니다.
- React.memo처럼 이전 props와 비교하여 변경된 경우에만 다시 렌더링하는 방식이 적용 가능할까요?
// + 함수형 컴포넌트인 경우 비교 후 재사용
if (typeof newNode.type === "function") {
const oldComponent = oldNode.type === newNode.type ? oldNode : null;
const nextVNode = newNode.type({
...newNode.props,
children: newNode.children,
});
return updateElement(parentElement, nextVNode, oldComponent, index);
}
답변: 말씀해주신 방향 모두 좋은 방식으로 접근하고 계신 것 같아요. 상태 유지의 경우 리액트에서는 Fiber 아키텍처를 통해 컴포넌트의 상태를 각 노드에 저장하고 있는데요. 이런 부분에 대한 구현을 살펴보셔도 좋을 것 같고 불필요한 연산에 대해서도 정확하게 메모이제이션에 대한 내용에 해당되어 구현이 가능할 것으로 보입니다. 캐싱 로직을 작성하면 될 것 같아요! 마지막으로 리엑트에서도 memo같은 곳에 파라미터를 전달해 비교 방식을 세밀하게 할 수 있는데요. 기본 방식을 따르게 하되, 메모처럼 파라미터를 받으면 좀 더 확장성 있는 인터페이스가 되지 않을까 싶네요 ㅎㅎ
질문2) 저희 회사에서는 각 모듈별로 이벤트 리스너를 달아주는데 이번 과제에서는 가이드대로 eventManager에서 모든 이벤트 타입에 대해 한 번만 리스너를 등록하는 방식으로 구현하였습니다.
이벤트 리스너가 자동으로 처리되는 방식이라 개발하기에는 더 수월한 것 같은데, 이벤트 버블링 오버헤드나, 이벤트 위임을 사용함에 따라 프로젝트 규모가 커지면 이벤트 흐름을 파악하기 어려운 등의 문제는 없을까요?
보통 어떤식으로 구현하는 것이 일반적인지 궁금합니다!
답변: 우선 위임을 했을 때 메모리에 대한 부분이나 동적인 요소에 대한 관리 부분에서 편하다는 부분을 아셨을 것으로 보이는데요! 보통 위임 방식을 구현하게 되는 경우 위임을 받은 부분에 대해 명확하게 디버깅 할 수 있는 방법이 필요하긴 한것 같아요! 사실 위임을 했다고 모든 장점을 누렸다라고 보기엔 어려운게 위임 후 위임받은 곳에서 추가적인 최적화들이 필요해 보이거든요. 리액트에서 실제로 어떻게 구현이 되어있는지 살펴보면 도움이 될 것 같은데요. 관련해서 도움이 될 수 있는 글 함께 공유 드릴게요! (최근에 아고라에 도운님도 글 올려주셨더라구요! 그것도 꼭 먼저 읽어보세요)
- https://dev.to/lukewanghanxiang/react-understanding-reacts-event-system-dm7
- https://laurent.tistory.com/entry/React-React%EC%9D%98-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
- https://www.greatfrontend.com/questions/quiz/explain-event-delegation
- https://dev.to/tejastn10/mastering-react-events-understanding-debugging-and-optimizing-event-handling-4kn0