본문 바로가기

Frontend

프레임워크 없는 프론트엔드 개발 | 렌더링

모든 웹 애플리케이션에서 가장 중요한 기능 중 하나는 데이터의 표시이다. 좀 더 명확하게 말해 데이터를 표시한다는 것은 요소를 화면이나 다른 출력 장치에 렌더링한다는 것을 의미한다. W3C(World Wide Web Consortium)은 프로그래밍 방식으로 요소를 렌더링하는 방식을 문서 객체 모델 DOM(Document Object Model)로 정의했다.

 

문서 객체 모델

DOM은 웹 애플리케이션을 구성하는 요소를 조작할 수 있는 API이다. 자세한 내용은 W3C 규격 페이지를 참조.

 

What is the Document Object Model?

What is the Document Object Model? Editors Jonathan Robie, Texcel Research Introduction The Document Object Model (DOM) is a programming API for HTML and XML documents. It defines the logical structure of documents and the way a document is accessed and ma

www.w3.org

 

기술적 관점에서 보면 모든 HTML 페이지는 트리로 구성된다. DOM은 HTML 요소로 정의된 트리를 관리하는 방법이다.

표준 CSS 선택자(selector)를 사용해 querySelector 메서드로 올바른 요소를 선택할 수 있다. querySelector 메서드는 Node 메서드이다. Node는 HTML 트리에서 노드를 나타내는 기본 인터페이스이다. 모질라 개발자 네트워크 페이지에서 모든 Node의 메서드와 속성을 확인할 수 있다.

렌더링 성능 모니터링

웹용 렌더링 엔진을 설계할 때는 가독성(readability)와 유지 관리성(maintainability)를 염두에 둬야 한다.렌더링은 모든 웹 애플리케이션에서 매우 중요한 작업이다.

 

렌더링 엔진에서 또 다른 중요한 요소는 성능이다. 

 

먼저 브라우저, 특히 크롬에서 잘 알려진 개발자 도구를 살펴본다. 렌더링 성능 모니터링에서 사용할 수 있는 기능 중 하나는 편리한 초당 프레임 수(FPS, Frames Per Second)이다. 

 

개발자 도구를 열고 Cmd + Shift + P를 눌러서 명령 메뉴를 표시한다. 그런 다음 Show frame per seconds(FPS) meter 메뉴 항목을 선택하면 다음과 같은 명령 메뉴를 확인할 수 있다.

 

