Vue 3로의 이전

Vue 2에서 3으로의 이전은 에픽 &6252에서 추적됩니다.

Vue 3.x로의 이전을 용이하게 하기 위해 ESLint 규칙을 추가했습니다. 이를 통해 코드베이스에서 사용 중인 다음과 같은 폐기된 기능의 사용을 방지합니다.

Vue 필터

왜?

Vue 3 API에서 필터가 제거되었습니다.

대신 사용할 것

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

이벤트 허브

왜?

Vue 인스턴스에서 $on, $once, 및 $off 메서드가 제거되었으므로, Vue 3에서 이벤트 허브를 생성하는 데 사용할 수 없습니다.

언제 사용할 것인가

이벤트 허브를 사용하지 않는 Vue 앱에서 새 이벤트 허브를 추가하는 것을 반드시 필요한 경우를 제외하고 피하십시오. 예를 들어, 자식 컴포넌트가 부모의 이벤트에 반응해야 하는 경우, 속성을 내려보내고 해당 속성의 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 지시문을 선호하는 방향으로 새로운 슬롯 문법으로 대체되었습니다. 여전히 slot 속성을 사용할 수 있으며 때로는 이를 사용하는 것이 좋습니다(예전 문법을 사용하면 슬롯이 shallowMount에서 렌더링됩니다). 그러나 Vue 3에서는 예전 문법을 더 이상 사용할 수 없습니다.

대신 사용할 것

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

<!-- 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에서는 프롭스의 기본값 팩토리 함수가 더 이상 this(컴포넌트 인스턴스)에 접근할 수 없습니다.

대신 사용할 것

다른 프롭스에서 원하는 값을 계산하는 컴퓨티드 프로퍼티를 작성하세요. 이는 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에서, 프롭스의 기본값 팩토리는 원시 프롭스를 인수로 전달받고 인젝션에도 접근할 수 있습니다.

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

문제

일부 라이브러리는 Vue.js 2 내부에 의존하고 있습니다. 이러한 라이브러리는 @vue/compat와 호환되지 않을 수 있으므로 Vue.js 3와의 호환성을 유지하면서 업데이트된 버전을 사용할 수 있는 전략이 필요합니다.

목표

  • 기존 코드에 가능한 한 적은 변경을 추가하여 새 라이브러리를 지원해야 합니다. 대신, 이전 버전과 호환되도록 새로운 코드를 추가하여 이를 facade로 구현해야 합니다.
  • 새로운 버전과 이전 버전 간의 전환은 빌드 도구 (webpack / jest) 내에서 숨겨져 있어야 하며 코드에 노출되지 않아야 합니다.
  • 마이그레이션에 특화된 facades는 향후 마이그레이션 단계를 단순화하기 위해 동일한 디렉터리에 있어야 합니다.

단계별 마이그레이션

이 단계별 가이드에서는 VueApollo 데모 프로젝트를 마이그레이션할 것입니다. 이를 통해 GitLab 프로젝트에서 복잡한 도구 설정의 세부 사항을 피하고 마이그레이션 특이점에 집중할 수 있습니다. 이 프로젝트는 의도적으로 다음과 같은 도구를 사용합니다:

  • webpack
  • 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에서 초기화에 실패합니다.

참고: 데모 프로젝트에서 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에서는 두 단계가 하나로 병합되어 바로 핸들러를 등록하고 구성을 전달합니다:

app.use(apolloProvider)

이 동작을 백포팅하기 위해 다음에 대한 지식이 필요합니다:

  • Vue 인스턴스의 추가 옵션은 $options를 통해 액세스할 수 있으므로 추가 apolloProviderthis.$options.apolloProvider로 볼 수 있습니다.
  • Vue.js 3에서 현재 app(의미상)은 this.$.appContext.app을 통해 Vue 인스턴스에서 액세스할 수 있습니다.

참고: 이 경우에는 비공개 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) 코드를 활용하여 마이크스인을 여기에 숨길 수 있고 앱 코드를 수정하지 않을 수 있습니다:

--- 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";

+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 = createApolloProvider({
+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 버전과 거의 동일한 코드(임포트를 제외한)를 가지고 있습니다. 저희는 facade를 별도의 파일로 이동시키고, vue-apollo를 사용하여 webpack이 조건부로 실행되도록 설정할 것입니다 Vue.js 3를 사용할 때 vue-apollo가 임포트 된 경우:

--- 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. 결과 확인

이 시점에서 두 가지 모두 가능해야 합니다 - yarn serve로 Vue.js 2 버전을 실행하거나 yarn serve:vue3로 Vue.js 3 버전을 실행할 수 있어야 합니다. 모든 변경 사항을 담은 최종 MR에서는 (우리의 목표였던) index.js의 변경 사항이 없어야 합니다.

GitLab 프로젝트에서 이 접근법 적용하기

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

  • 패러드에 추가적인 임포트를 추가해야 할 수 있습니다 (우리의 GitLab 코드에서 ApolloMutation 컴포넌트를 사용합니다)
  • 웹팩 뿐만 아니라 jest를 위해 별칭을 업데이트해야 합니다. 이러한 조치로 테스트에서도 우리의 facade를 사용할 수 있습니다.