Vue 3로의 이주

Vue 2에서 3으로의 이주는 epic &6252에서 추적됩니다.

Vue 3.x로의 이주를 용이하게 하기 위해, 코드베이스에서 사용 중인 다음과 같은 폐기된 기능을 사용하지 못하게 하는 ESLint rules를 추가했습니다.

Vue 필터

왜?

Vue 3 API에서 필터는 완전히 제거되었습니다.

대신 사용할 것

컴포넌트의 계산된 속성 / 메소드 또는 외부 도우미.

이벤트 허브

왜?

Vue 3에서는 Vue 인스턴스에서$on, $once, $off 메서드가 제거되었으므로 이제 이벤트 허브를 만들 수 없습니다.

언제 사용할 것인가

이벤트 허브를 사용하지 않는 Vue 앱을 사용 중이라면 절대적으로 필요하지 않은 한 새로운 이벤트 허브를 추가하지 않도록 노력하세요. 예를 들어, 자식 컴포넌트가 부모의 이벤트에 반응해야 하는 경우 부모로부터 prop을 전달하는 것이 좋습니다. 그런 다음, 자식 컴포넌트에서 해당 prop에 대한 watch 속성을 사용하여 원하는 부작용을 만들어냅니다.

다른 컴포넌트 간 통신이 필요하다면(다른 Vue 앱 간), 그럴 때는 새 허브를 추가하는 것이 옳은 결정일 수 있습니다.

대신 사용할 것

새로 mitt와 유사한 이벤트 허브를 인스턴스화하는 데 사용할 수 있는 팩토리를 만들었습니다.

기존 이벤트 허브를 새로운 권장된 방식으로 이주하거나 새 이벤트 허브를 만드는 것을 쉽게 만들어줍니다.

import createEventHub from '~/helpers/event_hub_factory';

export default createEventHub();

팩토리로 생성된 이벤트 허브는 이전의 Vue 2 이벤트 허브와 같은 메서드($on, $once, $off$emit)를 노출하여 이전 방식과 호환되도록 만들었습니다.

<template functional>

왜?

Vue 3에서{ functional: true } 옵션이 제거되었으며<template functional>은 더 이상 지원되지 않습니다.

대신 사용할 것

함수형 컴포넌트는 평범한 함수로 작성해야 합니다.

import { h } from 'vue'

const FunctionalComp = (props, slots) => {
  return h('div', `안녕하세요! ${props.name}`)
}

성능 향상이 반드시 필요한 경우가 아니라면 상태 유지형 컴포넌트를 함수형 컴포넌트로 대체하는 것은 권장되지 않습니다. Vue 3에서 함수형 컴포넌트의 성능 향상은 미미합니다.

slot 속성의 이전 구문

왜?

Vue 2.6에서 slot 속성은 v-slot 지시문을 선호하여 폐기되었습니다. slot 속성 사용은 여전히 허용되며 때로는 단순한 유닛 테스트를 위해 사용하는 경우가 있습니다(shallowMount에서 슬롯이 렌더링됩니다). 그러나 Vue 3에서는 이전 구문을 더 이상 사용할 수 없습니다.

대신 사용할 것

v-slot 지시문 구문. shallowMount에서 슬롯을 렌더링하기 위해 자식 컴포넌트를 명시적으로 스텁해야 합니다.

<!-- MyAwesomeComponent.vue -->
<script>
import SomeChildComponent from './some_child_component.vue'

export default {
  components: {
    SomeChildComponent
  }
}

</script>

<template>
  <div>
    <h1>Hello GitLab!</h1>
    <some-child-component>
      <template #header>
        Header content
      </template>
    </some-child-component>
  </div>
</template>
// MyAwesomeComponent.spec.js

import SomeChildComponent from '~/some_child_component.vue'

shallowMount(MyAwesomeComponent, {
  stubs: {
    SomeChildComponent
  }
})

Props 기본 함수 this 접근

왜?

Vue 3에서는 prop 기본 값 공장 함수가 더 이상 this(컴포넌트 인스턴스)에 접근할 수 없습니다.

대신 사용할 것

다른 prop에서 원하는 값을 해결하는 계산된 prop을 작성하십시오. 이는 Vue 2 및 3 모두에서 작동합니다.

<script>
export default {
  props: {
    metric: {
      type: String,
      required: true,
    },
    title: {
      type: String,
      required: false,
      default: null,
    },
  },
  computed: {
    actualTitle() {
      return this.title ?? this.metric;
    },
  },
}

</script>

<template>
  <div>{{ actualTitle }}</div>
</template>

Vue 3에서, props 기본 값 공장은 원시 props을 인수로 받으며 인젝션에도 액세스할 수 있습니다.

@vue/compat와 호환되지 않는 라이브러리 처리

문제

