InfoGrab DocsInfoGrab Docs

Vue 3 마이그레이션

요약

Vue 2에서 Vue 3으로의 마이그레이션은 에픽 &6252에서 추적됩니다. Vue 3.x로의 마이그레이션을 쉽게 하기 위해, 코드베이스에서 다음의 더 이상 사용되지 않는 기능을 쓰지 못하도록 막는 ESLint 규칙을 추가했습니다.

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

Vue 3.x로의 마이그레이션을 쉽게 하기 위해, 코드베이스에서 다음의 더 이상 사용되지 않는 기능을 쓰지 못하도록 막는 ESLint 규칙을 추가했습니다.

GitLab에서 Vue 3 (@vue/compat) 사용 가능#

GitLab 프론트엔드 팀은 GDK와 같은 개발 환경에서 Vue 3 (@vue/compat)을 활성화했습니다. 아직 프로덕션에 사용할 준비는 되지 않았지만, 로컬에서 opt-in하여 클라이언트 코드가 Vue 3과 순방향 호환되는지 확인할 수 있습니다.

동작 방식 빌드 도구(Vite 또는 Webpack)가 VUE_VERSION=3 환경 변수를 감지하면, 모듈 에일리어싱을 사용해 Vue 자체를 포함한 특정 의존성을 Vue 3 호환 버전으로 교체합니다.

이 대체 라이브러리 중 일부는 팀에서 직접 유지 관리합니다. 이들은 기존 라이브러리를 감싸는 얇은 래퍼 역할을 하며, 컨슈머 코드를 전혀 변경하지 않고도 Vue 3과 호환되게 합니다.

GDK에서 Vue 3 (@vue/compat) 설정#

이 가이드는 GitLab Development Kit(GDK)에서 Vite를 빌드 도구로 사용해 Vue 3을 구성하는 과정을 안내합니다.

사전 요구 사항#

  • GDK 설치 및 구성 완료
  • Vue.js와 Vite에 대한 기본적인 이해
  • GDK 환경에 Vite 구성(GDK Vite Settings 참고)

초기 설정#

Vue 버전 간 전환#

Vue 2와 Vue 3 사이를 전환하려면 다음 단계를 따릅니다.

  1. 원하는 Vue 버전을 설정합니다.

    gdk config set vite.vue_version 3  # or 2
    
  2. GDK를 재구성합니다.

    gdk reconfigure
    
  3. GDK를 재시작합니다.

    gdk restart # or `gdk start` if running for the first time
    

중요: Vue 버전 전환에 문제가 있으면 yarn clean 또는 gdk kill vite로 캐시를 지울 수 있습니다.

설정 확인#

gdk.yml 파일을 확인해 Vite 구성을 검증할 수 있습니다.

gdk config get vite

활성화 상태와 Vue 버전을 포함한 현재 Vite 설정이 표시됩니다. GDK도 실행 중이어야 합니다.

---
enabled: true
hot_module_reloading: true
https:
  enabled: true
port: 3038
vue_version: 3

문제 해결#

일반적인 디버깅#

문제가 발생하면 먼저 Vite 로그를 확인합니다.

gdk tail vite

이 명령은 실시간 Vite 출력과 오류 메시지를 보여 주며, 문제를 파악하는 데 도움이 됩니다.

버전 전환 후 빌드 오류#

Vue 버전을 전환한 뒤 빌드 오류가 발생하면 다음을 수행합니다.

  1. yarn clean으로 Vite 캐시를 지웠는지 확인합니다

  2. node_modules를 지우고 의존성을 다시 설치해 봅니다.

    rm -rf node_modules
    yarn install
    

Vite가 시작되지 않는 경우#

Vite 시작에 실패하면 다음을 확인합니다.

  • vite.enabled가 true로 설정되어 있는지 확인합니다
  • Node.js 버전이 Vite의 요구 사항을 충족하는지 확인합니다
  • GDK 로그에서 구체적인 오류 메시지를 확인합니다

추가 리소스#

호환성 변경 사항#

아래 변경 사항은 이 마이그레이션에서 가장 자주 마주치는 것들이며, 전체 범위는 아닙니다. 애플리케이션은 임포트하는 어떤 것 때문에도 깨질 수 있으므로, 이 목록은 체크리스트가 아니라 출발점으로 봅니다. config/helpers/context_aliases_shared.js에서 에일리어싱되는 라이브러리, 예를 들어 vuex, vue-router, vue-apollo, portal-vue, vuedraggable, 가상 스크롤러는 Vue 3 심을 거치므로 마이그레이션에 가장 민감합니다.

Vue 필터#

이유

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

대신 사용할 것

컴포넌트의 computed 속성이나 메서드, 또는 외부 헬퍼를 사용합니다.

이벤트 허브#

이유

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

대신 사용할 것

다른 props에서 원하는 값을 도출하는 computed 속성을 작성합니다. 이 방식은 Vue 2와 Vue 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.observable#

이유

Vue.observable은 이를 생성한 Vue 버전에 묶인 반응형 상태를 만듭니다. Vue 2/Vue 3 하이브리드 인펙션 시스템에서는 모듈이 Vue 버전마다 하나씩 복제될 수 있습니다. 이 모듈들이 Vue.observable()을 사용하면 각 복사본이 서로 분리된 반응형 객체를 만들기 때문에, 한쪽의 상태 변경이 다른 쪽에는 보이지 않습니다.

대신 사용할 것

~/lib/utils/observable의 observable()을 사용합니다.

import { observable } from '~/lib/utils/observable';

// Before
export const state = Vue.observable({ count: 0 });

// After
export const state = observable('unique_key', { count: 0 });

observable(key, defaults) 함수의 동작은 다음과 같습니다.

  • key를 키로 하는 전역 레지스트리에 단일 정본 상태를 저장합니다
  • 내부적으로 Vue.observable()을 통해 Vue 컨텍스트별 반응형 미러를 생성합니다
  • 모든 Vue 버전의 미러에 쓰기를 동기화하는 Proxy를 반환합니다
  • 평탄한 객체, getter, 메서드를 지원합니다

key는 고유한 문자열 식별자여야 합니다(예: 'super_sidebar_state'). 이 키가 두 모듈 복사본이 동일한 기반 상태를 공유하도록 보장합니다.

ESLint 규칙(no-restricted-properties)이 이를 강제하며, Vue.observable을 직접 사용하면 린트 오류가 발생합니다.

제약 사항

  • 평탄한 객체만 지원: state.nested.prop = value나 state.array.push(item) 같은 중첩 변경은 Vue 버전 간에 동기화되지 않습니다. 대신 최상위 속성을 교체하는 방식으로 리팩터링합니다.

    // Instead of: state.items.push(newItem)
    state.items = [...state.items, newItem];
    
    // Instead of: state.config[key] = value
    state.config = { ...state.config, [key]: value };
    

모듈 스코프 싱글턴#

마이그레이션된 페이지는 "인펙션" 메커니즘을 통해 일부 모듈을 Vue 2와 Vue 3 양쪽으로 로드할 수 있습니다. 각 Vue 버전이 같은 모듈을 실행하므로, 상태를 가진 모듈은 복제될 수 있고 코드를 실행하는 모듈은 두 번 실행될 수 있습니다. 예를 들어 이벤트 핸들러가 두 번 등록됩니다. 이 복제를 다루기 위해 이런 모듈을 싱글턴이라고 부릅니다.

복제는 발견하기 어렵습니다. 컨슈머는 모듈이 복제되었다는 사실을 알지 못합니다. 오래된 값을 읽게 되지만, 어떤 오류도 보고되지 않습니다.

발생하는 오류#

이런 싱글턴이 복제되는 것을 막기 위해 번들링 job(compile-production-assets)을 실패시키는 안전장치를 만들어 두었습니다. 복제가 발생하면 해당 job이 실패합니다.

스캐너는 다음과 같은 출력을 표시합니다.

[vue3-infection-scanner] Duplicated modules detected: 1 module(s) on 14 page state(s), 14 finding(s).

  pages.dashboard.issues (flag on)
    roots: app/assets/javascripts/main.js
           app/assets/javascripts/pages/dashboard/issues/index.js
           app/assets/javascripts/entrypoints/super_sidebar.js

    app/assets/javascripts/graphql_shared/issuable_default_client.js
       holds: apollo-client
       sink:  (none: this copy has no Vue 3 ancestor)
          V2  app/assets/javascripts/entrypoints/super_sidebar.js
          V2  app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
          V2  app/assets/javascripts/graphql_shared/issuable_client.js
          V2  app/assets/javascripts/graphql_shared/issuable_default_client.js

각 필드의 의미는 다음과 같습니다.

  • holds는 모듈에서 발견된 상태의 종류입니다.
  • 체인은 각 레인이 모듈에 도달하는 경로를 보여 줍니다. V2 또는 V3는 각 단계의 레인을 표시합니다.
  • sink는 서브트리를 Vue 2로 되돌린 모듈이 있을 경우 그 모듈의 이름입니다. 번들러가 exposedToVue를 따라간 뒤에는 INFECTION_BLOCKLIST에 있는 모듈만 sink가 될 수 있습니다.
  • 이 예시에서 roots 목록은 일부만 표시되어 있습니다.

