Vue 3으로의 마이그레이션

Vue 2에서 3으로의 마이그레이션은 에픽 &6252에서 추적됩니다.

Vue 3.x로의 마이그레이션을 쉽게 하기 위해, 코드베이스에서 다음과 같은 사용 중단된 기능을 사용하지 못하도록 하는 ESLint 규칙을 추가했습니다.

Vue 필터

이유는 무엇인가요?

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

대신 무엇을 사용해야 하나요?

컴포넌트의 계산된 속성/메서드 또는 외부 헬퍼를 사용하세요.

이벤트 허브

이유는 무엇인가요?

$on, $once, 및 $off 메서드는 Vue 인스턴스에서 제거되었습니다. 따라서 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 지시어를 지향하도록 사용 중단되었습니다. 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>
        헤더 내용
      </template>
    </some-child-component>
  </div>
</template>
// MyAwesomeComponent.spec.js

import SomeChildComponent from '~/some_child_component.vue'

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

Props 기본 함수 this 접근

이유

Vue 3에서 props 기본 값 팩토리 함수는 더 이상 this(컴포넌트 인스턴스)에 접근할 수 없습니다.

대신 사용할 것

다른 props에서 원하는 값을 해결하는 계산 속성을 작성하세요. 이는 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를 인수로 받아들이며, injection에도 접근할 수 있습니다.

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

문제

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

목표

  • 새로운 라이브러리를 지원하기 위해 기존 코드에 가능한 한 적은 변경을 추가해야 합니다. 대신, facade 역할을 하는 새로운 코드를 추가해야 합니다. 이것이 새로운 버전을 이전 버전과 호환되도록 합니다.

  • 새로운 버전과 이전 버전 간의 전환은 도구(webpack / jest) 내부에서 숨겨져야 하며, 코드에 노출되지 않아야 합니다.

  • 마이그레이션과 관련된 모든 facade는 향후 마이그레이션 단계를 단순화하기 위해 동일한 디렉터리에 있어야 합니다.

단계별 마이그레이션

단계별 가이드에서는 VueApollo 데모 프로젝트를 마이그레이션합니다. 이를 통해 GitLab 프로젝트의 복잡한 도구 설정의 뉘앙스를 피하면서 마이그레이션 구체사항에 집중할 수 있습니다. 이 프로젝트는 의도적으로 GitLab과 동일한 도구를 사용합니다.

  • webpack
  • yarn
  • Vue.js + VueApollo

초기 상태

클론 직후, Vue.js 2를 사용하는 VueApollo 데모yarn serve로 실행하거나 Vue.js 3(compat 빌드)를 사용하여 yarn serve:vue3를 실행할 수 있습니다. 그러나 후자는 즉시 충돌합니다:

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을 설치해야 하며(이 패키지는 Options API에 대한 VueApollo 지원을 제공합니다), 애플리케이션에 다음과 같은 변경을 해야 합니다.

--- a/src/index.js
+++ b/src/index.js
-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 인스턴스에서 현재 app(Vue.js 3 의미)은 this.$.appContext.app을 통해 액세스할 수 있습니다.

참고: 우리는 이 경우 비공식 Vue.js 3 API에 의존하고 있습니다. 그러나 @vue/compat 빌드는 3.2.x 브랜치에서만 사용 가능할 것으로 예상되므로 이 API가 변경될 위험이 줄어듭니다.

이 지식을 바탕으로, Vue2에서 도구를 가능한 빨리 초기화할 수 있습니다 - beforeCreate() 생명주기 훅에서:

--- a/src/index.js
+++ b/src/index.js
-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 자체)는 클래스를 대신하여 createApp과 같은 팩토리 사용을 선호합니다 (이전의 new Vue).

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

-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가 Vue.js 3를 사용할 때 임포트되면 조건부로 webpack이 실행되도록 설정할 것입니다:

--- 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에서 vue3compat/vue-apollo.jsVueApollo 클래스를 기본 내보내기로 이동하는 것은 명확성을 위해 생략되었습니다.)

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

단계 5. 결과 관찰하기

이 시점에서 둘 다 - yarn serve를 사용하여 Vue.js 2 버전과 yarn serve:vue3를 사용하여 Vue.js 3 버전을 다시 실행할 수 있어야 합니다.

최종 MR에서는 이전 단계의 모든 변경사항이 index.js(응용 프로그램 코드)에 변경이 없음을 표시하며, 이것이 우리의 목표였습니다.

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

VueApollo v4 지원 추가 커밋에서는 단계별 가이드에서 다루지 않은 추가적인 뉘앙스를 볼 수 있습니다:

  • 우리 파사드에 추가적인 임포트를 추가해야 할 수도 있습니다(우리의 GitLab 코드에서는 ApolloMutation 컴포넌트를 사용합니다).

  • 우리의 테스트가 우리의 파사드를 소모할 수 있도록 webpack뿐만 아니라 jest에 대한 별칭도 업데이트해야 합니다.