일부 라이브러리는 Vue.js 2 내부에 의존합니다. 이러한 라이브러리는 @vue/compat과 함께 작동하지 않을 수 있으므로 현재 코드베이스와 호환성을 유지하면서 Vue.js 3의 업데이트된 버전을 사용하기 위한 전략이 필요합니다.

목표

  • 새 라이브러리를 지원하기 위해 기존 코드에 가능한 적은 변경을 추가해야 합니다. 대신 새로운 코드를 추가하고 이전 버전과 호환되도록 작동할 것으로 하는 퍼사드 역할을 하는 코드를 추가해야 합니다.
  • 새 버전과 이전 버전 간의 전환은 도구(웹팩 / 제스트) 내부에서만 처리되어야 하며 코드에 노출되지 않아야 합니다.
  • 이주 관련 특정 퍼사드는 모두 동일한 디렉터리에 있어야 하여 향후 이주 단계를 간소화해야 합니다.

단계별 이주

이 단계별 가이드에서는 VueApollo 데모 프로젝트를 이주할 것입니다. 이를 통해 복잡한 도구 설정의 세부 사항을 회피하면서 이주 구체적인 사항에 중점을 둘 수 있게 됩니다. 프로젝트는 의도적으로 같은 도구를 사용합니다:

  • 웹팩
  • yarn
  • Vue.js + VueApollo

초기 상태

클론한 직후에 yarn serve를 사용하여 Vue.js 2로 VueApollo 데모를 실행할 수 있으며, yarn serve:vue3을 사용하여 Vue.js 3(compat 빌드)로 실행할 수 있습니다. 그러나 후자는 즉시 다음과 같은 오류가 발생합니다.

Uncaught TypeError: Cannot read properties of undefined (reading 'loading')

VueApollo v3(사용된 Vue.js 2용)이 Vue.js compat에서 초기화에 실패합니다.

note
데모 프로젝트에서Vue.version을 스텁하면 VueApollo 관련 문제를 해결할 수 있지만 여전히 특정 시나리오에서 반응이 상실될 수 있으므로 업그레이드가 필요합니다.

단계 1. 라이브러리 설명서에 따라 업그레이드 수행

VueApollo v4 설치 가이드에 따르면 @vue/apollo-option를 설치해야 하며 응용 프로그램에 변경을 가해야 합니다.

--- a/src/index.js
+++ b/src/index.js
@@ -1,19 +1,17 @@
-import Vue from "vue";
-import VueApollo from "vue-apollo";
+import { createApp, h } from "vue";
+import { createApolloProvider } from "@vue/apollo-option";
 
 import Demo from "./components/Demo.vue";
 import createDefaultClient from "./lib/graphql";

