오르막길 잊기전에 기록하기

Vue의 Event는 왜 버블링이 되지 않을까?

들어가며

Vue를 처음 접했을때 많이 헷갈렸던 부분이 Event와 관련된 부분이었다.

기본적으로 DOM에서 이벤트가 동작하는 방식과는 사뭇 다르게 동작하기 때문인데, 왜 기존에 DOM의 Event가 동작하는 방식과는 다르게 설계되었는지 간략히 정리하려 한다.

DOM과 Event

DOM의 Event의 특징은 크게 두가지, 버블링 Bubbling 과 캡쳐 Capture 라고 할 수 있겠다. 각각 이벤트가 상위 요소들로 전파되거나, 하위 요소를 따라 탐색하는 걸 일컫는다.

Vue와 Event

Vue의 Event는 위 두가지, 즉 이벤트의 전파가 존재하지 않는다. 자식 컴포넌트에서 발생한 이벤트가 처리되지 않았다고 할 지라도 이 이벤트가 더 상위의 부모 컴포넌트, 그 부모의 부모 컴포넌트까지 전파되지 않는다. 그래서 처음 Vue 컴포넌트를 구현하다 보면 특정 컴포넌트에서 이벤트를 발생시키고 증조 할아버지쯤 컴포넌트에서 이벤트를 잡으려 노력하며 ‘왜 이벤트가 안잡히지’ 하고 삽질을 하게 되는 것이다.

예제

예제코드는 다음 저장소를 참고한다.

다음과 같은 컴포넌트가 있다고 하자.

<template>
  <div class="card">
    <CardHeader/>
    <CardImage/>
    <CardContent/>
  </div>
</template>
<script>
import CardImage from '@/components/CardImage.vue';
import CardContent from '@/components/CardContent.vue';
import CardHeader from '@/components/CardHeader.vue';

export default {
  name: 'Card',
  components: {
    CardContent,
    CardImage,
    CardHeader,
  },
};
</script>

CardHeader 컴포넌트 안에는 다음과 같이 두가지 버튼이 포함되어 있다.

<template>
  <div class="icon-container">
    <AlertButton/>
    <CheckButton/>
  </div>
</template>

<script>
import AlertButton from '@/components/AlertButton.vue';
import CheckButton from '@/components/CheckButton.vue';

export default {
  name: 'CardHeader',
  components: {
    CheckButton,
    AlertButton,
  },
};
</script>

그중 체크유무를 표시하는 컴포넌트는 다음과 같다.

<template>
<span class="icon" :class="{checked: checked}" @click="check">
      <i class="fas fa-check"></i>
    </span>
</template>
<script>
export default {
  name: 'CheckButton',
  data() {
    return {
      checked: false,
    };
  },
  methods: {
    check() {
      this.checked = !this.checked;
    },
  },
};
</script>

정리하면 Card > CardHeader > CheckButton 으로 컴포넌트가 중첩되어 있는 구조이다. CheckButton 에서 클릭 이벤트가 발생하면 Cardborder 색상을 변경해주고 싶다고 했을 때, 단순히 구현한다면 다음과 같이 Card 컴포넌트에 클릭 이벤트 리스너를 등록하여 구현할 수 있을 것이다.

Card.vue

...
mounted() {
    this.$el.addEventListener('click', (e) => {
      if (e.target.classList.contains('btn-check')) {
        this.$el.style.border = '1px solid #2f9e4d';
      }
    });
  },
...

CheckButton 에서 발생한 클릭 이벤트가 버블링되어 Card까지 전파되고 이를 잡아서 처리하는. 자바스크립트에서 흔한 이벤트 처리방식이다. 그래서 뷰를 처음 접했을 때, 이와 똑같이 이벤트를 구현했었다.

CheckButton 컴포넌트에서는 클릭시 이벤트를 발생시키게 하고

CheckButton.vue

<template>
<span class="icon" :class="{checked: checked}" @click="check">
      <i class="fas fa-check btn-check"></i>
    </span>
</template>
<script>
export default {
  name: 'CheckButton',
  data() {
    return {
      checked: false,
    };
  },
  methods: {
    check() {
      this.checked = !this.checked;
      this.$emit('checked');
    },
  },
};
</script>

Card 컴포넌트에서 이벤트를 리스닝하게 구현을 하는데

Card.vue

<template>
  <div class="card">
    <CardHeader @checked="onChecked"/>
    <CardImage/>
    <CardContent/>
  </div>
</template>
<script>
import CardImage from '@/components/CardImage.vue';
import CardContent from '@/components/CardContent.vue';
import CardHeader from '@/components/CardHeader.vue';

export default {
  name: 'Card',
  components: {
    CardContent,
    CardImage,
    CardHeader,
  },
  methods: {
    onChecked() {
      this.$el.style.border = '1px solid #2f9e4d';
    },
  },
};
</script>

이 코드는 의도되로 동작되지 않는다. CheckButton 컴포넌트에서 발생한 이벤트가 CardHeader 를 거쳐 Card 까지 전파되지 않기 때문인데, 이를 의도되로 동작하게 하려면 다음과 같이 CardHeader 컴포넌트에서 명시적으로 위로 전파시켜주어야 한다.

CardHeader.vue

<template>
  <div class="icon-container">
    <AlertButton/>
    <CheckButton @checked="onChecked"/>
  </div>
</template>

<script>
import AlertButton from '@/components/AlertButton.vue';
import CheckButton from '@/components/CheckButton.vue';

export default {
  name: 'CardHeader',
  components: {
    CheckButton,
    AlertButton,
  },
  methods: {
    onChecked() {
      this.$emit('checked');
    },
  },
};
</script>

왜 Vue의 이벤트는 기존 자바스크립트 DOM의 이벤트와는 다르게 설계 되었을까?

Vue의 Event가 DOM의 Event와는 다르게 설계된 이유

그 이유는 Vue의 이벤트 모델이 Node의 EventEmitter 를 참고하여 설계되었기 때문이다.

기존에 이벤트를 전파시키는 메소드는 $dispatch 였는데, 2.0 업데이트가 되면서 deprecated 되었다. 그 이유는 컴포넌트간의 이벤트 버블링이 암묵적으로 일어나기 때문에 코드를 이해하기 어렵게 만들기 쉽다는 이유다. 이러한 이벤트 전파는 컴포넌트 트리 구조에 의존적이고, 이는 컴포넌트 트리의 규모가 커지면 추적하기가 어렵기 때문이라고 설명하고 있다.

이러한 중첩된 컴포넌트 사이에 이벤트를 주고 받아야 하는 상황이 생긴다면 EventBus 를 구현하거나, Vuex 스토어를 사용할 것을 권하고 있다.

https://github.com/vuejs/vue/issues/2873 https://github.com/vuejs/vue/issues/9868

comments powered by Disqus