오늘은 '프레임워크 없는 프론트엔드 개발' 책 중 챕터 2를 읽으며 정리했던 부분을 step by step으로 정리해보려고 한다. 그냥 책으로 읽고 넘어가기엔 내용이 중요하다고 생각되어, 이번 기회에 꼼꼼하게 정리하면 좋을 것 같다는 생각이 들었다.
[GOAL]
해당 책 속에 사용된 예제는 TodoMVC 템플릿이다. 아래 링크를 타고 들어가면 여러 예제들과 라이브 데모를 함께 볼 수 있다.
https://todomvc.com/
그럼 이제 코드 하나하나씩 뜯어보며 DOM 조작 플로우를 따라가보자.
*오늘 뜯어볼 코드는 구성 요소 기반의 애플리케이션으로, 레지스트리를 갖는 렌더링 엔진이라는 것이 가장 메인 포인트이니, 이를 명심하자.
우선 기본적인 HTML 구조를 정의하면 다음과 같다.
TodoMVC의 CSS를 링크하여 스타일을 적용하고, body 태그 내의 .todoapp section을 포함함으로써 해당 섹션이 메인 컨테이너임을 알 수 있다. 그리고 당연하겠지만, index.js를 script 태그를 통해 불러오게 된다.
그리고 코드를 자세히 보면, data-component 속성을 볼 수 있는데, 이는 뷰 함수의 필수 호출을 대체하는 역할을 한다.
구성 요소 라이브러리를 생성하기 위한 또 다른 필수 조건은 바로 registry이다.
우선 애플리케이션의 진입점인 index.js를 먼저 뜯어보자.
일단, registry에 사전에 만들어두었던 view들이 추가된다.
그럼 todosView, counterView, filterView는 어떤 코드들인지 알아보자.
*다음과 같은 형태로 레지스트리가 구성된다.
const registry = {
'todos': todosView,
'counter': counterView,
'filters': filtersView
}
초기상태 설정이 완료되면 렌더링이 시작되고, getTodos.js를 통해 초기 투두리스트를 가져와 상태를 정의하게 된다. getTodos.js에서 어떤 값을 반환해서 상태를 구성하는지 계속 알아보자.
getTodos.js를 뜯어봤으니 다시 index.js로 돌아가보자.
사진에..꽤 복잡한 선들이 있지만 하나씩 차근차근 정리해보겠다.
여기서부터 함께 정신차리며 따라가보자.
...사실 나한테 하는 말
우선 render안에서 사용된 requestAnimationFrame
은 브라우저의 렌더링 주기에 맞춰 부드러운 애니메이션 효과를 제공할 수 있도록 하는 자바스크립트 내장 API라고 하며, RAF라고도 부른다.
아래는 MDN에서 가져온 정의이다.
window.requestAnimationFrame()
메서드는 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트 바로 전에 브라우저가 애니메이션을 업데이트할 지정된 함수를 호출하도록 요청합니다. 이 메서드는 리페인트 이전에 호출할 인수로 콜백을 받습니다.
우리가 봐야할 핵심 포인트는,
이 두 가지라고 할 수 있다.
=> 즉, requestAnimationFrame
을 사용해 브라우저의 다음 리페인트 전에 함수를 실행하도록 요청하는 함수이며, 보통 성능 최적화를 위해 사용되며 화면 갱신과 자연스럽게 동기화된다는 점!
const render = () => {
window.requestAnimationFrame(() => {
const main = document.querySelector(".todoapp");
const newMain = registry.renderRoot(main, state);
applyDiff(doucemnt.body, main, newMain);
});
}
다시, 렌더함수를 해석해보면..
main
은, 클래스가 .todoapp
인 요소를 찾아 저장된다.newMain
은, 간단히 말하자면 새로운 가상 DOM 트리를 생성하고 저장되는 아이인데...newMain이 만들어지는 과정을 자세히 알아보도록 하자.우선 newMain이 선언된 부분을 다시 보자.
const newMain = registry.renderRoot(main, state);
코드에서도 알 수 있듯이, 앞서 index.js에서 추가해뒀던 registry를 활용해 renderRoot를 호출하면 된다.
const renderRoot = (root, state) => {
const cloneComponent = (root) => {
return root.cloneNode(true);
};
return renderWrapper(cloneComponent)(root, state);
};
const renderWrapper = (component) => {
return (targetElement, state) => {
// 인자 받아서 컴포넌트를 렌더링
const element = component(targetElement, state); // 컴포넌트 렌더링
// 결과로 나온 DOM 요소를 element에 저장
const childComponents = element.querySelectorAll("[data-component]");
// element 내부에서 data-component 속성을 가진 모든 자식 컴포넌트를 찾는다.
// 이 속성은 자식 컴포넌트의 이름을 나타낸다.
console.log("childComponents", childComponents);
// 자식 컴포넌트들을 배열로 변환 -> 각각에 대해 반복 처리
Array.from(childComponents).forEach((target) => {
const name = target.dataset.component;
console.log("name", name);
// registry에서 해당 컴포넌트 렌더링 함수를 찾아 호출
const child = registry[name];
if (!child) {
return;
}
// 결과로 나온 DOM 요소로 기존의 자식 컴포넌트를 대체
target.replaceWith(child(target, state));
});
return element;
};
};
우선 아래 코드를 보기 전에 다음 개념을 알고가면 좋을 것 같다.
[가상 DOM]
⇒ DOM의 변경 사항을 최소화하여 성능을 개선하는 데 도움을 준다.
⇒ 가상 DOM과 실제 DOM 사이의 차이점만을 적용함으로써, 불필요한 DOM 조작을 줄이고 애플리케이션의 반응 속도를 향상시킨다.
⇒ 렌더링 엔진을 최대한 간단하게 유지하는 걸 권장!
index.html
: 애플리케이션의 기본 구조를 정의하는 HTML 파일. TodoMVC의 CSS를 활용하여 스타일링하고 있으며, index.js를 메인 스크립트로 로드.index.js
: 애플리케이션의 진입점으로, 초기 상태를 설정하고 렌더링을 트리거.getTodos.js
: 더미 데이터를 생성하여 초기 투두 리스트를 반환하는 모듈. 더미 데이터 랜덤 생성을 위해 faker.js 라이브러리 활용view.js
: 현재 상태를 기반으로 DOM 요소를 생성하고 업데이트하는 렌더링 로직을 담고 있다.registry.js
: 컴포넌트와 해당 렌더링 함수를 등록하고 관리하는 레지스트리 모듈applyDiff.js
: 기존 DOM과 새로운 DOM을 비교하여 변경된 부분만 실제 DOM에 적용하는 함수.책으로 읽기만 하니까 처음에 이해가 잘 안되는 부분도 있었지만, 역시 직접 코드를 치면서 다이어그램으로 정리하니... 이해가 더 잘 되는 것 같다!