해결 방법#

  1. 일반적인 해결: 상태를 Vue에 노출되지 않는 별도 모듈로 옮깁니다. 이 모듈은 아무것도 임포트하지 않거나, Vue에 노출되지 않는 모듈만 임포트합니다. 번들러는 이런 모듈을 절대 복제하지 않으므로 모든 레인이 하나의 복사본을 공유합니다. app/assets/javascripts/lib/graphql_pending_requests.js와 app/assets/javascripts/graphql_shared/issuable_client_state.js가 기존 예시입니다.
  2. 대안: 상태 모듈이 임포트를 유지해야 한다면, config/helpers/context_aliases_shared.js의 INFECTION_BLOCKLIST에 추가합니다. 그러면 모든 레인이 하나의 복사본을 공유하지만, 그 모듈 아래의 서브트리는 Vue 3 페이지 안에서 Vue 2로 실행됩니다. app/assets/javascripts/lib/utils/breadcrumbs_state.js가 기존 예시입니다.
  3. 평탄한 반응형 상태에는 ~/lib/utils/observable의 observable()을 사용합니다.

블록리스트는 Vue 버전에 특화된 설정을 실행하지 않는 모듈에만 사용합니다. Pinia 인스턴스나 VueApollo 프로바이더는 이를 만든 Vue 버전에 묶이므로, 하나의 공유 복사본은 잘못된 버전에 묶입니다.

@vue/compat에서 동작하지 않는 라이브러리 처리#

문제

일부 라이브러리는 Vue.js 2 내부 구현에 의존합니다. 이런 라이브러리는 @vue/compat에서 동작하지 않을 수 있어, 호환성 레이어로 어댑터나 대체 구현을 추가했습니다.

목표

  • 새 라이브러리를 지원하기 위해 기존 코드에 가하는 변경은 최소화합니다. 대신 파사드 역할을 하는 새 코드를 추가해 새 버전이 이전 버전과 호환되게 합니다
  • 새 버전과 이전 버전 사이의 전환은 툴링(webpack / jest) 내부에 숨기고 코드에 노출하지 않습니다
  • 마이그레이션에 한정된 파사드는 모두 같은 디렉터리에 두어 이후 마이그레이션 단계를 단순하게 합니다

Vue 3으로 마이그레이션#

Vue 3 마이그레이션에 대한 일반적인 정보는 Vue 3 공식 마이그레이션 가이드를 참고합니다.

옵션 1(권장): 피처 플래그와 vue3_migration.yml을 사용해 페이지 엔트리포인트 마이그레이션#

GitLab은 app/assets/javascripts/pages(및 EE·JH 대응 경로) 아래 모든 페이지 엔트리의 마이그레이션 상태를 index.js라는 페이지 엔트리포인트와 같은 위치에 있는 vue3_migration.yml 파일에 선언합니다.

└── pages/
    └── [area]/
        └── [page]/
            ├── index.js              # Entrypoint: imports and calls the initializer
            └── vue3_migration.yml    # Declares the page's migration status

번들러는 이 파일들을 읽어 Vue 3 청크를 생성할지 결정하고, Rails는 요청 시점에 피처 플래그에 따라 어떤 청크를 렌더링할지 결정합니다.

status: rollout
feature_flag: vue3_migrate_jobs # we recommend naming the flag `vue3_migrate_<page>`
group: group::pipeline authoring # optional
migration_issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/... # optional

이 파일이 있으면 다음 두 값 중 하나를 status 필드로 선언해야 합니다.

  • rollout: Vue 2와 Vue 3 청크가 모두 빌드됩니다(Vue 3 청크는 .vue3 형제 엔트리포인트로 생성됩니다). Rails는 선언된 피처 플래그가 활성화되면 Vue 3 청크를, 그렇지 않으면 Vue 2 청크를 제공합니다. feature_flag 필드가 필요합니다.
  • migrated: 원래 엔트리포인트 이름으로 Vue 3 청크만 빌드되므로, Rails는 별도의 조회 없이 이를 제공합니다. 피처 플래그를 완전히 롤아웃하고 제거한 뒤에 사용합니다.

선택 필드 group과 migration_issue는 문서화 용도로 허용됩니다. 스키마는 config/helpers/vue3_migration_file_validation.js에 정의되어 있습니다.

메타데이터가 프로덕션에 전달되는 방식#

Rails는 프로덕션 런타임에 vue3_migration.yml 파일을 읽지 않습니다. 패키지 빌드(예: Omnibus)는 Rails 애플리케이션에서 app/assets를 제거하므로 그곳에는 이 파일이 존재하지 않습니다. 대신 webpack 빌드가 rollout 항목들을 하나의 public/assets/webpack/vue3_migration.json 매니페스트로 컴파일하며 (config/plugins/vue3_migration_manifest_plugin.js 참고), 이 매니페스트는 모든 배포판에서 컴파일된 애셋과 함께 제공됩니다.

어떤 환경에서든 어떤 애플리케이션이 Vue 3으로 실행되는지 확인하려면, Vue 3 런타임이 설정한 DOM 마커를 조회합니다. document.querySelectorAll('[data-gitlab-vue3-app]').

피처 플래그 유형은 beta를 사용합니다. 마이그레이션을 기본으로 활성화하면서도 회귀가 발생하면 다시 끌 수 있는 유일한 유형이기 때문입니다.

전역 번들#

엔트리포인트는 페이지 엔트리만 있는 것이 아닙니다. super_sidebar, performance_bar, tracker, sentry, redirect_listbox, jira_connect_app, graphql_explorer, 샌드박스 뷰어처럼 페이지와 함께 로드되는 번들은 config/helpers/entry_points.js에 수동으로 선언되고 webpack_bundle_tag로 렌더링됩니다.

이 번들들은 페이지 엔트리와 동일한 메커니즘과 동일한 YAML 문서를 사용합니다. 파일은 설명 대상 엔트리 모듈 옆에 놓이며, 파일 이름이 어느 모듈을 설명하는지 알려 줍니다. 이름이 붙지 않은 vue3_migration.yml은 옆에 있는 index.js를, <name>.vue3_migration.yml은 옆에 있는 <name>.js를 설명합니다. 페이지 엔트리는 항상 index.js이므로 이름이 붙지 않은 형태를 씁니다. config/helpers/entry_points.js에 선언된 번들은 엔트리 모듈에 맞는 형태를 사용합니다.

└── entrypoints/
    ├── performance_bar.js                      # Entrypoint
    └── performance_bar.vue3_migration.yml      # Declares the bundle's migration status
└── sentry/
    ├── index.js                                # Entrypoint of the `sentry` bundle
    └── vue3_migration.yml                      # Declares the bundle's migration status

이 문서는 페이지의 것과 동일합니다. status 값도, feature_flag 규칙도, 선택 필드도 같습니다. 설명 대상 모듈은 번들러 엔트리여야 합니다. 로더는 config/helpers/entry_points.js나 페이지 경로에서 엔트리 이름을 해석하며, 그 외의 모듈에 대해서는 오류를 발생시킵니다.

main은 이 방식으로 마이그레이션할 수 없습니다. config/helpers/entry_points.js는 이를 default: ['./main']로 선언하고, config/webpack.helpers.js는 이를 독자적인 번들로 생성하지 않고 모든 페이지 엔트리 앞에 붙이므로, Rails가 교체할 애셋이 없습니다. 로더는 main.vue3_migration.yml을 발견하면 오류를 발생시킵니다. main.js에서 도달하는 코드에는 대신 옵션 2를 사용합니다.

아래 마이그레이션 단계는 그대로 적용되며, 1단계에서 pages/ 아래의 페이지가 아니라 config/helpers/entry_points.js의 번들 엔트리 모듈을 가리킨다는 점만 다릅니다.

마이그레이션 단계#

  1. app/assets/javascripts/pages(또는 ee/...) 아래에서 페이지의 엔트리포인트를 찾습니다. 예를 들어 app/assets/javascripts/pages/projects/jobs/show/index.js입니다.

  2. config/feature_flags/에서 피처 플래그를 생성하거나 업데이트합니다. 마이그레이션 메커니즘은 현재 사용자를 actor로 사용합니다.

  3. 페이지의 index.js 옆에 vue3_migration.yml 파일을 만들고, 피처 플래그 이름과 함께 status: rollout을 선언합니다.

    # app/assets/javascripts/pages/projects/jobs/show/vue3_migration.yml
    status: rollout
    feature_flag: vue3_migrate_jobs
    

    페이지가 CE와 EE에 걸쳐 섀도잉되어 있으면 현재 index.js를 가진 디렉터리에 파일을 추가하고, 두 디렉터리 모두 index.js를 가지고 있으면 양쪽에 추가합니다. 같은 페이지의 CE·EE YAML은 status와 feature_flag가 일치해야 합니다.

  4. gdk restart vite로 Vite를 재시작합니다. Vite는 시작할 때 페이지 엔트리 맵을 만들기 때문에, 실행 중에 추가된 엔트리포인트는 제공하지 않습니다.

  5. 피처 플래그를 활성화하고 로컬에서 페이지를 로드합니다.

  6. 콘솔에 다음이 표시되는지 확인합니다. [gitlab] [V] Using Vue.js 3 (with @vue/compat) for <your app name>.

  7. document.querySelectorAll('[data-gitlab-vue3-app]')가 해당 애플리케이션을 반환하는지 확인합니다.

  8. 로컬에서 애플리케이션이 정상 동작하는지 확인합니다. 이 확인을 리뷰어가 직접 볼 수 있는 증적으로 만들려면 검증을 영상으로 기록을 참고합니다.

  9. 변경 사항으로 MR을 열고 머지되도록 합니다.

  10. user actor로 피처 플래그 롤아웃을 진행합니다.

  11. 피처 플래그를 제거할 때 YAML을 status: migrated로 바꾸고 feature_flag 줄을 삭제합니다. 이것으로 완료됩니다.