애플리케이션의 FPS를 모니터링하는 또 다른 방법은 어떤 웹 애플리케이션에도 쉽게 포함할 수 있는 아주 간단한 라이브러리인 stats.js(https://github.com/mrdoob/stats.js/)를 이용하는 것이다. 또한 이 도구는 프레임과 할당된 메가바이트의 메모리를 렌더링하는데 필요한 필리초를 표시할 수도 있다. 

 

애플리케이션의 정보를 보여주는 위젯을 작성해보자. requestAnimationFrame 콜백을 사용해 현재 렌더링 사이클과 다음 사이클 사이의 시간을 추적하고 콜백이 1초 내에 호출되는 횟수를 추적하면 된다.(48p)

 

렌더링 함수

순수하게 함수를 사용해 요소를 DOM에 렌더링하는 다양한 방법을 분석해보자. 순수 함수로 요소를 렌더링한다는 것은 DOM 요소가 애플리케이션의 상태에만 의존한다는 것을 의미한다. 

 

view = f(state) . => 순수 함수 렌더링의 수학적 표현

순수함수를 사용하면 테스트 가능성이나 구성 가능성(composable) 같은 많은 장점이 있지만 문제도있다.

 

간단한 렌더링 엔진은 requestAnimationFrame을 기반으로 한다. 모든 DOM 조작이나 애니메이션은 이 DOM API를 기반으로 해야한다. 이 콜백 내에서 DOM 작업을 수행하면 더 효율적이다. 이 API는 메인 스레드를 차단하지 않으며 다음 다시 그리기(repaint)가 이벤트 루프에서 스케줄링되기 직전에 실행된다.

 

컴포넌트 함수

컴포넌트 기반의 애플리케이션을 작성하려면 컴포넌트 간의 상호작용에 선언적 방식을 사용해야 한다. 다음 애플리케이션(https://github.com/Apress/frameworkless-front-end-development/tree/master/Chapter02/03)은 컴포넌트 레지스트리를 갖는 렌더링 엔진의 예이다.  이 목표를 달성하고자 먼저 해야 할 일은 특정 사례에서 사용할 컴포넌트를 선언하는 방법을 정의하는 것이다. 이 때 data 속성을 사용할 수 있다.

 

컴포넌트 라이브러리를 생성하기 위한 또 다른 필수 조건은 레지스트리(registry)로, 레지스트리는 애플리케이션에서 사용할 수 있는 모든 컴포넌트의 인덱스이다. 여기서 구현할 수 있는 가장 간단한 레지스트리는 아래와 같이 일반 자바스크립트 객체이다.

 

const registry = {
  'todos': todosView,
  'counter': counterView,
  'filters': filtersView
}

 

레지스트리의 키는 data-component 속성 값과 일치한다. 이것이 컴포넌트 기반 렌더링 엔진의 핵심 메커니즘이다. 이 메커니즘은 루트 컨테이너(애플리케이션 뷰 함수)뿐만 아니라 생성할 모든 컴포넌트에도 적용돼야 한다. 이렇게하면 모든 컴포넌트가 다른 컴포넌트 안에서 사용될 수 있다.

 

이 작업을 위해서는 모든 컴포넌트가 data-component 속성의 값을 읽고 올바른 함수를 자동으로 호출하는 기본 컴포넌트에서 상속돼야 한다. 하지만 순수 함수로 작성하고 있기 때문에 실제로는 이 기본 객체에서 상속받을 수 없다. 따라서 컴포넌트를 래핑하는 고차함수를 생성해야한다.

const renderWrapper = component => {
  return (targetElement, state) => {
    const element = component(targetElement, state)
    
    const childComponents = element.querySelectorAll('[data-component]')
    
    Array.from(childComponents)
    .forEach(target => {
      const name = target.dataset.component
      const child = registry[name]
      if (!child) {
        return
      }
      
      target.replaceWith(child(target,state))
    })
    return element
  }
}

가상 DOM

리액트에 의해 유명해진 가상 DOM 개념은 선언적 렌더링 엔진의 성능을 개선시키는 방법이다. UI 표현은 메모리에 유지되고 '실제' DOM과 동기화된다. 실제 DOM은 가능한 한 적은 작업을 수행한다. 이 과정은 조정(reconciliation)이라고 불린다. 

 

가상 DOM의 핵심은 Diff 알고리즘이다. 이 알고리즘은 실제 DOM을 문서에서 분리된 새로운 DOM 요소의 사본으로 바꾸는 가장 빠른 방법을 찾아낸다.

const applyDiff = (parentNode, realNode, virtualNode) => {
  if (realNode && !virtualNode) {
    realNode.remove()
    return
  }
  if (!realNode && virtualNode) {
    parentNode.appendChild(virtualNode)
    return
  }
  if (isNodeChanged(virtualNode, realNode)) {
    realNode.replaceWith(virtualNode)
    return
  }
  
  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 isNodeChanged = (node1, node2) => {
  const n1Attributes = node1.attributes
  const n2Attributes = node2.attributes
  
  if (n1Attributes.length !== n2Attributes.length) {
    return true
  }
  
  const differentAttribute = Array.from(n1Attributes).find(attribute => {
    const { name } = attribute
    
    const attribute1 = node1.getAttribute(name)
    const attribute2 = node2.getAttribute(name)
    
    return attribute1 !== attribute2
  })
  
  if (differentAttribute) {
    return true
  }
  
  if (node1.children.length === 0 && node2.children.length === 0 && node1.textContetn !== node2.textContent) {
    return true
  }
  
  return false
}

이 diff 알고리즘 구현에서는 노드를 다른 노드와 비교해 노드가 변경됐는지 확인한다.

- 속성 수가 다르다.

- 하나 이상의 속성이 변경됐다.

- 노드에는 자식이 없으며, textContent가 다르다.

 

개선된 검사 수행으로 성능을 높일 수 있지만 렌더링 엔진을 최대한 간단하게 유지하는 것이 좋다. 문제가 발생하면 상황에 맞게 알고리즘을 조정한다. 도널드 크루스는 "시기 상조의 최적화는 모든(또는 적어도 대부분의)악의 근원이다라고 말했다.

 

Future Work

- requestAnimationFrame에 대한 이해

- requestAnimationFrame을 반영한 브라우저 렌더링 프로세스

- https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes