바닐라로 상태 기반 렌더링 컴포넌트 만들기 4 - 코어 컴포넌트 만들기

Sonny·2022년 4월 25일
7

Vanilla

목록 보기
4/5
post-thumbnail

이 시리즈의 이전 글은 아래로...

레포지토리 보러가기🗃

Github - Vanilla Component

feat/#2_CoreComponent 브랜치에서 자세히 확인 하실 수 있습니다! 😊

코어 컴포넌트? 🤔

컴포넌트 구현 시, 중복 코드를 줄이고 컴포넌트들이 모두 동일한 라이프 사이클을 가지게 하여 유지보수를 편리하게 하기 위한 컴포넌트들의 기본 구조

컴포넌트 라이프 사이클 ♻️

라이프 사이클 이미지

컴포넌트 컨셉 🧐

  • Observer 패턴의 구독, 알림 형태를 차용하여 부모의 상태를 구독한 컴포넌트만 부모 컴포넌트의 상태를 추적합니다.
  • preventRenderStateKey에 있는 state key가 변경이 되는 경우, 현재 컴포넌트는 상태만 변경이 되고 렌더링은 진행되지 않습니다.

코어 컴포넌트 데이터 🔧

  • node: 컴포넌트의 node
  • initalState: 컴포넌트 상태의 초기값
  • preventRenderStateKey: 구독중인 컴포넌트의 상태 변경 시, 내부적으로 상태만 업데이트 후 자식 컴포넌트만 렌더링하기 위한 key
  • needRender: 컴포넌트의 상태 변경에 따른 렌더링 여부
  • needUpdate: 컴포넌트의 상태 변경에 따른 setState 여부
  • subscribers: 컴포넌트 상태 변경 시, 상태가 같이 변경될 하위 컴포넌트

코어 컴포넌트 메서드 🔧

  • template(): 컴포넌트의 markup을 반환하는 메서드
  • init(): 렌더링 전, 내부적으로 사용될 변수, 함수 정의 또는 초기 데이터를 받아올 때 사용되는 라이프사이클 메서드
  • fetch(): 초기 렌더링 이후 컴포넌트의 fetching이 필요할 때 실행되는 라이프 사이클 메서드
  • render(): 빈 태그를 컴포넌트의 markup으로 변환, 이벤트를 바인딩, 하위 컴포넌트를 부착을 하는 라이프 사이클 메서드
  • update(): 상태 변경 시, 렌더링을 위한 라이프사이클 메서드
  • updateChildren(): 상태 변경 시, 하위 컴포넌트의 렌더링을 위한 라이프 사이클 메서드
  • attachChildComponent(): 하위 컴포넌트를 상위 컴포넌트의 template과 연결하는 라이프 사이클 메서드
  • subscribe(): 상위 컴포넌트에 구독을 하는 메서드
  • validationState: 컴포넌트의 상태 변경 시, 현재 컴포넌트가 가지고 있는 상태인지 판별하는 메서드
  • setState(): 컴포넌트의 상태 변경 시, 컴포넌트의 상태를 업데이트, 하위 컴포넌트들에게 알리는 메서드
  • notify(): 상위 컴포넌트로부터 받은 새로운 상태로 하위 컴포넌트들의 setState(), render()하게 해주는 메소드
  • setEvent(): 컴포넌트의 node에 이벤트를 바인딩하는 라이프 사이클 메서드
  • clearEvent(): 컴포넌트의 node에 바인딩되어 있는 이벤트를 지우는 라이프 사이클 메서드

코어 컴포넌트 동작 흐름 👀

1. 컴포넌트 생성

// Component.ts

...
constructor({
  node,
  initalState,
  preventRenderStateKey = []
}: IComponentParams<StateType>) {
  this.node = node
  this.state = initalState as StateType
  this.preventRenderStateKey = new Set(preventRenderStateKey)
  this.needRender = false
  this.needUpdate = false
  this.subscribers = new Set([])

  this.init()
  this.render()
  this.fetch()
}

Component를 extends해서 컴포넌트를 생성하는 경우,
Component의 constructor에 의해 init -> render -> fetch가 자동으로 실행됩니다.

2. 렌더링 진행 과정

Component.prototype.render()

// Component.ts
...
render(): void {
  convertTemplateAsComponent.call(this)
  this.setEvent()
  this.attachChildComponent()
}

render() 라이프 사이클 메서드는 내부에 세 가지 단계를 거치게 됩니다.

  • convertTemplateAsComponent() 함수의 this를 확장된 현재 컴포넌트로 명시적 바인딩을 하여 호출합니다.
  • setEvent() 라이프 사이클 메서드를 실행합니다.
  • attachChildComponent() 라이프 사이클 메서드를 실행합니다.

convertTemplateAsComponent()

// utils/dom.ts
...
function convertTemplateAsComponent(this: any): void {
  const oldNode = this.node
  const componentChildren = Array.from(
    new DOMParser().parseFromString(this.template(), 'text/html').body.children
  )
  const component = new DocumentFragment()
  component.append(...componentChildren)

  oldNode.after(component)
  this.node = oldNode.nextSibling

  // CSS 상속
  const oldCSS = oldNode.classList.value.trim()
  const newCSS = this.node.classList.value.trim()
  const isChangedCSS = oldCSS !== newCSS
  const cssValue = isChangedCSS ? newCSS || oldCSS : oldCSS
  this.node.className = cssValue

  oldNode.remove()
}