옵션 2: ?vue3을 사용해 페이지를 부분적으로 마이그레이션#

vue3_migration.yml 파일은 페이지 엔트리 전체에 적용되므로, 엔트리포인트가 초기화하는 모든 애플리케이션을 함께 옮깁니다. 일부 엔트리포인트는 서로 다른 팀이 소유한 독립적인 애플리케이션을 여럿 마운트합니다. 설정 페이지가 대표적인 예로, 하나의 index.js가 서로 관련 없는 애플리케이션 10여 개를 초기화합니다.

그런 페이지에서 애플리케이션 하나만 소유하고 있다면 Option 1은 범위가 너무 넓습니다. 피처 플래그를 켜면 소유하지 않은 애플리케이션까지 함께 옮겨집니다.

대신 자신이 소유한 애플리케이션의 임포트에 ?vue3을 추가합니다. 그 임포트 아래의 모든 모듈은 Vue 3으로 빌드되고, 페이지의 나머지는 Vue 2로 유지됩니다. 이 옵션은 컨트롤러를 거치는 피처 플래그와 페이지 엔트리포인트의 조건 분기처럼 코드가 더 들기 때문에, 옵션 1이 맞지 않을 때 사용합니다.

마이그레이션 단계#

  1. config/feature_flags/에서 피처 플래그를 생성하거나 업데이트합니다.

  2. 페이지를 렌더링하는 컨트롤러에서 플래그를 프론트엔드로 푸시합니다.

    # app/controllers/projects/settings/ci_cd_controller.rb
    before_action do
      push_frontend_feature_flag(:vue3_migrate_my_app, current_user)
    end
    
  3. 페이지 엔트리포인트에서 플래그가 활성화되면 애플리케이션의 Vue 3 빌드를 동적으로 임포트하고, 그 임포트가 실패하면 Vue 2 빌드로 폴백합니다.

    // app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
    import { initMyApp } from '~/my_app';
    import { initOtherApp } from '~/other_app';
    import * as Sentry from '~/sentry/sentry_browser_wrapper';
    
    // Other apps on this page stay on Vue 2.
    initOtherApp();
    
    if (gon.features?.vue3MigrateMyApp) {
      (async () => {
        try {
          // eslint-disable-next-line no-shadow -- Override with Vue 3 app
          const { initMyApp } = await import('~/my_app?vue3');
          initMyApp();
          return;
        } catch (e) {
          Sentry.captureException(e);
        }
    
        initMyApp();
      })();
    } else {
      initMyApp();
    }
    

    번들러는 런타임에 조립된 경로를 볼 수 없으므로 ?vue3 경로는 문자열 리터럴로 유지합니다. ?vue3 임포트는 Vue 인스턴스를 만드는 모듈을 가리키게 합니다. 인펙션은 아래로 전파되므로, 트리에서 더 위에 있는 임포트는 그 아래의 다른 애플리케이션까지 모두 함께 옮깁니다. 폴백에서 바깥쪽 Vue 2 임포트를 쓸 수 있도록 임포트는 try 블록 안에 선언합니다.

  4. 피처 플래그를 활성화하고 로컬에서 페이지를 로드합니다.

  5. 콘솔에 다음이 표시되는지 확인합니다. [gitlab] [V] Using Vue.js 3 (with @vue/compat) for <your app name>.

  6. document.querySelectorAll('[data-gitlab-vue3-app]')가 해당 애플리케이션을 반환하는지 확인합니다.

  7. 로컬에서 애플리케이션이 정상 동작하는지 확인하고, 리뷰어를 위한 증적으로 워크스루를 녹화합니다.

  8. 변경 사항으로 MR을 열고 머지되도록 합니다.

  9. user actor로 피처 플래그 롤아웃을 진행합니다.

  10. 피처 플래그를 제거할 때는 ~/my_app?vue3을 직접 임포트하고 조건 분기와 Vue 2 임포트를 모두 삭제합니다.

옵션 2는 main_ee.js와 main_jh.js를 포함해 main.js에서 도달하는 코드에 대한 유일한 경로이기도 합니다. 이 파일들은 모든 페이지에서 실행되고 모든 페이지 엔트리에 컴파일되어 들어가므로, vue3_migration.yml이 대상으로 삼을 번들 이름이 없습니다.

한 가지 주의할 점이 있습니다. 인펙션된 서브트리와 애플리케이션의 나머지가 공유하는 모듈은 런타임마다 하나씩, 두 번 빌드됩니다. 상태가 없는 코드라면 문제가 없지만, 이벤트 허브·캐시·변경 가능한 플래그 같은 모듈 수준 싱글턴은 싱글턴 두 개가 되고, 두 쪽은 서로를 보지 못하게 됩니다. 마이그레이션하는 코드가 그 경계를 넘어 상태를 공유한다면, 두 런타임에 쓰기를 미러링하는 ~/lib/utils/observable을 거치게 합니다. 공유를 유지해야 하는 모듈은 config/helpers/context_aliases_shared.js에 나열되어 있습니다.

검증을 영상으로 기록#

두 옵션 모두 마지막에 같은 수동 단계가 남습니다. 애플리케이션이 Vue 3에서도 정상 동작하는지 확인하는 일입니다. 단위 테스트는 컴포넌트를 격리해 마운트하므로 브라우저가 필요한 회귀는 테스트가 모두 통과해도 살아남고, "로컬에서 확인함"이라고만 적힌 MR은 리뷰어가 볼 것이 없습니다. AI 에이전트가 브라우저에서 애플리케이션을 조작하게 하고, 세션을 녹화해 그 영상을 MR에 첨부합니다.

Warning

에이전트는 로그인된 세션으로 실제 브라우저를 조작하므로, 에이전트가 수행하는 모든 상호작용은 실제 쓰기입니다. 데이터를 생성·변경·삭제할 수 있습니다. 녹화는 로컬 GDK의 시딩된 데이터에서만 수행하고 공유 환경이나 프로덕션 환경에서는 절대 수행하지 않으며, 에이전트에 어떤 레코드를 건드려도 되는지 알려 줍니다. 별도의 브라우저 프로필(--user-data-dir)은 세션과 쿠키를 격리하지만, 에이전트가 애플리케이션에서 변경할 수 있는 범위를 제한하지는 않습니다. 그 보장이 필요하다면 폐기 가능한 GDK에서 녹화합니다.

Playwright Record MCP#

Playwright Record MCP는 에이전트에 Playwright 브라우저 도구(browser_navigate, browser_click, browser_type, browser_snapshot 등)를 제공하고, 세션을 녹화해 H.264 .mp4로 내보내는 Model Context Protocol(MCP) 서버입니다.

설치하려면 에이전트에 README의 Instructions for AI agents (autonomous install) 섹션을 따르도록 요청합니다. 이 섹션은 사전 요구 사항 (Node.js 18 이상, ffmpeg), 클론, npx playwright install chromium, MCP 클라이언트 등록을 다룹니다. 설치한 세션에서는 서버를 사용할 수 없으므로, 설치 후 MCP 클라이언트를 재시작합니다.

권장 워크플로#

  1. 애플리케이션이 지원하는 상호작용을 목록으로 만들되, 기억이 아니라 컴포넌트 템플릿과 그 스펙에서 도출합니다. 각 상호작용은 녹화의 한 단계가 됩니다. 폼 제출, 필터, 드롭다운, 대화 상자, 드래그, 페이지네이션, 빈 상태, 오류 상태가 여기에 해당합니다.

  2. 녹화 전에 무대를 준비합니다. 체크리스트에 필요한 프로젝트, 레코드, 상태가 여기에 해당합니다. 이 작업을 직접 하면 준비 과정이 영상에 들어가지 않고, 에이전트가 GDK에 임의로 픽스처를 만들지 않습니다. lib/gitlab/seeders/의 시더가 일반적인 경우를 다룹니다. 준비를 위임할 수도 있지만, 에이전트에 명시적인 목록을 주고, 녹화 전에 별도 단계로 실행한 뒤, 무엇을 만들었는지 확인합니다.

  3. 에이전트에 목록을 두 번 실행하도록 요청합니다. 한 번은 피처 플래그를 끈 상태로, 한 번은 켠 상태로 실행합니다. Vue 2 실행이 기준선입니다. 두 실행 모두에서 실패하는 단계는 기존 버그이지, 마이그레이션 회귀가 아닙니다.

  4. 두 번째 실행이 Vue 3으로 돌았는지 확인합니다. Vue 2를 제공해도 오류가 나지 않으므로, 피처 플래그나 ?vue3 임포트에 실수가 있으면 Vue 2 애플리케이션의 영상이 만들어집니다. --console-overlay-pin "Using Vue.js"로 엔진 알림을 오버레이에 고정하면 영상 자체가 그 증적을 담습니다. 실행 로그에 남기고 싶다면 browser_console_messages가 같은 줄을 보고합니다.

  5. 마지막 단계로 .mp4 파일 이름과 함께 browser_video_save를 호출합니다. 이 호출은 영상을 확정하기 위해 브라우저를 닫으므로, 이후에는 다른 브라우저 도구가 동작하지 않습니다. 한 세션은 영상 하나를 만들므로, 각 실행에는 각자의 세션이 필요합니다.

  6. .mp4를 단계 목록과 함께 MR에 첨부하고, 두 실행 사이의 차이를 설명합니다. glab CLI가 설치되어 있으면 에이전트가 직접 처리할 수도 있습니다. 에이전트는 다음과 같이 파일을 업로드합니다.

    glab api projects/<project-id-or-path>/uploads -X POST --form "file=@vue3-my-app.mp4"
    

    응답에는 markdown 필드가 있습니다. 에이전트는 이 스니펫을 glab mr update <mr-id> --description ...로 MR 설명에 넣거나, glab mr note <mr-id>로 코멘트에 넣습니다.