-Vue.use(VueApollo);
-
-const apolloProvider = new VueApollo({
+const apolloProvider = createApolloProvider({
   defaultClient: createDefaultClient(),
 });

-new Vue({
-  el: "#app",
-  apolloProvider,
-  render(h) {
+const app = createApp({
+  render() {
     return h(Demo);
   },
 });
+app.use(apolloProvider);
+app.mount("#app");

이 변경 사항은 데모 프로젝트의 01-upgrade-vue-apollo 브랜치에서 확인할 수 있습니다.

단계 2. Vue.js 2 및 3에서 애플리케이션을 확장하는 방법의 차이 해결

Vue.js 2에서 VueApollo와 같은 도구는 “느긋하게” 초기화됩니다:

// 우리는 VueApollo "핸들러"를 나중에 어떤 데이터를 처리할지 담당하도록 등록하고 있습니다
Vue.use(VueApollo)
// ...
// apolloProvider가 앱 생성 시 제공되며,
// 이전에 등록된 VueApollo가 이를 처리하게 됩니다
new Vue({ /- ... */, apolloProvider })

Vue.js 3에서는 두 단계가 하나로 Merge되었습니다. 즉, 즉시 핸들러를 등록하고 구성을 전달합니다:

app.use(apolloProvider)

이러한 동작을 되감아야 할 경우 다음 지식이 필요합니다:

  • Vue 인스턴스로 제공된 추가 옵션에는 $options을 통해 액세스할 수 있으므로 추가 apolloProviderthis.$options.apolloProvider로 볼 수 있습니다.
  • Vue.js 3에서 현재 ‘app’에는 this.$.appContext.app을 통해 Vue 인스턴스에서 액세스할 수 있습니다.
note
이 경우에는 비공개 Vue.js 3 API에 의존하고 있습니다. 그러나 @vue/compat 빌드가 3.2.x 브랜치에서만 사용할 수 있을 것으로 기대되므로 이 API가 변경될 위험을 줄였습니다.

이러한 지식을 바탕으로 Vue2에서 우리의 도구 초기화를 가능한 한 빨리 이동할 수 있습니다. 이를 위해 beforeCreate() 라이프사이클 훅을 사용합니다:

--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,4 @@
-import { createApp, h } from "vue";
+import Vue from "vue";
 import { createApolloProvider } from "@vue/apollo-option";
 
 import Demo from "./components/Demo.vue";
@@ -8,10 +8,13 @@ const apolloProvider = createApolloProvider({
   defaultClient: createDefaultClient(),
 });

-const app = createApp({
-  render() {
+new Vue({
+  el: "#app",
+  apolloProvider,
+  render(h) {
     return h(Demo);
   },
+  beforeCreate() {
+    this.$.appContext.app.use(this.$options.apolloProvider);
+  },
 });
-app.use(apolloProvider);
-app.mount("#app");

이 변경 사항은 데모 프로젝트의 02-bring-back-new-vue 브랜치에서 확인할 수 있습니다.

단계 3. VueApollo 클래스 재생성

Vue.js 3 라이브러리(및 Vue.js 자체)는 이전의 new Vue 대신 createApp과 같은 팩토리를 사용하는 것을 선호합니다.

VueApollo 클래스는 두 가지 목적으로 사용되었습니다:

  • apolloProvider 생성을 위한 생성자
  • 컴포넌트에서 apollo 관련 로직을 설치

우리는 코드베이스에 존재했던 Vue.use(VueApollo) 코드를 활용하여 여기에 mixin을 숨기고 앱 코드 수정을 피할 수 있습니다:

--- a/src/index.js
+++ b/src/index.js
@@ -4,7 +4,26 @@ import { createApolloProvider } from "@vue/apollo-option";
 import Demo from "./components/Demo.vue";
 import createDefaultClient from "./lib/graphql";

-const apolloProvider = createApolloProvider({
+class VueApollo {
+  constructor(...args) {
+    return createApolloProvider(...args);
+  }
+
+  // Vue.use에 의해 호출됨
+  static install() {
+    Vue.mixin({
+      beforeCreate() {
+        if (this.$options.apolloProvider) {
+          this.$.appContext.app.use(this.$options.apolloProvider);
+        }
+      },
+    });
+  }
+}
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
   defaultClient: createDefaultClient(),
 });

@@ -14,7 +33,4 @@ new Vue({
   render(h) {
     return h(Demo);
   },
-  beforeCreate() {
-    this.$.appContext.app.use(this.$options.apolloProvider);
-  },
 });

이 변경 사항은 데모 프로젝트의 03-recreate-vue-apollo 브랜치에서 확인할 수 있습니다.

단계 4. VueApollo 클래스를 별도의 파일로 이동하고 별칭 설정

이제 우리는 Vue.js 2 버전과 거의 동일한 코드(임포트를 제외하고)를 가지고 있습니다. 우리는 패러드를 별도의 파일로 이동하고 webpackvue-apollo가 import된 경우에만 조건적으로 실행하도록 설정할 것입니다:

--- a/src/index.js
+++ b/src/index.js
@@ -1,5 +1,5 @@
 import Vue from "vue";
-import { createApolloProvider } from "@vue/apollo-option";
+import VueApollo from "vue-apollo";
 
 import Demo from "./components/Demo.vue";
 import createDefaultClient from "./lib/graphql";
diff --git a/webpack.config.js b/webpack.config.js
index 6160d3f..b8b955f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -12,6 +12,7 @@ if (USE_VUE3) {
   
   VUE3_ALIASES = {
     vue: "@vue/compat",
+    "vue-apollo": path.resolve("src/vue3compat/vue-apollo"),
   };
 }

(VueApollo 클래스를 index.js에서 vue3compat/vue-apollo.js로 옮기고 기본 내보내기는 명확성을 위해 생략되었습니다)

이 변경 사항은 데모 프로젝트의 04-add-webpack-alias 브랜치에서 확인할 수 있습니다.

단계 5. 결과 확인

이 시점에서 yarn serve를 사용하여 모두 Vue.js 2 버전 및 yarn serve:vue3를 사용하여 Vue.js 3 버전을 다시 실행할 수 있어야 합니다. 이전 단계의 모든 변경 사항이 표시되는 최종 MR은 (앱 코드인) index.js에 대한 변경 사항이 없어야 했던 것이 목표였습니다

GitLab 프로젝트에서 이 접근 방식 적용

VueApollo v4 지원 추가 커밋에서 단계별 안내에서 다루지 않은 추가 세부 정보를 볼 수 있습니다:

  • 패러드에 추가적인 import를 추가해야 할 수 있습니다(GitLab의 우리 코드는 ApolloMutation 컴포넌트를 사용합니다)
  • webpack 뿐만 아니라 jest에 대한 별칭도 업데이트해야 합니다. 이렇게 해야 테스트에서도 패러드를 소비할 수 있습니다.