https://m.yes24.com/Goods/Detail/96639825
프레임워크 없는 프론트엔드 개발 - 예스24
프레임워크 없이 효과적으로 작업하는 방법과 프로젝트에 적합한 프레임워크를 선택하는 방법의 두 가지 주제를 다룬다. 프레임워크나 서드파티 라이브러리를 사용하지 않고 프론트엔드 애플
m.yes24.com
프레임워크 없이 DOM을 효과적으로 조작하는 방법에 대해서 배워봅니다.
DOM
웹 애플리케이션을 구성하는 요소를 조작할 수 있는 API
렌더링 성능 모니터링
requestAnimationFrame()을 사용해 현재 렌더링과 다음 렌더링 사이에 1초에 몇 프레임을 그렸는지(FPS) 대략적으로 출력할 수 있음
const tick = () => {
frames++
const now = window.performance.now()
// now가 start로부터 1초이상 지났다면
if (now >= start + 1000) {
// 현재 frame값을 화면에 그리고 초기화
panel.innerText = frames
frames = 0
start = now
}
window.requestAnimationFrame(tick)
}
const init = (parent = document.body) => {
panel = create()
// tick을 반복해서 호출
window.requestAnimationFrame(() => {
start = window.performance.now()
parent.appendChild(panel)
tick()
})
}
requestAnimationFrame()
- 리페인트 바로 전에 지정한 함수를 호출하는 애니메이션 최적화 API
- 사람 눈은 1초에 60프레임 정도를 인식할 수 있기 때문에, 프레임이 16.6ms 안에 변경되면 자연스러운 움직임을 구현할 수 있음
- requestAnimationFrame은 기본적으로 초당 60프레임에 해당하는 주기로 호출되지만, 보통 모니터 주사율에 맞춰서 출력됨
- 실제 화면이 갱신되어 표시되는 주기에 따라 함수를 호출해주기 때문에, setInterval()로 1000/60초 마다 함수를 실행하는 것과 달리 프레임드랍을 예방할 수 있음 (*프레임 드랍 : setInterval은 콜백함수가 시작되는 시간이 프레임 시작 시간과 동일하지 않기 때문에, 콜백함수 실행중에 다음 프레임을 그릴 시간이 되면, 해당함수를 실행하느라 프레임이 누락될 수 있음)
- setInterval과 달리 현재 페이지가 비활성화 되어있는 경우(여러 탭을 실행중이라든지) 백그라운드에서 호출되지 않고 대기하므로 자원낭비x
https://developer.mozilla.org/ko/docs/Web/API/window/requestAnimationFrame
Window: requestAnimationFrame() method - Web API | MDN
화면에 애니메이션을 업데이트할 준비가 될 때마다 이 메서드를 호출해야 합니다. 이는 브라우저가 다음 리페인트를 수행하기 전에 애니메이션 함수를 호출하도록 요청합니다. 콜백의 수는 보
developer.mozilla.org
requestAnimationFrame을 이용해 순수함수로 요소 렌더링
순수함수로 요소를 렌더링한다는 것은 DOM요소가 애플리케이션의 상태에만 의존한다는 것을 의미
// index.js
const state = {
todos: getTodos(),
currentFilter: "All",
};
const main = document.querySelector(".todoapp");
window.requestAnimationFrame(() => {
const newMain = view(main, state);
main.replaceWith(newMain);
});
// view.js
export default (targetElement, state) => {
const { currentFilter, todos } = state;
const element = targetElement.cloneNode(true);
const list = element.querySelector(".todo-list");
const counter = element.querySelector(".todo-count");
const filters = element.querySelector(".filters");
list.innerHTML = todos.map(getTodoElement).join("");
counter.textContent = getTodoCount(todos);
Array.from(filters.querySelectorAll("li a")).forEach((a) => {
if (a.textContent === currentFilter) {
a.classList.add("selected");
} else {
a.classList.remove("selected");
}
});
return element;
};
view는 원래 DOM노드를 받아서 복제하고, state 매개변수의 값에 따라서 노드를 업데이트
만들어진 새 노드 (=가상노드)는 requestAnimationFrame안에서 기존의 DOM노드를 replace
requestAnimationFrame API를 기반으로한 DOM 조작은 메인스레드를 차단하지 않으며, 다음 리페인트 전에 실행됨
생각 : 왜 DOM노드를 직접 수정하지 않고 복제해서 사용할까?
아래에서 가상 DOM 방식을 사용하기 위함이 아닐까 추측.
직접 수정하는 것보다, 복사해서 수정본을 가진 다음에 나중에 원본과 수정본을 비교해서 최소한으로 DOM요소를 변경하기 위함
리팩토링
list.replaceWith(todosView(list, state));
counter.replaceWith(counterView(counter, state));
filters.replaceWith(filtersView(filters, state));
뷰를 하나의 함수가 아니라, 기능별로 노드와 상태를 주면 수정된 새 노드를 리턴하는 각각의 함수로 만들 수 있음
= 컴포넌트 함수
data-attributes를 활용해 컴포넌트 함수 만들기
https://developer.mozilla.org/ko/docs/Learn/HTML/Howto/Use_data_attributes
데이터 속성 사용하기 - Web 개발 학습하기 | MDN
HTML은 특정 요소와 연관되어야 하지만, 정의된 의미를 갖지 않는 데이터에 대한 확장성을 고려하여 설계되었습니다. data-* 속성은 표준이 아닌 속성이나 추가적인 DOM 속성과 같은 다른 조작을 하
developer.mozilla.org
- data-attributes를 활용해 컴포넌트 간의 상호작용에 선언적 방식을 사용할 수 있게 하자
- 클래스 이름으로 domNode를 가져와서 그자리에 새 노드를 렌더했었는데, 그 노드마다 data-component=”컴포넌트명” 을 붙여주자
- 각 이름에 따른 컴포넌트 정보는 레지스트리에 저장하자
// 레지스트리 : 애플리케이션에서 사용할 수 있는 컴포넌트의 인덱스
const registry = {
// 키 : data-component 속성값
// 값 : 컴포넌트 함수
todos: todosView,
counter: counterView,
filters: filtersView,
};
data-component를 가진 노드에 알맞은 컴포넌트를 렌더링 하는 법
1. root element를 렌더링하면 내부에 data-component를 가진 요소들을 레지스트리에서 찾아서 컴포넌트 실행
2. 컴포넌트 안에서도 컴포넌트를 사용할 수 있게 하기 위해서 컴포넌트를 래핑하는 고차함수 사용
const add = (name, component) => {
registry[name] = renderWrapper(component);
};
const renderWrapper = (component) => {
// 인자로 들어온 컴포넌트와 동일한 서명의 컴포넌트를 리턴
return (targetElement, state) => {
const element = component(targetElement, state);
// data-component 속성을 가지는 모든 자식 노드들
const childComponents = element.querySelectorAll("[data-component]");
Array.from(childComponents).forEach((target) => {
const name = target.dataset.component;
const child = registry[name];
if (!child) {
return;
}
// registry에 있는 컴포넌트 함수를 찾아 교체
target.replaceWith(child(target, state));
});
return element;
};
};
const renderRoot = (root, state) => {
const cloneComponent = (root) => {
return root.cloneNode(true);
};
return renderWrapper(cloneComponent)(root, state);
};
동적 데이터 렌더링
실제 애플리케이션에서는 사용자 이벤트에 따라 데이터가 변경되고, 그때마다 데이터를 리렌더링해주어야함
이 경우 매번 루트요소를 새로만들어서 교체한다면, 대규모 애플리케이션에서는 성능이 저하될 수 있음
가상 DOM을 사용해서 이 문제를 해결해보자.
가상 DOM
: 선언적 렌더링 엔진의 성능을 개선하는 방법
: UI 표현은 메모리에만 유지되고, "실제" DOM은 가능한 적은 변경 작업을 수행함
: 이 과정을 reconciliation이라고 부름
간단한 Diff 알고리즘
- 속성 수가 다르다
- 속성이 변경됐다
- 자식이 없으며, textContent가 다르다
const applyDiff = (parentNode, realNode, virtualNode) => {
// 실제 노드는 있으나 가상노드는 없는 경우 -> 실제 노드 삭제
if (realNode && !virtualNode) {
realNode.remove();
return;
}
// 실게 노드는 없으나 가상 노드는 있는 경우 -> 부모노드에 추가
if (!realNode && virtualNode) {
parentNode.appendChild(virtualNode);
return;
}
// 두 노드 모두 있는 경우 -> 달라진 경우에만 교체
if (isNodeChange(virtualNode, realNode)) {
realNode.replaceWith(virtualNode);
}
const realChildren = Array.from(realNode.children);
const virtualChildren = Array.from(virtualNode.children);
const max = Math.max(realChildren.length, virtualChildren.length);
for (let i = 0; i < max; i++) {
applyDiff(realNode, realChildren[i], virtualChildren[i]);
}
};
const isNodeChange = (node1, node2) => {
const n1Attributes = node1.attributes;
const n2Attributes = node2.attributes;
// 속성의 수가 다른 경우 -> 변경됨
if (n1Attributes.length !== n2Attributes.length) {
return true;
}
// 속성의 수가 같은 경우 -> 속성이 다른지 확인
const diffrentAttribute = Array.from(n1Attributes).find((attribute) => {
const { name } = attribute;
const attribute1 = node1.getAttribute(name);
const attribute2 = node2.getAttribute(name);
return attribute1 != attribute2;
});
// 속성이 하나라도 다르다 -> 변경됨
if (diffrentAttribute) {
return true;
}
// 모든 속성이 같은 경우 -> 자식이 없고 textContent가 다른 경우 -> 변경됨
if (
node1.children.length === 0 &&
node2.children.length === 0 &&
node1.textContent !== node2.textContent
) {
return true;
}
return false;
};

생각 : attributes를 비교하는 걸 보고, 요소의 태그이름은 비교안하나? 생각했었는데, attribute는 생각보다 상당히 많은 정보를 가지고 있었다. 실제 리액트의 diff 알고리즘은, 리액트 엘리먼트의 type이 동일하면 속성만 변경해서 사용하고 type이 다르면 자식까지 새로 렌더링하는 것으로 알고있는데, 완전히 동일하지 않더라도 비교조건을 좀 더 상세하게 수정하면 성능을 높일 수 있을 것 같다.