녹화에 콘솔 표시#

일부 Vue 3 회귀는 화면에 전혀 나타나지 않습니다. @vue/compat 지원 중단 경고나, UI를 그대로 둔 채 핸들러 안에서 발생한 오류는 콘솔에만 존재합니다. 앞 단계에서 browser_console_messages로 이를 잡아내지만, 리뷰어는 영상에서 콘솔을 볼 수 없습니다.

콘솔을 영상에 담으려면 서버 인수에 오버레이 플래그를 추가합니다.

claude mcp add -s user playwright-record -- \
  node "$HOME/playwright-record-mcp/cli.js" \
  --record-video --video-dir "$HOME/playwright-record-mcp/mcp_videos" \
  --video-size 1280x800 --video-speed 1.5 \
  --console-overlay --console-overlay-pin "Using Vue.js"

그러면 모든 콘솔 오류와 경고, 모든 잡히지 않은 오류, 모든 거부된 프로미스가 페이지 오른쪽 아래 패널에 그려지고, 녹화가 이를 담습니다. 패널은 최근 메시지 여덟 개를 최신순으로 보여 주며 페이지 이동 후에도 유지되므로, 리뷰어는 워크스루가 진행되는 동안 콘솔을 함께 읽습니다. 이것들은 서버 플래그이므로 에이전트에 별도로 지시할 필요가 없습니다.

패널에 표시되는 내용은 세 가지 옵션으로 조정합니다. 각 옵션은 --console-overlay를 함의하므로, 핀만 지정해도 충분합니다.

패널은 애플리케이션이 console을 통해 남긴 로그와 잡히지 않은 오류, 거부된 프로미스를 다룹니다. 실패한 요청처럼 브라우저가 직접 기록하는 메시지는 console을 거치지 않으므로 패널에 나타나지 않습니다. 이런 메시지는 browser_console_messages와 browser_network_requests에서 확인합니다.

시작 프롬프트#

실제 브라우저에서 마이그레이션을 처음부터 끝까지 검증하고 녹화를 증적으로 남기려면, 다음 프롬프트를 에이전트에 복사해 사용합니다. 실행하기 전에 애플리케이션 이름, 피처 플래그, 시딩된 데이터 같은 세부 사항을 상황에 맞게 수정합니다.

에이전트에 전달할 시작 프롬프트
You are verifying a Vue 2 to Vue 3 (`@vue/compat`) migration for a Vue app, in a real browser,
with a recorded video as evidence.

Fill these in before you run this prompt:

- GDK URL: `http://gdk.test:3000`
- Feature flag: `<vue3_migrate_my_app>`
- Page that mounts the app: `<path>`
- Sign-in: the username and password of a local GDK account.
- Project or records to use: `<what I already prepared for you>`

Only ever use a local GDK account. Never pass me credentials for a shared or production
environment.

Follow these steps in order.

Constraints:

- You drive a real browser against my local GDK, signed in as a real user. Every interaction is a
  real write, so stay on the records I named above and tell me anything you changed.
- Do not modify application code to make a step pass. Report the failure instead.
- Do not create projects, users, or fixtures, and do not run seeders or migrations. Verify against
  what I prepared. If the checklist needs a record that does not exist, or the environment errors
  on you, stop and tell me. A broken local environment is not a migration finding.
- Record with the console overlay on, so the video carries the console instead of a separate log.
  The server needs these arguments, and each overlay option implies `--console-overlay`:

  ```shell
  --record-video --video-size 1280x800 --video-speed 1.5 \
    --console-overlay --console-overlay-pin "Using Vue.js"
  ```

  If the panel is too noisy to read, narrow it with `--console-overlay-match <regex>`. If you need
  a level that the default does not paint, name it with `--console-overlay-levels error,warn,log`.
  Prefer these options over filtering the output yourself. You cannot restart the server, so tell
  me when a flag has to change and wait.
- Use the `playwright-record` MCP server for all browser actions: `browser_navigate`,
  `browser_click`, `browser_type`, `browser_hover`, `browser_drag`, `browser_select_option`,
  `browser_press_key`, `browser_snapshot`, `browser_wait`, `browser_console_messages`,
  `browser_network_requests`, `browser_video_save`. It cannot evaluate arbitrary JavaScript, so
  read state from `browser_snapshot` and `browser_console_messages`, not from the DOM.

1. Read the code before you touch a browser. Work out from the diff and the component source what
   the app does: which components sit in the migrated dependency tree, every `$emit` and every
   `v-on` or `@` listener, the `emits:` declarations, props with default factory functions, scoped
   slots, `v-model` usage, Vue Router usage, and anything the Vue 3 migration is known to break.
   Read the component's Jest specs too. They enumerate the behavior, and they show which covered
   behavior a browser walkthrough does not reach.

   Then read the compatibility changes in the migration guide,
   <https://docs.gitlab.com/development/fe_guide/vue3_migration/#compatibility-changes>, and check
   the app against each one. That list is a starting point, not the whole surface: look for
   anything else the app depends on. Pay attention to the libraries aliased in
   `config/helpers/context_aliases_shared.js`, because they run through a Vue 3 shim, and give the
   ones this app imports their own checklist entries.

1. Build an explicit checklist of interactions from that reading, and share it with me before you
   run anything, so I can correct it. Group it into:
   - Main features: the app's primary user flows.
   - Event and emit paths: each emitted event and the observable result its listener produces.
   - Edge cases: empty state, error state, loading state, permission-restricted state, long or
     truncated content, and keyboard-only interaction.

1. Sign in at `/users/sign_in` with the credentials above, as your first browser action
   in every session. The GitLab session cookie does not survive a browser restart, so each
   recorded session signs in again. The sign-in form is itself a Vue app, so read a fresh
   `browser_snapshot` before each field: typing into one field re-renders the form and stales the
   reference to the other.

1. Confirm the page runs under Vue 3 before you verify anything. The pin keeps every
   `[gitlab] [V] Using Vue.js 3` line on screen for the whole recording, one per app root that
   started, so the video states which engine ran. Read that pinned block from `browser_snapshot`.
   If your app is missing from it, stop and report it. A walkthrough on Vue 2 proves nothing about
   the migration.

1. Run the checklist twice, in two separate recorded sessions: once with the feature flag off
   (Vue 2 baseline) and once with it on (Vue 3). A step that fails in both is an existing bug, not
   a migration regression.

1. For every interaction, assert an observable result, not just that the click happened. Examples:
   text that appears or changes, a row count, a URL query parameter, a request in
   `browser_network_requests`, an element that appears or disappears in `browser_snapshot`. If the
   handler for an event produces nothing observable on the page, call it out as unverifiable from
   the browser. Do not report it as passing.

1. Read the overlay after each step. Treat any error or `@vue/compat` warning that appears in the
   Vue 3 pass but not in the Vue 2 pass as a migration regression. `browser_console_messages` holds
   the full log for your report, including the messages the panel clipped.

1. Call `browser_video_save` with an `.mp4` filename as your last call in each session. It closes
   the browser to finalize the video, so no browser tool works after it. Then report the checklist
   with a pass or fail for each item, the console difference between the two passes, anything you
   could not verify from the browser, and the video path for each session. If `glab` is installed,
   upload the videos and attach them to the merge request.

유의 사항#

  • 영상은 에이전트가 수행한 상호작용이 해당 커밋에서 동작한다는 것을 보여 줄 뿐, 테스트는 아닙니다. 이후 변경에 대해 아무것도 다시 실행하지 않습니다. 수정한 동작은 계속 Jest 스펙으로 커버합니다.
  • 녹화에는 오디오가 없으므로 --video-speed 1.5로 세부 내용을 잃지 않으면서 클립을 짧게 만듭니다.

공통 마이그레이션 문제#

Vue Router props 반응성#

props 함수로 전달된 라우터 props는 Vue 3에서 반응형이 아닙니다. 대신 this.$route에서 값을 읽는 computed 속성을 사용합니다.

// Component - use computed property instead of props
computed: {
  currentPath() {
    return this.$route?.params.path || '';
  }
}

Watch 표현식#

$route 객체 전체가 아니라 문자열 경로로 특정 라우트 속성을 watch합니다. $route에 deep: true를 사용할 수도 있지만 성능 오버헤드가 생깁니다. 특정 속성을 watch하는 편이 더 효율적이고 컴포넌트의 의존성도 명확하게 드러냅니다.

watch: {
  '$route.params.path'() {
    this.fetchData();
  }
}

테스팅#

Vue 3 사용 중 실패하는 테스트를 구현하거나 수정하는 방법에 대한 자세한 내용은 Vue 3 테스팅 가이드를 참고합니다.

@vue/compat 패치 업데이트#

@vue/compat 패치를 업데이트하는 방법은 까다로울 수 있으므로 이 문서를 참고합니다.

Vue 3 마이그레이션

GitLab v19.4
원문 보기

요약

Vue 2에서 Vue 3으로의 마이그레이션은 에픽 &#x26;6252에서 추적됩니다. Vue 3.x로의 마이그레이션을 쉽게 하기 위해, 코드베이스에서 다음의 더 이상 사용되지 않는 기능을 쓰지 못하도록 막는 ESLint 규칙을 추가했습니다.

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

Vue 3.x로의 마이그레이션을 쉽게 하기 위해, 코드베이스에서 다음의 더 이상 사용되지 않는 기능을 쓰지 못하도록 막는 ESLint 규칙을 추가했습니다.

GitLab에서 Vue 3 (@vue/compat) 사용 가능#

GitLab 프론트엔드 팀은 GDK와 같은 개발 환경에서 Vue 3 (@vue/compat)을 활성화했습니다. 아직 프로덕션에 사용할 준비는 되지 않았지만, 로컬에서 opt-in하여 클라이언트 코드가 Vue 3과 순방향 호환되는지 확인할 수 있습니다.

동작 방식 빌드 도구(Vite 또는 Webpack)가 VUE_VERSION=3 환경 변수를 감지하면, 모듈 에일리어싱을 사용해 Vue 자체를 포함한 특정 의존성을 Vue 3 호환 버전으로 교체합니다.

이 대체 라이브러리 중 일부는 팀에서 직접 유지 관리합니다. 이들은 기존 라이브러리를 감싸는 얇은 래퍼 역할을 하며, 컨슈머 코드를 전혀 변경하지 않고도 Vue 3과 호환되게 합니다.

GDK에서 Vue 3 (@vue/compat) 설정#

이 가이드는 GitLab Development Kit(GDK)에서 Vite를 빌드 도구로 사용해 Vue 3을 구성하는 과정을 안내합니다.

사전 요구 사항#

  • GDK 설치 및 구성 완료
  • Vue.js와 Vite에 대한 기본적인 이해
  • GDK 환경에 Vite 구성(GDK Vite Settings 참고)

초기 설정#

Vue 버전 간 전환#

Vue 2와 Vue 3 사이를 전환하려면 다음 단계를 따릅니다.

  1. 원하는 Vue 버전을 설정합니다.

    gdk config set vite.vue_version 3  # or 2
    
  2. GDK를 재구성합니다.

    gdk reconfigure
    
  3. GDK를 재시작합니다.

    gdk restart # or `gdk start` if running for the first time
    

중요: Vue 버전 전환에 문제가 있으면 yarn clean 또는 gdk kill vite로 캐시를 지울 수 있습니다.

설정 확인#

gdk.yml 파일을 확인해 Vite 구성을 검증할 수 있습니다.

gdk config get vite

활성화 상태와 Vue 버전을 포함한 현재 Vite 설정이 표시됩니다. GDK도 실행 중이어야 합니다.

---
enabled: true
hot_module_reloading: true
https:
  enabled: true
port: 3038
vue_version: 3

문제 해결#

일반적인 디버깅#

문제가 발생하면 먼저 Vite 로그를 확인합니다.

gdk tail vite

이 명령은 실시간 Vite 출력과 오류 메시지를 보여 주며, 문제를 파악하는 데 도움이 됩니다.

버전 전환 후 빌드 오류#

Vue 버전을 전환한 뒤 빌드 오류가 발생하면 다음을 수행합니다.

  1. yarn clean으로 Vite 캐시를 지웠는지 확인합니다

  2. node_modules를 지우고 의존성을 다시 설치해 봅니다.

    rm -rf node_modules
    yarn install
    

Vite가 시작되지 않는 경우#

Vite 시작에 실패하면 다음을 확인합니다.

  • vite.enabled가 true로 설정되어 있는지 확인합니다
  • Node.js 버전이 Vite의 요구 사항을 충족하는지 확인합니다
  • GDK 로그에서 구체적인 오류 메시지를 확인합니다

추가 리소스#

호환성 변경 사항#

아래 변경 사항은 이 마이그레이션에서 가장 자주 마주치는 것들이며, 전체 범위는 아닙니다. 애플리케이션은 임포트하는 어떤 것 때문에도 깨질 수 있으므로, 이 목록은 체크리스트가 아니라 출발점으로 봅니다. config/helpers/context_aliases_shared.js에서 에일리어싱되는 라이브러리, 예를 들어 vuex, vue-router, vue-apollo, portal-vue, vuedraggable, 가상 스크롤러는 Vue 3 심을 거치므로 마이그레이션에 가장 민감합니다.

Vue 필터#

이유

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

대신 사용할 것

컴포넌트의 computed 속성이나 메서드, 또는 외부 헬퍼를 사용합니다.

이벤트 허브#

이유

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

대신 사용할 것

다른 props에서 원하는 값을 도출하는 computed 속성을 작성합니다. 이 방식은 Vue 2와 Vue 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.observable#

이유

Vue.observable은 이를 생성한 Vue 버전에 묶인 반응형 상태를 만듭니다. Vue 2/Vue 3 하이브리드 인펙션 시스템에서는 모듈이 Vue 버전마다 하나씩 복제될 수 있습니다. 이 모듈들이 Vue.observable()을 사용하면 각 복사본이 서로 분리된 반응형 객체를 만들기 때문에, 한쪽의 상태 변경이 다른 쪽에는 보이지 않습니다.

대신 사용할 것

~/lib/utils/observable의 observable()을 사용합니다.

import { observable } from '~/lib/utils/observable';

// Before
export const state = Vue.observable({ count: 0 });

// After
export const state = observable('unique_key', { count: 0 });

observable(key, defaults) 함수의 동작은 다음과 같습니다.

  • key를 키로 하는 전역 레지스트리에 단일 정본 상태를 저장합니다
  • 내부적으로 Vue.observable()을 통해 Vue 컨텍스트별 반응형 미러를 생성합니다
  • 모든 Vue 버전의 미러에 쓰기를 동기화하는 Proxy를 반환합니다
  • 평탄한 객체, getter, 메서드를 지원합니다

key는 고유한 문자열 식별자여야 합니다(예: 'super_sidebar_state'). 이 키가 두 모듈 복사본이 동일한 기반 상태를 공유하도록 보장합니다.

ESLint 규칙(no-restricted-properties)이 이를 강제하며, Vue.observable을 직접 사용하면 린트 오류가 발생합니다.

제약 사항

  • 평탄한 객체만 지원: state.nested.prop = value나 state.array.push(item) 같은 중첩 변경은 Vue 버전 간에 동기화되지 않습니다. 대신 최상위 속성을 교체하는 방식으로 리팩터링합니다.

    // Instead of: state.items.push(newItem)
    state.items = [...state.items, newItem];
    
    // Instead of: state.config[key] = value
    state.config = { ...state.config, [key]: value };
    

모듈 스코프 싱글턴#

마이그레이션된 페이지는 "인펙션" 메커니즘을 통해 일부 모듈을 Vue 2와 Vue 3 양쪽으로 로드할 수 있습니다. 각 Vue 버전이 같은 모듈을 실행하므로, 상태를 가진 모듈은 복제될 수 있고 코드를 실행하는 모듈은 두 번 실행될 수 있습니다. 예를 들어 이벤트 핸들러가 두 번 등록됩니다. 이 복제를 다루기 위해 이런 모듈을 싱글턴이라고 부릅니다.

복제는 발견하기 어렵습니다. 컨슈머는 모듈이 복제되었다는 사실을 알지 못합니다. 오래된 값을 읽게 되지만, 어떤 오류도 보고되지 않습니다.

발생하는 오류#

이런 싱글턴이 복제되는 것을 막기 위해 번들링 job(compile-production-assets)을 실패시키는 안전장치를 만들어 두었습니다. 복제가 발생하면 해당 job이 실패합니다.

스캐너는 다음과 같은 출력을 표시합니다.

[vue3-infection-scanner] Duplicated modules detected: 1 module(s) on 14 page state(s), 14 finding(s).

  pages.dashboard.issues (flag on)
    roots: app/assets/javascripts/main.js
           app/assets/javascripts/pages/dashboard/issues/index.js
           app/assets/javascripts/entrypoints/super_sidebar.js

    app/assets/javascripts/graphql_shared/issuable_default_client.js
       holds: apollo-client
       sink:  (none: this copy has no Vue 3 ancestor)
          V2  app/assets/javascripts/entrypoints/super_sidebar.js
          V2  app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
          V2  app/assets/javascripts/graphql_shared/issuable_client.js
          V2  app/assets/javascripts/graphql_shared/issuable_default_client.js

각 필드의 의미는 다음과 같습니다.

  • holds는 모듈에서 발견된 상태의 종류입니다.
  • 체인은 각 레인이 모듈에 도달하는 경로를 보여 줍니다. V2 또는 V3는 각 단계의 레인을 표시합니다.
  • sink는 서브트리를 Vue 2로 되돌린 모듈이 있을 경우 그 모듈의 이름입니다. 번들러가 exposedToVue를 따라간 뒤에는 INFECTION_BLOCKLIST에 있는 모듈만 sink가 될 수 있습니다.
  • 이 예시에서 roots 목록은 일부만 표시되어 있습니다.

해결 방법#

  1. 일반적인 해결: 상태를 Vue에 노출되지 않는 별도 모듈로 옮깁니다. 이 모듈은 아무것도 임포트하지 않거나, Vue에 노출되지 않는 모듈만 임포트합니다. 번들러는 이런 모듈을 절대 복제하지 않으므로 모든 레인이 하나의 복사본을 공유합니다. app/assets/javascripts/lib/graphql_pending_requests.js와 app/assets/javascripts/graphql_shared/issuable_client_state.js가 기존 예시입니다.
  2. 대안: 상태 모듈이 임포트를 유지해야 한다면, config/helpers/context_aliases_shared.js의 INFECTION_BLOCKLIST에 추가합니다. 그러면 모든 레인이 하나의 복사본을 공유하지만, 그 모듈 아래의 서브트리는 Vue 3 페이지 안에서 Vue 2로 실행됩니다. app/assets/javascripts/lib/utils/breadcrumbs_state.js가 기존 예시입니다.
  3. 평탄한 반응형 상태에는 ~/lib/utils/observable의 observable()을 사용합니다.

블록리스트는 Vue 버전에 특화된 설정을 실행하지 않는 모듈에만 사용합니다. Pinia 인스턴스나 VueApollo 프로바이더는 이를 만든 Vue 버전에 묶이므로, 하나의 공유 복사본은 잘못된 버전에 묶입니다.

@vue/compat에서 동작하지 않는 라이브러리 처리#

문제

일부 라이브러리는 Vue.js 2 내부 구현에 의존합니다. 이런 라이브러리는 @vue/compat에서 동작하지 않을 수 있어, 호환성 레이어로 어댑터나 대체 구현을 추가했습니다.

목표

  • 새 라이브러리를 지원하기 위해 기존 코드에 가하는 변경은 최소화합니다. 대신 파사드 역할을 하는 새 코드를 추가해 새 버전이 이전 버전과 호환되게 합니다
  • 새 버전과 이전 버전 사이의 전환은 툴링(webpack / jest) 내부에 숨기고 코드에 노출하지 않습니다
  • 마이그레이션에 한정된 파사드는 모두 같은 디렉터리에 두어 이후 마이그레이션 단계를 단순하게 합니다

Vue 3으로 마이그레이션#

Vue 3 마이그레이션에 대한 일반적인 정보는 Vue 3 공식 마이그레이션 가이드를 참고합니다.

옵션 1(권장): 피처 플래그와 vue3_migration.yml을 사용해 페이지 엔트리포인트 마이그레이션#

GitLab은 app/assets/javascripts/pages(및 EE·JH 대응 경로) 아래 모든 페이지 엔트리의 마이그레이션 상태를 index.js라는 페이지 엔트리포인트와 같은 위치에 있는 vue3_migration.yml 파일에 선언합니다.

└── pages/
    └── [area]/
        └── [page]/
            ├── index.js              # Entrypoint: imports and calls the initializer
            └── vue3_migration.yml    # Declares the page's migration status

번들러는 이 파일들을 읽어 Vue 3 청크를 생성할지 결정하고, Rails는 요청 시점에 피처 플래그에 따라 어떤 청크를 렌더링할지 결정합니다.

status: rollout
feature_flag: vue3_migrate_jobs # we recommend naming the flag `vue3_migrate_<page>`
group: group::pipeline authoring # optional
migration_issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/... # optional

이 파일이 있으면 다음 두 값 중 하나를 status 필드로 선언해야 합니다.

  • rollout: Vue 2와 Vue 3 청크가 모두 빌드됩니다(Vue 3 청크는 .vue3 형제 엔트리포인트로 생성됩니다). Rails는 선언된 피처 플래그가 활성화되면 Vue 3 청크를, 그렇지 않으면 Vue 2 청크를 제공합니다. feature_flag 필드가 필요합니다.
  • migrated: 원래 엔트리포인트 이름으로 Vue 3 청크만 빌드되므로, Rails는 별도의 조회 없이 이를 제공합니다. 피처 플래그를 완전히 롤아웃하고 제거한 뒤에 사용합니다.

선택 필드 group과 migration_issue는 문서화 용도로 허용됩니다. 스키마는 config/helpers/vue3_migration_file_validation.js에 정의되어 있습니다.

메타데이터가 프로덕션에 전달되는 방식#

Rails는 프로덕션 런타임에 vue3_migration.yml 파일을 읽지 않습니다. 패키지 빌드(예: Omnibus)는 Rails 애플리케이션에서 app/assets를 제거하므로 그곳에는 이 파일이 존재하지 않습니다. 대신 webpack 빌드가 rollout 항목들을 하나의 public/assets/webpack/vue3_migration.json 매니페스트로 컴파일하며 (config/plugins/vue3_migration_manifest_plugin.js 참고), 이 매니페스트는 모든 배포판에서 컴파일된 애셋과 함께 제공됩니다.

어떤 환경에서든 어떤 애플리케이션이 Vue 3으로 실행되는지 확인하려면, Vue 3 런타임이 설정한 DOM 마커를 조회합니다. document.querySelectorAll('[data-gitlab-vue3-app]').

피처 플래그 유형은 beta를 사용합니다. 마이그레이션을 기본으로 활성화하면서도 회귀가 발생하면 다시 끌 수 있는 유일한 유형이기 때문입니다.

전역 번들#

엔트리포인트는 페이지 엔트리만 있는 것이 아닙니다. super_sidebar, performance_bar, tracker, sentry, redirect_listbox, jira_connect_app, graphql_explorer, 샌드박스 뷰어처럼 페이지와 함께 로드되는 번들은 config/helpers/entry_points.js에 수동으로 선언되고 webpack_bundle_tag로 렌더링됩니다.

이 번들들은 페이지 엔트리와 동일한 메커니즘과 동일한 YAML 문서를 사용합니다. 파일은 설명 대상 엔트리 모듈 옆에 놓이며, 파일 이름이 어느 모듈을 설명하는지 알려 줍니다. 이름이 붙지 않은 vue3_migration.yml은 옆에 있는 index.js를, <name>.vue3_migration.yml은 옆에 있는 <name>.js를 설명합니다. 페이지 엔트리는 항상 index.js이므로 이름이 붙지 않은 형태를 씁니다. config/helpers/entry_points.js에 선언된 번들은 엔트리 모듈에 맞는 형태를 사용합니다.

└── entrypoints/
    ├── performance_bar.js                      # Entrypoint
    └── performance_bar.vue3_migration.yml      # Declares the bundle's migration status
└── sentry/
    ├── index.js                                # Entrypoint of the `sentry` bundle
    └── vue3_migration.yml                      # Declares the bundle's migration status

이 문서는 페이지의 것과 동일합니다. status 값도, feature_flag 규칙도, 선택 필드도 같습니다. 설명 대상 모듈은 번들러 엔트리여야 합니다. 로더는 config/helpers/entry_points.js나 페이지 경로에서 엔트리 이름을 해석하며, 그 외의 모듈에 대해서는 오류를 발생시킵니다.

main은 이 방식으로 마이그레이션할 수 없습니다. config/helpers/entry_points.js는 이를 default: ['./main']로 선언하고, config/webpack.helpers.js는 이를 독자적인 번들로 생성하지 않고 모든 페이지 엔트리 앞에 붙이므로, Rails가 교체할 애셋이 없습니다. 로더는 main.vue3_migration.yml을 발견하면 오류를 발생시킵니다. main.js에서 도달하는 코드에는 대신 옵션 2를 사용합니다.

아래 마이그레이션 단계는 그대로 적용되며, 1단계에서 pages/ 아래의 페이지가 아니라 config/helpers/entry_points.js의 번들 엔트리 모듈을 가리킨다는 점만 다릅니다.

마이그레이션 단계#

  1. app/assets/javascripts/pages(또는 ee/...) 아래에서 페이지의 엔트리포인트를 찾습니다. 예를 들어 app/assets/javascripts/pages/projects/jobs/show/index.js입니다.

  2. config/feature_flags/에서 피처 플래그를 생성하거나 업데이트합니다. 마이그레이션 메커니즘은 현재 사용자를 actor로 사용합니다.

  3. 페이지의 index.js 옆에 vue3_migration.yml 파일을 만들고, 피처 플래그 이름과 함께 status: rollout을 선언합니다.

    # app/assets/javascripts/pages/projects/jobs/show/vue3_migration.yml
    status: rollout
    feature_flag: vue3_migrate_jobs
    

    페이지가 CE와 EE에 걸쳐 섀도잉되어 있으면 현재 index.js를 가진 디렉터리에 파일을 추가하고, 두 디렉터리 모두 index.js를 가지고 있으면 양쪽에 추가합니다. 같은 페이지의 CE·EE YAML은 status와 feature_flag가 일치해야 합니다.

  4. gdk restart vite로 Vite를 재시작합니다. Vite는 시작할 때 페이지 엔트리 맵을 만들기 때문에, 실행 중에 추가된 엔트리포인트는 제공하지 않습니다.

  5. 피처 플래그를 활성화하고 로컬에서 페이지를 로드합니다.

  6. 콘솔에 다음이 표시되는지 확인합니다. [gitlab] [V] Using Vue.js 3 (with @vue/compat) for <your app name>.

  7. document.querySelectorAll('[data-gitlab-vue3-app]')가 해당 애플리케이션을 반환하는지 확인합니다.

  8. 로컬에서 애플리케이션이 정상 동작하는지 확인합니다. 이 확인을 리뷰어가 직접 볼 수 있는 증적으로 만들려면 검증을 영상으로 기록을 참고합니다.

  9. 변경 사항으로 MR을 열고 머지되도록 합니다.

  10. user actor로 피처 플래그 롤아웃을 진행합니다.

  11. 피처 플래그를 제거할 때 YAML을 status: migrated로 바꾸고 feature_flag 줄을 삭제합니다. 이것으로 완료됩니다.

