Do not use arrow function as methods in Vue.js
다음 사항을 주의하세요.
{ { someValue } }
으로 표기된 코드는 띄어쓰기를 붙여야지 정상적으로 동작합니다.
1. 문제 현상
Vue.js
에서 컴포넌트를 만들 때 화살표 함수(arrow function, =>)를 사용하는 경우 정상적으로 this
키워드를 찾지 못하는 현상이 있었습니다.
간단하게 코드를 통해 문제를 살펴보겠습니다.
1.1. HelloWorld.vue
HelloWorld
컴포넌트의methods
속성의hideAndVisible
함수를 화살표 함수 형태로 선언하였습니다.this
객체를alert
함수로 출력합니다.- 출력되는
this
객체가undefined
임을 확인합니다.
- 출력되는
<template>
<div class="hello">
<h1>{ { msg } }</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br/>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<button @click="hideAndVisible">{ { visible } }</button>
<div v-if="visible === 'HIDE'">
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
visible: "VISIBLE"
}
},
methods: {
hideAndVisible: () => {
alert(this) // this is undefined
if (this.visible === "VISIBLE") {
this.visible = "HIDE"
} else {
this.visible = "VISIBLE"
}
}
}
}
</script>
<style scoped>
/* styles */
</style>
2. 문제 원인
화살표 함수를 사용하면 this
객체가 바인딩되지 않는 이유가 궁금하여 원인을 찾아봤습니다.
2.1. createApp 함수 탐색하기
Vue
어플리케이션을 만들기 위해 사용하는 createApp
함수를 탐색해봤습니다.
methods
속성과 해당 컴포넌트 객체를 연결해주는 코드가 있을 것이라 예상했고, 관련된 코드를 createApp
함수 내부에서 찾아보았습니다.
파이어폭스(firefox) 디버깅을 통해 해당 기능으로 의심되는 코드의 실행 여부를 확인하였습니다.
2.1.1. applyOptions 함수
@vue/runtime-core/dist
폴더에 위치한runtime-core.esm-bundler.js
에서 다음과 같은 코드를 확인하였습니다.- Vue 컴포넌트를 생성할 때 함께 정의하는 data, computed, methods, watch 등의 속성들을
options
객체에서 필요한 이름으로 디스트럭쳐링(destructuring) 합니다. methods
속성에 정의된 함수들을 반복문으로 통해 댜음과 같은 수행을 처리합니다.- 배포 환경이 아닌 경우 Object 객체의
defineProperty
함수를 통해 Vue 컴포넌트 객체와 대상 함수 객체를 연결합니다. - 배포 환경인 경우 대상 함수 객체의
bind
함수를 사용하여 Vue 컴포넌트 객체를 연결합니다.
- 배포 환경이 아닌 경우 Object 객체의
function applyOptions(instance) {
const options = resolveMergedOptions(instance);
const publicThis = instance.proxy;
const ctx = instance.ctx;
// ... other logics
const {
// state
data: dataOptions, computed: computedOptions, methods, watch: watchOptions, provide: provideOptions, inject: injectOptions,
// lifecycle
created, beforeMount, mounted, beforeUpdate, updated, activated, deactivated, beforeDestroy, beforeUnmount, destroyed, unmounted, render, renderTracked, renderTriggered, errorCaptured, serverPrefetch,
// public API
expose, inheritAttrs,
// assets
components, directives, filters } = options;
// ... other logics
if (methods) {
for (const key in methods) {
const methodHandler = methods[key];
if (isFunction(methodHandler)) {
// In dev mode, we use the `createRenderContext` function to define
// methods to the proxy target, and those are read-only but
// reconfigurable, so it needs to be redefined here
if ((process.env.NODE_ENV !== 'production')) {
Object.defineProperty(ctx, key, {
value: methodHandler.bind(publicThis),
configurable: true,
enumerable: true,
writable: true
});
}
else {
ctx[key] = methodHandler.bind(publicThis);
}
if ((process.env.NODE_ENV !== 'production')) {
checkDuplicateProperties("Methods" /* METHODS */, key);
}
}
else if ((process.env.NODE_ENV !== 'production')) {
warn(`Method "${key}" has type "${typeof methodHandler}" in the component definition. ` +
`Did you reference the function correctly?`);
}
}
}
// ... other logics
}
2.1.2. Call stack and debugging expressions
위의 코드가 실행되는 시점의 콜 스택과 각 변수들이 어떤 값을 가지고 있는지 확인해보았습니다.
Call Stack
createApp
함수를 통해 만들어진app
객체의mount
함수를 타고 올라가면applyOptions
함수를 만날 수 있습니다.
Debugging Expressions
- 반복문에서 사용하는
key
값은 메소드 이름인hideAndVisible
입니다. methodHandler
는hideAndVisible
이름의 함수 객체입니다.publicThis
는 내부에HelloWorld
컴포넌트 객체를 타겟으로 지닌 프록시 객체입니다.
2.2. 그래서 원인은?
코드만 봐서는 크게 문제가 없어 보이지만, 사실 화살표 함수는 bind
함수를 통해 this
를 재정의할 수 없습니다.
MDN
화살표 함수 표현(arrow function expression)은 전통적인 함수 표현(function)의 간편한 대안입니다. 하지만, 화살표 함수는 몇 가지 제한점이 있고 모든 상황에 사용할 수는 없습니다.
- this나 super에 대한 바인딩이 없고, methods 로 사용될 수 없습니다.
- new.target키워드가 없습니다.
- 일반적으로 스코프를 지정할 때 사용하는 call, apply, bind methods를 이용할 수 없습니다.
- 생성자(Constructor)로 사용할 수 없습니다.
- yield를 화살표 함수 내부에서 사용할 수 없습니다.
즉, Vue 프레임워크 내부에서 methods
속성에 정의한 함수의 스코프를 해당 Vue 컴포넌트로 지정할 때 bind
함수를 사용하는데, 화살표 함수로 정의된 경우 정상적으로 스코프가 재정의되지 않아서 문제가 발생한 것 입니다.
운영 환경이 아닌 경우엔 Object
객체의 defineProperty
함수를 사용하지만, 결국 methodHandler
객체의 bind 함수를 사용하기 때문에 화살표 함수로 정의된 경우 정상적인 스코프 연결이 되지 않습니다.
예시 코드
아래 예시 코드를 통해 확인할 수 있습니다.
const module = {
x: 42
};
function normalFunc () {
return this.x
}
console.log('call normalFunc - ', normalFunc()) // undefined
const bounedNormalFunc = normalFunc.bind(module)
console.log('call bounedNormalFunc - ', bounedNormalFunc()) // 42
const arrowFunc = () => {
return this.x
}
console.log('call arrowFunc - ', arrowFunc()) // undefined
const boundedArrowFunc = arrowFunc.bind(module)
console.log('call boundedArrowFunc - ', boundedArrowFunc()) // undefined
결과
> "call normalFunc - " undefined
> "call bounedNormalFunc - " 42
> "call arrowFunc - " undefined
> "call boundedArrowFunc - " undefined
3. 문제 해결
문제 해결 방법은 단순합니다.
methods
속성에 정의할 때 화살표 함수를 사용하지 않아야 합니다.
<template>
<!-- ... vue elements -->
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
visible: "VISIBLE"
}
},
methods: {
// bind error
// hideAndVisible: () => {
// alert(this)
// if (this.visible === "VISIBLE") {
// this.visible = "HIDE"
// } else {
// this.visible = "VISIBLE"
// }
// }
hideAndVisible() {
if (this.visible === "VISIBLE") {
this.visible = "HIDE"
} else {
this.visible = "VISIBLE"
}
}
}
}
</script>
<style scoped>
/* ... styles */
</style>
댓글남기기