프레임워크 없는 프론트엔드 개발 2장 정리

이종호·2023년 10월 7일
0

이 글의 목적
자꾸 완독이 안되다보니 첫 장만 반복해서 보게 되고 있었다.
점진적으로라도 나아가기위해 이해했던 부분만 알아볼 수 있게 적어 놓는다.

1. 기본 변경함수를 통한 변경

코드: https://github.com/Apress/frameworkless-front-end-development/tree/master/Chapter02/01

  1. 부모 요소를 복제한다.
    const element = targetElement.cloneNode(true)
  2. element 하위 DOM에서 변경이 필요한 요소를 찾는다.
    const list = element.querySelector('.todo-list')
    const counter = element.querySelector('.todo-count')
    const filters = element.querySelector('.filters')
  3. 하위 DOM(요소)를 상태(ex) todos)와 함수(ex) getTodoElement)로 통해 수정한다.
    list.innerHTML = todos.map(getTodoElement).join('')
    counter.textContent = getTodoCount(todos)
  4. requestAnimationFrame를 통해 적절한 시점에 새로만든 요소(element)로 기존 요소를 대체한다.()
    window.requestAnimationFrame(() => {
     const newMain = view(main, state)
     main.replaceWith(newMain)
    })

알게된 점

  • 요소를 렌더링 한다는게 생각보다 큰 작업은 아님을 알게됨
    물론 아직 최적화나, 복잡한 요소를 하기위한 구성은 아니지만

2. 각 요소를 그리는 부분은 작은 함수로 쪼갬

코드: https://github.com/Apress/frameworkless-front-end-development/tree/master/Chapter02/02

before:

  const element = targetElement.cloneNode(true)
  const list = element.querySelector('.todo-list')
  // ..

  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')
      }
    })

after

  const element = targetElement.cloneNode(true)

  const list = element
    .querySelector('.todo-list')
  // ..

  list.replaceWith(todosView(list, state))
  counter.replaceWith(counterView(counter, state))
  filters.replaceWith(filtersView(filters, state))

알게된 점

  • 크게는 없는듯
  • 역시 코드는 보기 좋아야 한다.

3. registry를 통한 선언적으로 요소 - view함수 매핑..?

코드: https://github.com/Apress/frameworkless-front-end-development/tree/master/Chapter02/03

핵심

const registry = {
  'dataset으로 매핑된 키값': view함수
}
  1. html 요소에 변경이 필요한 요소마다 dataset 속성을 이용하여 마킹을 해준다.

  2. 해당 dataset의 값에 매칭되어야할 view함수들을 선언적으로 코딩한다.

    registry.add('todos', todosView)
    registry.add('counter', counterView)
    registry.add('filters', filtersView)
  3. 부모 요소에서 dataset을 가진 요소를 전부 찾는다.

      const childComponents = element.querySelectorAll('[data-component]')
  4. 해당 요소가 registry에 매핑된 view함수가 있다면 해당 함수를 호출한 결과값으로 교체한다.

    const child = registry[name]
    if (!child) {
      return
    }
    
    target.replaceWith(child(target, state))
  • 선언적 이라는 단어가 어떤 느낌인지 알기까지가 좀 어려운듯
// before

window.requestAnimationFrame(() => {
  const main = document.querySelector('.todoapp')
  const newMain = appView(main, state)
  main.replaceWith(newMain)
})
// after
registry.add('todos', todosView)
registry.add('counter', counterView)
registry.add('filters', filtersView)

window.requestAnimationFrame(() => {
  const main = document.querySelector('.todoapp')
  const newMain = registry.renderRoot(main, state)
  main.replaceWith(newMain)
})
  • selector로 요소를 호출하는 부분과 view함수로 새로운 요소를 만드는 것도 재귀함수로 구현하게 됨
// before
  const list = element.querySelector('.todo-list')
  const counter = element.querySelector('.todo-count')
  const filters = element.querySelector('.filters')

  list.replaceWith(todosView(list, state))
  counter.replaceWith(counterView(counter, state))
  filters.replaceWith(filtersView(filters, state))
// after
  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
    }
  }

알게된 점

  • 선언적?인 표현, 재귀함수를 통한 반복 함수 호출 제거가 아름다웠다.
  • dataset를 활용하는 한가지 방법을 배웠다.(그전에도 비슷한 용도로 쓰긴했지만, 이렇게 본격?적으로 사용하는건 못본듯)

4. 가상DOM을 통한 더 효율적인 렌더링

코드: https://github.com/Apress/frameworkless-front-end-development/tree/master/Chapter02/04

기존 코드의 문제:

  • 변경을 할 때 전체 요소(노드)를 변경했어야만 한다.(replaceWith())
  • 가령 ul안에 li 한개만 추가되어야 하는 변경에도 ul 전체를 새로 만들어 변경해야한다.

가상DOM의 핵심 원리는 큰 요소를 바꿔끼는 비용보다, 변경되어야 하는 최소한의 부분을 계산하고 적용하는게 더 빠르다는 원리이다.

전체 변경 비용 > 변경해야할 부분을 찾기 + 변경된 부분(요소)만 변경

이런 "변경해야할 부분을 찾기"는 reconciliation이라 불린다.

React Reconciliation: https://ko.legacy.reactjs.org/docs/reconciliation.html

저자도 간단한 형태의 reconciliation함수를 만들었다.
형태는 다음과 같다.

applyDiff(부모요소, 실제돔, 가상돔)

저자가 간단히 만든 diff 함수 규칙

  1. 실제노드에는 있는데 가상노드에는 없다면 '삭제'한다.
  if (realNode && !virtualNode) {
    realNode.remove()
    return
  }
  1. 실제노드에는 없는데 가상노드에 있다면 '추가'한다.
    if (!realNode && virtualNode) {
      parentNode.appendChild(virtualNode)
      return
    }
  2. 두 개의 노드을 비교해 차이가 있으면 가상돔으로 '교체'한다.
    if (isNodeChanged(virtualNode, realNode)) {
      realNode.replaceWith(virtualNode)
      return
    }
  3. 마지막으로 해당 해당 노드에 자식 노드가 있을 경우 재귀적으로 호출한다.

매우 간단하고 직관적이다

알게된 점

  • 까먹고 있던 React의 재조정함수에 중요성과 가상DOM의 유효성에 대해 다시 알게되었다.
profile
코딩은 해봐야 아는 것

0개의 댓글