옵션 2: ?vue3을 사용해 페이지를 부분적으로 마이그레이션#

vue3_migration.yml 파일은 페이지 엔트리 전체에 적용되므로, 엔트리포인트가 초기화하는 모든 애플리케이션을 함께 옮깁니다. 일부 엔트리포인트는 서로 다른 팀이 소유한 독립적인 애플리케이션을 여럿 마운트합니다. 설정 페이지가 대표적인 예로, 하나의 index.js가 서로 관련 없는 애플리케이션 10여 개를 초기화합니다.

그런 페이지에서 애플리케이션 하나만 소유하고 있다면 Option 1은 범위가 너무 넓습니다. 피처 플래그를 켜면 소유하지 않은 애플리케이션까지 함께 옮겨집니다.

대신 자신이 소유한 애플리케이션의 임포트에 ?vue3을 추가합니다. 그 임포트 아래의 모든 모듈은 Vue 3으로 빌드되고, 페이지의 나머지는 Vue 2로 유지됩니다. 이 옵션은 컨트롤러를 거치는 피처 플래그와 페이지 엔트리포인트의 조건 분기처럼 코드가 더 들기 때문에, 옵션 1이 맞지 않을 때 사용합니다.

마이그레이션 단계#

  1. config/feature_flags/에서 피처 플래그를 생성하거나 업데이트합니다.

  2. 페이지를 렌더링하는 컨트롤러에서 플래그를 프론트엔드로 푸시합니다.

    # app/controllers/projects/settings/ci_cd_controller.rb
    before_action do
      push_frontend_feature_flag(:vue3_migrate_my_app, current_user)
    end
    
  3. 페이지 엔트리포인트에서 플래그가 활성화되면 애플리케이션의 Vue 3 빌드를 동적으로 임포트하고, 그 임포트가 실패하면 Vue 2 빌드로 폴백합니다.

    // app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
    import { initMyApp } from '~/my_app';
    import { initOtherApp } from '~/other_app';
    import * as Sentry from '~/sentry/sentry_browser_wrapper';
    
    // Other apps on this page stay on Vue 2.
    initOtherApp();
    
    if (gon.features?.vue3MigrateMyApp) {
      (async () => {
        try {
          // eslint-disable-next-line no-shadow -- Override with Vue 3 app
          const { initMyApp } = await import('~/my_app?vue3');
          initMyApp();
          return;
        } catch (e) {
          Sentry.captureException(e);
        }
    
        initMyApp();
      })();
    } else {
      initMyApp();
    }
    

    번들러는 런타임에 조립된 경로를 볼 수 없으므로 ?vue3 경로는 문자열 리터럴로 유지합니다. ?vue3 임포트는 Vue 인스턴스를 만드는 모듈을 가리키게 합니다. 인펙션은 아래로 전파되므로, 트리에서 더 위에 있는 임포트는 그 아래의 다른 애플리케이션까지 모두 함께 옮깁니다. 폴백에서 바깥쪽 Vue 2 임포트를 쓸 수 있도록 임포트는 try 블록 안에 선언합니다.

  4. 피처 플래그를 활성화하고 로컬에서 페이지를 로드합니다.

  5. 콘솔에 다음이 표시되는지 확인합니다. [gitlab] [V] Using Vue.js 3 (with @vue/compat) for <your app name>.

  6. document.querySelectorAll('[data-gitlab-vue3-app]')가 해당 애플리케이션을 반환하는지 확인합니다.

  7. 로컬에서 애플리케이션이 정상 동작하는지 확인하고, 리뷰어를 위한 증적으로 워크스루를 녹화합니다.

  8. 변경 사항으로 MR을 열고 머지되도록 합니다.

  9. user actor로 피처 플래그 롤아웃을 진행합니다.

  10. 피처 플래그를 제거할 때는 ~/my_app?vue3을 직접 임포트하고 조건 분기와 Vue 2 임포트를 모두 삭제합니다.

옵션 2는 main_ee.js와 main_jh.js를 포함해 main.js에서 도달하는 코드에 대한 유일한 경로이기도 합니다. 이 파일들은 모든 페이지에서 실행되고 모든 페이지 엔트리에 컴파일되어 들어가므로, vue3_migration.yml이 대상으로 삼을 번들 이름이 없습니다.

한 가지 주의할 점이 있습니다. 인펙션된 서브트리와 애플리케이션의 나머지가 공유하는 모듈은 런타임마다 하나씩, 두 번 빌드됩니다. 상태가 없는 코드라면 문제가 없지만, 이벤트 허브·캐시·변경 가능한 플래그 같은 모듈 수준 싱글턴은 싱글턴 두 개가 되고, 두 쪽은 서로를 보지 못하게 됩니다. 마이그레이션하는 코드가 그 경계를 넘어 상태를 공유한다면, 두 런타임에 쓰기를 미러링하는 ~/lib/utils/observable을 거치게 합니다. 공유를 유지해야 하는 모듈은 config/helpers/context_aliases_shared.js에 나열되어 있습니다.

검증을 영상으로 기록#

두 옵션 모두 마지막에 같은 수동 단계가 남습니다. 애플리케이션이 Vue 3에서도 정상 동작하는지 확인하는 일입니다. 단위 테스트는 컴포넌트를 격리해 마운트하므로 브라우저가 필요한 회귀는 테스트가 모두 통과해도 살아남고, "로컬에서 확인함"이라고만 적힌 MR은 리뷰어가 볼 것이 없습니다. AI 에이전트가 브라우저에서 애플리케이션을 조작하게 하고, 세션을 녹화해 그 영상을 MR에 첨부합니다.

Warning

에이전트는 로그인된 세션으로 실제 브라우저를 조작하므로, 에이전트가 수행하는 모든 상호작용은 실제 쓰기입니다. 데이터를 생성·변경·삭제할 수 있습니다. 녹화는 로컬 GDK의 시딩된 데이터에서만 수행하고 공유 환경이나 프로덕션 환경에서는 절대 수행하지 않으며, 에이전트에 어떤 레코드를 건드려도 되는지 알려 줍니다. 별도의 브라우저 프로필(--user-data-dir)은 세션과 쿠키를 격리하지만, 에이전트가 애플리케이션에서 변경할 수 있는 범위를 제한하지는 않습니다. 그 보장이 필요하다면 폐기 가능한 GDK에서 녹화합니다.

Playwright Record MCP#

Playwright Record MCP는 에이전트에 Playwright 브라우저 도구(browser_navigate, browser_click, browser_type, browser_snapshot 등)를 제공하고, 세션을 녹화해 H.264 .mp4로 내보내는 Model Context Protocol(MCP) 서버입니다.

설치하려면 에이전트에 README의 Instructions for AI agents (autonomous install) 섹션을 따르도록 요청합니다. 이 섹션은 사전 요구 사항 (Node.js 18 이상, ffmpeg), 클론, npx playwright install chromium, MCP 클라이언트 등록을 다룹니다. 설치한 세션에서는 서버를 사용할 수 없으므로, 설치 후 MCP 클라이언트를 재시작합니다.

권장 워크플로#

  1. 애플리케이션이 지원하는 상호작용을 목록으로 만들되, 기억이 아니라 컴포넌트 템플릿과 그 스펙에서 도출합니다. 각 상호작용은 녹화의 한 단계가 됩니다. 폼 제출, 필터, 드롭다운, 대화 상자, 드래그, 페이지네이션, 빈 상태, 오류 상태가 여기에 해당합니다.

  2. 녹화 전에 무대를 준비합니다. 체크리스트에 필요한 프로젝트, 레코드, 상태가 여기에 해당합니다. 이 작업을 직접 하면 준비 과정이 영상에 들어가지 않고, 에이전트가 GDK에 임의로 픽스처를 만들지 않습니다. lib/gitlab/seeders/의 시더가 일반적인 경우를 다룹니다. 준비를 위임할 수도 있지만, 에이전트에 명시적인 목록을 주고, 녹화 전에 별도 단계로 실행한 뒤, 무엇을 만들었는지 확인합니다.

  3. 에이전트에 목록을 두 번 실행하도록 요청합니다. 한 번은 피처 플래그를 끈 상태로, 한 번은 켠 상태로 실행합니다. Vue 2 실행이 기준선입니다. 두 실행 모두에서 실패하는 단계는 기존 버그이지, 마이그레이션 회귀가 아닙니다.

  4. 두 번째 실행이 Vue 3으로 돌았는지 확인합니다. Vue 2를 제공해도 오류가 나지 않으므로, 피처 플래그나 ?vue3 임포트에 실수가 있으면 Vue 2 애플리케이션의 영상이 만들어집니다. --console-overlay-pin "Using Vue.js"로 엔진 알림을 오버레이에 고정하면 영상 자체가 그 증적을 담습니다. 실행 로그에 남기고 싶다면 browser_console_messages가 같은 줄을 보고합니다.

  5. 마지막 단계로 .mp4 파일 이름과 함께 browser_video_save를 호출합니다. 이 호출은 영상을 확정하기 위해 브라우저를 닫으므로, 이후에는 다른 브라우저 도구가 동작하지 않습니다. 한 세션은 영상 하나를 만들므로, 각 실행에는 각자의 세션이 필요합니다.

  6. .mp4를 단계 목록과 함께 MR에 첨부하고, 두 실행 사이의 차이를 설명합니다. glab CLI가 설치되어 있으면 에이전트가 직접 처리할 수도 있습니다. 에이전트는 다음과 같이 파일을 업로드합니다.

    glab api projects/<project-id-or-path>/uploads -X POST --form "file=@vue3-my-app.mp4"
    

    응답에는 markdown 필드가 있습니다. 에이전트는 이 스니펫을 glab mr update <mr-id> --description ...로 MR 설명에 넣거나, glab mr note <mr-id>로 코멘트에 넣습니다.

