Vue 3로의 이주

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

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

Vue 필터

왜?

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

대신 사용할 것

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

이벤트 허브

왜?

Vue 인스턴스에서 $on, $once, $off 메서드가 제거되어서 Vue 3에서 이벤트 허브를 만드는 데 사용할 수 없습니다.

언제 사용할 것인가

이벤트 허브를 사용하지 않는 Vue 앱에서는 절대 필요할 때를 제외하고 새로 추가하지 않도록 노력하세요. 예를 들어, 자식 컴포넌트가 부모의 이벤트에 반응해야 하는 경우, 프롭을 전달하는 것이 좋습니다. 그런 다음 자식 컴포넌트에서 해당 프롭을 감시하여 원하는 부작용을 만들 수 있습니다.

컴포넌트 간 통신(다른 Vue 앱 간)이 필요한 경우, 아마도 허브를 도입하는 것이 올바른 결정일 수 있습니다.

대신 사용할 것

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

이로써 기존 이벤트 허브를 새로운 권장 방식으로 쉽게 이주하거나 새로운 것을 만드는 데 용이해집니다.

import createEventHub from '~/helpers/event_hub_factory';

export default createEventHub();

팩토리로 생성된 이벤트 허브는 이전 방식과 동일한 메서드($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에서 기능적 컴포넌트에 대한 성능 이점은 미미합니다.

기본 프롬프트 함수 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의 업데이트 버전을 사용하기 위한 전략이 필요합니다.

목표

  • 새로운 라이브러리를 지원하기 위해 가능한 한 적은 변경을 기존 코드에 추가해야 합니다. 대신 새로운 코드를 추가하여, 이전 버전과 호환되도록하는 것처럼 동작할 것입니다.
  • 새로운 버전과 예전 버전 간의 전환은 기존 코드에 노출되지 않고 도구 (웹팩 / jest) 내에서 숨겨져야 합니다.
  • 이주에 특화된 모든 페이서드는 향후 이주 단계를 단순화하기 위해 동일한 디렉토리에 있어야 합니다.

단계별 이주

단계별 안내서에서 VueApollo 데모 프로젝트를 이주할 것입니다. 이는 깃랩 프로젝트의 복잡한 도구 설정의 미묘한 점을 피하면서 이주의 특징에 초점을 맞추도록 허용합니다.

프로젝트는 의도적으로 같은 도구를 사용합니다:

  • 웹팩
  • yarn
  • Vue.js + VueApollo

초기 상태

복제한 후 VueApollo 데모yarn serve로 Vue.js 2로 실행하거나 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을 설치해야 합니다(`이 패키지는 옵션 API에 대한 VueApollo 지원을 제공함) 및 응용 프로그램을 변경해야 합니다:

--- 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(의미 상으로)에 Vue 인스턴스를 사용할 수 있습니다. (this.$.appContext.app)

참고: 이 경우에는 비공개 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({
-  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과 같은 팩토리를 선호합니다(previously 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(),
});

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

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

이제 우리는 Vue.js 2 버전과 거의 동일한 코드(가져오기 제외)를 가지고 있습니다. 우리의 퍼사드를 별도의 파일로 이동하고 Vue.js 3 사용 시 vue-apollo가 가져올 때 조건적으로 실행할 수 있도록 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"),
};
}

(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 지원 추가 커밋에서는 단계별 가이드에서 다루지 않는 추가 세부 사항을 볼 수 있습니다.

  • Facedes에 추가적인 imports를 추가해야 할 수 있습니다. (GitLab의 우리 코드에서 ApolloMutation 컴포넌트를 사용합니다.)
  • 웹팩뿐만 아니라 jest에 대한 별칭을 업데이트해야 합니다. 이렇게 하면 테스트에서도 우리 facade를 사용할 수 있습니다.