Vue 예외 케이스 - 이벤트 리스너, 순환 참조

YEZI🎐·2022년 12월 4일
0

Vue

목록 보기
23/45

프로그래밍적 이벤트 리스너

지금까지 본 $emit을 사용하고 'v-on으로 듣는 방법 이외에도 Vue 인스턴스는 또 다른 이벤트 인터페이스 사용 방법을 가지고 있다.

  • $on(eventName, eventHandler)을 이용한 이벤트 청취
  • $once(eventName, eventHandler)을 이용한 단발성 이벤트 청취
  • $off(eventName, eventHandler)을 이용한 이벤트 청취 중단

Limit

// datepicker를 input에 한 번 연결함
// DOM에 직접 연결됨
mounted: function () {
  // Pikaday는 서드파티 라이브러리
  this.picker = new Pikaday({	// 인스턴스 변수
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
},
// 컴포넌트를 destroy하기 직전에
// datepicker를 destroy함
beforeDestroy: function () {
  this.picker.destroy()
}
  • 라이프사이클 훅에서만 picker에 접근할 수 있는 경우, picker가 컴포넌트 인스턴스 안에 저장되어야 한다.
  • 셋업을 위한 코드와 제거를 위한 코드가 분리되어 있기에 무언가를 제거하거나 설치하는데있어 (프로그래밍적으로) 어려워진다.

프로그래밍적 리스너를 이용하면 위 두가지 이슈를 모두 해결할 수 있다.

mounted: function () {
  // mounted에서만 쓸 수 있게 로컬 변수로 선언
  var picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
  // mounted 내에서 destroy 처리
  this.$once('hook:beforeDestroy', function () {
    picker.destroy()
  })
}

위 방법을 사용하면 Pikaday를 다양한 엘리먼트에 사용할 수 있고 각각의 새로운 인스턴스는 사용된 후 destroy 하기 전에 스스로 picker를 destroy 하게 된다.

mounted: function () {
  this.attachDatepicker('startDateInput')
  this.attachDatepicker('endDateInput')
},
methods: {
  attachDatepicker: function (refName) {
    var picker = new Pikaday({
      field: this.$refs[refName],
      format: 'YYYY-MM-DD'
    })

    this.$once('hook:beforeDestroy', function () {
      picker.destroy()
    })
  }
}

앞서 말했듯, 위와 같이 Pikaday 여러 개를 컴포넌트 안에서 만들어 사용할 수 있다.

순환 참조

재귀 컴포넌트

컴포넌트는 재귀적으로 템플릿 안에서 호출될 수 있지만 name 옵션을 이용해서만 호출될 수 있다.

name: 'unique-name-of-my-component'

Vue.component를 이용해 컴포넌트를 전역으로 등록하는 경우, 전역ID는 자동으로 컴포넌트의 name 옵션의 값으로 설정된다.

Vue.component('unique-name-of-my-component', {
  // ...
})

주의하지 않으면 재귀 컴포넌트는 무한 루프를 발생 시킬 수 있다.

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

위와 같은 컴포넌트는 "max stack size exceeded(최대 스택 사이즈가 초과되었습니다)" 오류를 발생시키므로 재귀 호출이 조건부인지 확인한다.
이와 같은 이유로 이 방법은 추천하지 않는다.

두 컴포넌트 간의 순환 참조

Finder나 File Explorer 같은 파일 디렉토리 트리를 만드는 경우를 생각했을 때,

<!-- 부모 컴포넌트 -->
<p>
  <span>{{ folder.name }}</span>
  <tree-folder-contents :children="folder.children"/>
</p>
<!-- 자식 컴포넌트 -->
<ul>
  <li v-for="child in children">
    <tree-folder v-if="child.children" :folder="child"/>
    <span v-else>{{ child.name }}</span>
  </li>
</ul>

역설적이게도 렌더링 트리에서 컴포넌트끼리 서로의 자식이자 부모임을 알 수 있다.
(닭과 계란 중 뭐가 먼저인가 같은 상황)
컴포넌트를 글로벌하게 등록하는 경우 이 역설은 자동으로 해결된다.

단, 모듈 시스템(Webpack 또는 Browserify)을 통해 컴포넌트를 require 혹은 import를 시도하는 경우 아래와 같은 오류가 발생한다.

Failed to mount component: template or render function not defined.

A를 부르려니 B가 필요하고 B를 부르려니 A가 필요한, 서로가 서로에 의해 정의되어지는 상황이 생긴다.
즉, 서로 정의되기 전에 정의가 될 수 없는 무한 루프에 빠지게 된다.

이 문제를 해결하려면 모듈 시스템에 "A는 결국 B를 필요로 하지만 B를 먼저 해결할 필요는 없다"고 말할 수 있는 지점을 제공해야 한다.
beforeCreate 라이플 사이클 훅을 이용해 호출되기를 기다렸다가 자식 컴포넌트를 등록하는 방법으로 처리하면 된다.

beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}
components: {
  TreeFolderContents: () => import('./tree-folder-contents.vue')
}
profile
까먹지마도토도토잠보🐘

0개의 댓글