녹화에 콘솔 표시#

일부 Vue 3 회귀는 화면에 전혀 나타나지 않습니다. @vue/compat 지원 중단 경고나, UI를 그대로 둔 채 핸들러 안에서 발생한 오류는 콘솔에만 존재합니다. 앞 단계에서 browser_console_messages로 이를 잡아내지만, 리뷰어는 영상에서 콘솔을 볼 수 없습니다.

콘솔을 영상에 담으려면 서버 인수에 오버레이 플래그를 추가합니다.

claude mcp add -s user playwright-record -- \
  node "$HOME/playwright-record-mcp/cli.js" \
  --record-video --video-dir "$HOME/playwright-record-mcp/mcp_videos" \
  --video-size 1280x800 --video-speed 1.5 \
  --console-overlay --console-overlay-pin "Using Vue.js"

그러면 모든 콘솔 오류와 경고, 모든 잡히지 않은 오류, 모든 거부된 프로미스가 페이지 오른쪽 아래 패널에 그려지고, 녹화가 이를 담습니다. 패널은 최근 메시지 여덟 개를 최신순으로 보여 주며 페이지 이동 후에도 유지되므로, 리뷰어는 워크스루가 진행되는 동안 콘솔을 함께 읽습니다. 이것들은 서버 플래그이므로 에이전트에 별도로 지시할 필요가 없습니다.

패널에 표시되는 내용은 세 가지 옵션으로 조정합니다. 각 옵션은 --console-overlay를 함의하므로, 핀만 지정해도 충분합니다.

패널은 애플리케이션이 console을 통해 남긴 로그와 잡히지 않은 오류, 거부된 프로미스를 다룹니다. 실패한 요청처럼 브라우저가 직접 기록하는 메시지는 console을 거치지 않으므로 패널에 나타나지 않습니다. 이런 메시지는 browser_console_messages와 browser_network_requests에서 확인합니다.

시작 프롬프트#

실제 브라우저에서 마이그레이션을 처음부터 끝까지 검증하고 녹화를 증적으로 남기려면, 다음 프롬프트를 에이전트에 복사해 사용합니다. 실행하기 전에 애플리케이션 이름, 피처 플래그, 시딩된 데이터 같은 세부 사항을 상황에 맞게 수정합니다.

에이전트에 전달할 시작 프롬프트
You are verifying a Vue 2 to Vue 3 (`@vue/compat`) migration for a Vue app, in a real browser,
with a recorded video as evidence.

Fill these in before you run this prompt:

- GDK URL: `http://gdk.test:3000`
- Feature flag: `<vue3_migrate_my_app>`
- Page that mounts the app: `<path>`
- Sign-in: the username and password of a local GDK account.
- Project or records to use: `<what I already prepared for you>`

Only ever use a local GDK account. Never pass me credentials for a shared or production
environment.

Follow these steps in order.

Constraints:

- You drive a real browser against my local GDK, signed in as a real user. Every interaction is a
  real write, so stay on the records I named above and tell me anything you changed.
- Do not modify application code to make a step pass. Report the failure instead.
- Do not create projects, users, or fixtures, and do not run seeders or migrations. Verify against
  what I prepared. If the checklist needs a record that does not exist, or the environment errors
  on you, stop and tell me. A broken local environment is not a migration finding.
- Record with the console overlay on, so the video carries the console instead of a separate log.
  The server needs these arguments, and each overlay option implies `--console-overlay`:

  ```shell
  --record-video --video-size 1280x800 --video-speed 1.5 \
    --console-overlay --console-overlay-pin "Using Vue.js"
  ```

  If the panel is too noisy to read, narrow it with `--console-overlay-match <regex>`. If you need
  a level that the default does not paint, name it with `--console-overlay-levels error,warn,log`.
  Prefer these options over filtering the output yourself. You cannot restart the server, so tell
  me when a flag has to change and wait.
- Use the `playwright-record` MCP server for all browser actions: `browser_navigate`,
  `browser_click`, `browser_type`, `browser_hover`, `browser_drag`, `browser_select_option`,
  `browser_press_key`, `browser_snapshot`, `browser_wait`, `browser_console_messages`,
  `browser_network_requests`, `browser_video_save`. It cannot evaluate arbitrary JavaScript, so
  read state from `browser_snapshot` and `browser_console_messages`, not from the DOM.

1. Read the code before you touch a browser. Work out from the diff and the component source what
   the app does: which components sit in the migrated dependency tree, every `$emit` and every
   `v-on` or `@` listener, the `emits:` declarations, props with default factory functions, scoped
   slots, `v-model` usage, Vue Router usage, and anything the Vue 3 migration is known to break.
   Read the component's Jest specs too. They enumerate the behavior, and they show which covered
   behavior a browser walkthrough does not reach.

   Then read the compatibility changes in the migration guide,
   <https://docs.gitlab.com/development/fe_guide/vue3_migration/#compatibility-changes>, and check
   the app against each one. That list is a starting point, not the whole surface: look for
   anything else the app depends on. Pay attention to the libraries aliased in
   `config/helpers/context_aliases_shared.js`, because they run through a Vue 3 shim, and give the
   ones this app imports their own checklist entries.

1. Build an explicit checklist of interactions from that reading, and share it with me before you
   run anything, so I can correct it. Group it into:
   - Main features: the app's primary user flows.
   - Event and emit paths: each emitted event and the observable result its listener produces.
   - Edge cases: empty state, error state, loading state, permission-restricted state, long or
     truncated content, and keyboard-only interaction.

1. Sign in at `/users/sign_in` with the credentials above, as your first browser action
   in every session. The GitLab session cookie does not survive a browser restart, so each
   recorded session signs in again. The sign-in form is itself a Vue app, so read a fresh
   `browser_snapshot` before each field: typing into one field re-renders the form and stales the
   reference to the other.

1. Confirm the page runs under Vue 3 before you verify anything. The pin keeps every
   `[gitlab] [V] Using Vue.js 3` line on screen for the whole recording, one per app root that
   started, so the video states which engine ran. Read that pinned block from `browser_snapshot`.
   If your app is missing from it, stop and report it. A walkthrough on Vue 2 proves nothing about
   the migration.

1. Run the checklist twice, in two separate recorded sessions: once with the feature flag off
   (Vue 2 baseline) and once with it on (Vue 3). A step that fails in both is an existing bug, not
   a migration regression.

1. For every interaction, assert an observable result, not just that the click happened. Examples:
   text that appears or changes, a row count, a URL query parameter, a request in
   `browser_network_requests`, an element that appears or disappears in `browser_snapshot`. If the
   handler for an event produces nothing observable on the page, call it out as unverifiable from
   the browser. Do not report it as passing.

1. Read the overlay after each step. Treat any error or `@vue/compat` warning that appears in the
   Vue 3 pass but not in the Vue 2 pass as a migration regression. `browser_console_messages` holds
   the full log for your report, including the messages the panel clipped.

1. Call `browser_video_save` with an `.mp4` filename as your last call in each session. It closes
   the browser to finalize the video, so no browser tool works after it. Then report the checklist
   with a pass or fail for each item, the console difference between the two passes, anything you
   could not verify from the browser, and the video path for each session. If `glab` is installed,
   upload the videos and attach them to the merge request.

유의 사항#

  • 영상은 에이전트가 수행한 상호작용이 해당 커밋에서 동작한다는 것을 보여 줄 뿐, 테스트는 아닙니다. 이후 변경에 대해 아무것도 다시 실행하지 않습니다. 수정한 동작은 계속 Jest 스펙으로 커버합니다.
  • 녹화에는 오디오가 없으므로 --video-speed 1.5로 세부 내용을 잃지 않으면서 클립을 짧게 만듭니다.

공통 마이그레이션 문제#

Vue Router props 반응성#

props 함수로 전달된 라우터 props는 Vue 3에서 반응형이 아닙니다. 대신 this.$route에서 값을 읽는 computed 속성을 사용합니다.

// Component - use computed property instead of props
computed: {
  currentPath() {
    return this.$route?.params.path || '';
  }
}

Watch 표현식#

$route 객체 전체가 아니라 문자열 경로로 특정 라우트 속성을 watch합니다. $route에 deep: true를 사용할 수도 있지만 성능 오버헤드가 생깁니다. 특정 속성을 watch하는 편이 더 효율적이고 컴포넌트의 의존성도 명확하게 드러냅니다.

watch: {
  '$route.params.path'() {
    this.fetchData();
  }
}

테스팅#

Vue 3 사용 중 실패하는 테스트를 구현하거나 수정하는 방법에 대한 자세한 내용은 Vue 3 테스팅 가이드를 참고합니다.

@vue/compat 패치 업데이트#

@vue/compat 패치를 업데이트하는 방법은 까다로울 수 있으므로 이 문서를 참고합니다.