렌더링에서 가장 중요한 부분을 담당하는 dom 유틸 함수입니다!
현재 컴포넌트의 template을 html로 변경을 해주는 역할을 담당합니다.

  • 이전의 렌더링된 ElementoldNode에 임시 저장합니다.
  • 현재 컴포넌트의 templateElement로 변경한 뒤, oldNode의 뒤에 추가합니다.
  • 추가된 Elementthis.node로 변경합니다.
    • 늘 같은 node를 참조하여 사이드 이펙트 방지, removeEventListener를 해줄 수 있게 되었습니다!
  • 이전의 렌더링된 Element의 class를 상속합니다.
  • 이전 렌더링 된 oldNode를 지워주는 것으로 렌더링 끝!

3. 컴포넌트 부착(연결)하기

// ExampleText.ts
...
template(): string {
  return `
    <main id="App">
      <ExampleText></ExampleText>
      <Button>Change State!</Button>
    </main>
  `
}

attachChildComponent(): void {
  const { text, onClick } = this.state

  const exampleText = new ExampleText({
    node: selectEl(this.node, 'ExampleText'),
    initalState: {
      text
    }
  })

  new Button({
    node: selectEl(this.node, 'Button'),
    initalState: {
      onClick
    }
  })

  this.subscribe(exampleText)
}
  • template()에 컴포넌트로 치환되어야할 부분을 명시합니다.
    (creatElement로 해도 되지만 저는.. 리액트를 따라하고 싶었슴니다,,)

  • attachChildComponent() 내부에서 컴포넌트를 연결합니다.

    컴포넌트 연결 방식은 총 2가지 방식입니다 (구독O / 구독X)

  • node에 컴포넌트가 들어갔으면 하는 자리를 선택합니다.
  • 초기 상태값을 전달합니다.
  • 구독을 원하는 경우, subscribe() 메서드에 해당하는 컴포넌트를 넣어줍니다.

4. 컴포넌트 상태 업데이트 과정

Component.prototype.setState()

// Component.ts
...
setState(newState: Partial<StateType>): void {
  const validState = this.validationState(newState)

  if (!this.needUpdate) {
    return
  }

  const currentState = { ...this.state } as StateType
  const preventRenderStateKey = Array.from(this.preventRenderStateKey)

  validState?.forEach(key => {
    const stateKey = key as keyof StateType

    if (!preventRenderStateKey.includes(key)) {
      this.needRender = true
    }

    currentState[stateKey] = newState[stateKey] as StateType[keyof StateType]
  })

  this.state = currentState
  this.notify(newState)
}
  • 현재 컴포넌트에 변경된 stateKey가 있는지 검사한 뒤, 없으면 바로 종료시킵니다.
  • 변경이 필요한 stateKey를 순회하며 상태를 업데이트하며 렌더링 필요 여부를 체크합니다.
  • 구독자(Component.subscribes)가 있는 경우, notify()로 새로운 상태를 전달합니다.

Component.prototype.notify()

// Component.ts
...
notify(newState: Partial<StateType>): void {
  const subscribers = Array.from(this.subscribers)

  const validSubscribers = subscribers.filter(
    subscriber => subscriber.validationState(newState).length
  )

  validSubscribers?.forEach(subscriber => {
    subscriber.setState(newState)

    if (subscriber.needRender) {
      subscriber.update()
      return
    }

    subscriber.updateChildren()
  })
}
  • 구독자들을 대상으로 변경된 stateKey가 있는지 확인합니다.
  • 구독자들을 순회하며 setState()를 호출합니다.
  • 이때, 렌더링이 필요한 경우, 현재 컴포넌트(+ 하위 컴포넌트) 모두를 렌더링합니다.
  • 렌더링이 필요없는 경우, 하위 컴포넌트만 렌더링을 진행합니다.

Component.prototype.update()

// Component.ts
...
update(): void {
  this.needRender = false
  this.clearEvent()
  this.render()
}
  • 현재 컴포넌트의 렌더링 필요 여부를 다시 초기화합니다.
  • 현재 컴포넌트에 부착되어 있는 이벤트들을 모두 해제합니다.
  • render()를 다시 진행합니다.

Component.prototype.updateChildren()

// Component.ts
...
updateChildren(): void {
  this.needRender = false
  this.attachChildComponent()
}
  • 현재 컴포넌트의 렌더링 필요 여부를 다시 초기화합니다.
  • 현재 컴포넌트는 리렌더링이 되지 않음으로 하위 컴포넌트만 다시 부착을 해줍니다.

고민중

시리즈가 일찍 끝날 줄 알았는데.. 생각보다 오래 걸린다.. 고치고 싶은 부분도 많고...
다음 편은 코어 컴포넌트 활용편으로...

profile
FrontEnd Developer

0개의 댓글