Vue 3로의 이주

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

Vue 3.x로의 이주를 용이하게 하기 위해 코드베이스에서 다음의 폐기된 기능을 사용하지 못하도록 하는 ESLint 규칙을 추가했습니다.

Vue 필터

왜?

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

대신 사용할 것

컴포넌트의 계산된 속성/메서드나 외부 도우미를 사용하세요.

이벤트 허브

왜?

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

사용 시점

이벤트 허브를 사용하지 않는 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', `Hello! ${props.name}`)
}

성능 향상이 절대적으로 필요하지 않은 한 상태 지향적 컴포넌트를 함수형 컴포넌트로 교체하는 것은 권장되지 않습니다. Vue 3에서 함수형 컴포넌트의 성능 향상은 무의미합니다.

slot 속성과 이전 슬롯 구문

왜?

Vue 2.6에서 slot 속성은 v-slot 지시문으로 대체되기로 했으며 여전히 사용 가능합니다. 때로는 이전 구문을 사용하는 것이 성능 향상 및 유닛 테스트 간단화 때문에 선호됩니다. 그러나 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
  }
})

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에서는 prop 기본값 팩토리가 원시 props을 인수로 받으며 injections에도 접근할 수 있습니다.

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

문제

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

목표

  • 새로운 라이브러리를 지원하도록 기존 코드를 최소한으로 변경해야 합니다. 대신 새로운 코드를 추가하여 새 버전을 이전 버전과 호환되게 하는 퍼사드로 작동해야 합니다
  • 새 버전과 이전 버전 간의 전환은 도구(웹팩/jest) 내에서 숨겨져 있어야 하며 코드에 노출되어서는 안 됩니다
  • 이주를 위한 모든 퍼사드는 나중의 이주 단계를 단순화하기 위해 동일한 디렉터리에 있어야 합니다

단계별 이주

간단한 안내서에서는 VueApollo 데모 프로젝트를 이주할 것입니다. 이로써 GitLab 프로젝트에서의 복잡한 도구 설정의 세부 사항을 피하고 이주의 구체적인 점에 초점을 맞출 수 있습니다. 프로젝트는 의도적으로 GitLab과 동일한 도구를 사용합니다:

  • 웹팩
  • 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을 스텁(spy)하는 것은 VueApollo 관련 문제를 해결하지만 여전히 특정 시나리오에서 반응성을 잃게 만들기 때문에 업그레이드가 필요합니다.

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

VueApollo v4 설치 가이드에 따르면 @vue/apollo-option을 설치해야 합니다(VueApollo 옵션 API 지원 패키지). 그리고 애플리케이션을 변경해야 합니다:

--- 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)

이 동작을 backport하기 위해 다음 지식이 필요합니다:

  • Vue 인스턴스에서 $options로 제공된 추가 옵션에 액세스할 수 있으므로 추가 apolloProviderthis.$options.apolloProvider로 보입니다
  • Vue.js 3에서 현재 app(의미상의)에 액세스할 수 있습니다. Vue 인스턴스에서는 this.$.appContext.app를 사용할 수 있습니다
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(),
 });

-new Vue({
-  el: "#app",
-  apolloProvider,
-  render(h) {
+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 자체)는 클래스 대신 createApp과 같은 팩토리를 사용하는 것을 선호합니다(이전에는 new Vue)

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 버전과 거의 동일한 코드 (가져오기를 제외한)가 있습니다. 우리는 퍼사드를 별도 파일로 이동하고 vue-apollo가 가져온 경우에만 webpack에서 조건부로 실행되도록 설정할 것입니다:

```diff — 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”), }; }

(index.js에서 VueApollo 클래스를 vue3compat/vue-apollo.js로 이동하고 기본 내보내기는 명확성을 위해 생략)

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

단계 5. 결과 확인

이 시점에서 Vue.js 2 버전을 yarn serve로, Vue.js 3 버전을 yarn serve:vue3로 다시 실행할 수 있어야합니다. 최종 MR에서 이전 단계의 모든 변경 사항이 표시되며, index.js(응용 프로그램 코드)에는 변경 사항이 없습니다. 이것이 우리의 목표였습니다.

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

VueApollo v4 지원 추가 커밋에서는 단계별 가이드에서 다루지 않는 추가 세부 사항을 볼 수 있습니다:

  • 퍼사드에 추가적인 임포트를 추가해야 할 수 있습니다(GitLab의 우리 코드는 ApolloMutation 컴포넌트를 사용합니다).
  • 우리의 테스트도 퍼사드를 사용할 수 있도록 웹팩 뿐만 아니라 jest에 대한 별칭도 업데이트해야 합니다.