Vue

Vue를 시작하려면 공식 문서를 읽어보세요.

예시

다음 섹션에서 설명하는 내용은 다음 예시에서 찾을 수 있습니다:

Vue 애플리케이션을 추가해야 하는 경우

가끔씩, HAML 페이지만으로 요구 사항을 충족하는 데 충분합니다. 이 명제는 주로 정적 페이지나 로직이 매우 적은 페이지에 대해 옳습니다. 페이지에 Vue 애플리케이션을 추가할 가치가 있는지 어떻게 알 수 있을까요? “애플리케이션 상태를 유지하고 렌더링된 페이지를 동기화해야 하는 경우”가 대답입니다.

이를 더 잘 설명하기 위해, 한 개의 토글이 있는 페이지를 상상해보죠. 그리고 이 토글을 전환하면 API 요청이 전송됩니다. 이 경우 유지하고 싶은 상태는 포함되지 않으며, 요청을 전송하고 토글을 전환합니다. 그러나, 첫 번째 토글과 항상 반대여야 하는 추가 토글을 하나 더 추가한다면, 우리는 _상태_가 필요합니다: 한 토글은 다른 토글의 상태를 “인식”해야 합니다. 일반 자바스크립트로 작성할 때, 이러한 로직은 일반적으로 DOM 이벤트를 청취하고 DOM 수정으로 반응하는 것을 포함합니다. Vue.js로 이러한 경우를 다루는 것이 훨씬 쉽기 때문에 여기에 Vue 애플리케이션을 만들어야 합니다.

Vue 애플리케이션이 필요할 것으로 신호를 보내는 몇 가지 플래그는 무엇인가요?

  • 여러 요소에 기반한 복잡한 조건문을 정의하고 사용자 상호 작용에 따라 업데이트해야 할 때
  • 모든 형태의 애플리케이션 상태를 유지하고 태그/요소 사이에서 공유해야 할 때
  • 미래에 복잡한 로직이 추가될 것으로 예상될 때 - 다음 단계에서 JS/HAML을 Vue로 다시 작성하는 것보다 기본 Vue 애플리케이션부터 시작하는 것이 더 쉽습니다.

페이지에 여러 Vue 애플리케이션 추가하지 않기

과거에는 페이지에 상호 작용성을 조금씩 추가하여 렌더링된 HAML 페이지의 여러 부분에 다양한 작은 Vue 애플리케이션을 추가했습니다. 그러나, 이 방식은 여러 가지 복잡성으로 이어졌습니다:

  • 대부분의 경우, 이러한 애플리케이션은 상태를 공유하지 않고 개별적으로 API 요청을 수행하여 요청의 수가 증가합니다.
  • 여러 엔드포인트를 통해 Rails에서 Vue로 데이터를 제공해야 합니다.
  • 페이지 구조가 유연하지 않아 Vue 애플리케이션을 동적으로 렌더링할 수 없으므로 페이지 구조가 제한적해집니다.
  • 레일스 라우팅을 대체하기 위해 클라이언트 측 라우팅을 완전히 활용할 수 없습니다.
  • 여러 애플리케이션은 예측할 수 없는 사용자 경험, 증가된 페이지 복잡성, 더 어려운 디버깅 과정으로 이어집니다.
  • 애플리케이션끼리의 통신 방식이 Web Vitals 수치에 영향을 줍니다.

이러한 이유로, 이미 존재하는 Vue 애플리케이션이 있는 페이지에 새로운 Vue 애플리케이션을 추가하는 데 신중해져야 합니다 (기존 또는 새로운 내비게이션은 이에 포함되지 않습니다). 새로운 애플리케이션을 추가하기 전에, 원하는 기능을 얻기 위해 기존 애플리케이션을 확장하는 것이 절대 불가능한지 확인하세요. 의문이 들 경우, #frontend 또는 #frontend-maintainers Slack 채널에서 아키텍처 상의 조언을 요청하세요.

새로운 애플리케이션을 추가해야 하는 경우, 기존 애플리케이션과 로컬 상태를 공유하도록 보장하세요(REST API를 사용하는 경우라면 Apollo Client나 Vuex를 선호합니다).

Vue 아키텍처

Vue 아키텍처로 이루고자 하는 주요 목표는 데이터 흐름이 한 가지만 있고, 데이터 입력도 한 가지만 있어야 하는 것입니다. 이 목표를 달성하기 위해 VuexApollo Client를 사용합니다.

Vue 문서의 상태 관리일방향 데이터 흐름에 관해 읽어볼 수도 있습니다.

컴포넌트와 리포지터리

Vue.js로 구현된 이슈 보드 같은 일부 기능들에서 관심사를 명확하게 구분할 수 있습니다:

new_feature
├── components
│   └── component.vue
│   └── ...
├── store
│  └── new_feature_store.js
├── index.js

일관성을 유지하기 위해 위 구조를 따르는 것을 권장합니다.

아래 각 항목들을 살펴봅시다:

index.js 파일

이 파일은 새로운 기능의 인덱스 파일입니다. 새로운 기능의 루트 Vue 인스턴스는 여기에 있어야 합니다.

리포지터리와 서비스를 이 파일에서 가져와 초기화하고 주요 컴포넌트에 속성으로 제공해야 합니다.

페이지별 JavaScript에 관해 읽어보세요.

부트스트래핑 주의사항

HAML에서 JavaScript로 데이터 제공

Vue 애플리케이션을 마운트하는 동안, 레일스에서 JavaScript로 데이터를 제공해야 할 수도 있습니다. 이를 위해, HTML 요소의 데이터 속성을 사용하고 애플리케이션을 마운트하는 동안 요소를 쿼리할 수 있습니다. HTML 요소의 데이터 속성은 문자열 값만을 수락할 수 있습니다, 따라서 다른 변수 유형을 문자열로 형변환해야 합니다.

주된 Vue 컴포넌트 내부에서 DOM을 쿼리하는 대신 propsrender 함수 내에서 provide를 사용하여 Vue 인스턴스로부터 DOM에 데이터를 제공하는 것은 유닛 테스트에서 픽스처나 HTML 요소를 생성하지 않게 해줘서 이점이 있습니다.

initSimpleApp 도우미

initSimpleApp는 Vue.js에서 컴포넌트를 마운트하는 과정을 간소화하는 도우미 함수입니다. 두 개의 인자, HTML 안에 존재하는 마운트 지점을 나타내는 선택자 문자열과 Vue 컴포넌트를 받습니다.

initSimpleApp를 사용하기 위해:

  1. 페이지에 ID나 고유한 클래스를 가진 HTML 요소를 포함합니다.
  2. JSON 개체를 포함하는 data-view-model 속성을 추가합니다.
  3. 원하는 Vue 컴포넌트를 가져와 유효한 CSS 선택자 문자열과 함께 initSimpleApp에 전달합니다. 이 문자열은 지정된 위치에 컴포넌트를 마운트합니다.

initSimpleAppdata-view-model 속성의 내용을 JSON 개체로 자동으로 검색하여 마운트된 Vue 컴포넌트에 속성으로 전달합니다. 이를 통해 컴포넌트를 데이터로 미리 채울 수 있습니다.

예시:

//my_component.vue
<template>
  <div>
    <p>Prop1: {{ prop1 }}</p>
    <p>Prop2: {{ prop2 }}</p>
  </div>
</template>

<script>
export default {
  name: 'MyComponent',
  props: {
    prop1: {
      type: String,
      required: true
    },
    prop2: {
      type: Number,
      required: true
    }
  }
}
</script>
<div id="js-my-element" data-view-model='{"prop1": "my object", "prop2": 42 }'></div>
//index.js
import MyComponent from './my_component.vue'
import { initSimpleApp } from '~/helpers/init_simple_app_helper'

initSimpleApp('#js-my-element', MyComponent)
provideinject

Vue는 provideinject를 통해 의존성 주입을 지원합니다. inject 구성은 provide가 전파한 값을 접근합니다. 다음 Vue 애플리케이션 초기화의 예시는 provide 구성이 HAML에서 컴포넌트로 값을 전달하는 것을 보여줍니다:

#js-vue-app{ data: { endpoint: 'foo' }}

// index.js
const el = document.getElementById('js-vue-app');

if (!el) return false;

const { endpoint } = el.dataset;

return new Vue({
  el,
  name: 'MyComponentRoot',
  render(createElement) {
    return createElement('my-component', {
      provide: {
        endpoint
      },
    });
  },
});

컴포넌트나 그 하위 컴포넌트는 다음과 같이 inject를 통해 속성에 접근할 수 있습니다:

<script>
  export default {
    name: 'MyComponent',
    inject: ['endpoint'],
    ...
    ...
  };
</script>
<template>
  ...
  ...
</template>

의존성 주입은 아래 조건이 충족될 때 이상적입니다:

  • 주입된 값이 데이터 형식이나 내용에 대해 명시적인 유효성 검사가 필요하지 않을 때
  • 값이 반응적일 필요가 없을 때
  • 같은 속성을 계층 구조 내에서 여러 컴포넌트에서 사용해야 하는 경우, 코드 작성이 번거로울 때.

의존성 주입은 부모 컴포넌트가 provide로 값을 전달하지 않았을 때, inject 구성에서 기본값을 정의하지 않으면 자식 컴포넌트(즉시 자식이거나 여러 계층이 깊은)를 망가뜨릴 수 있습니다.

기본값은 상황에 맞는 경우에 유용할 수 있습니다.

props

만약 HAML에서 값이 의존성 주입의 기준에 맞지 않는다면 props를 사용하세요. 다음 예시를 참고하세요.

// haml
#js-vue-app{ data: { endpoint: 'foo' }}

// index.js
const el = document.getElementById('js-vue-app');

if (!el) return false;

const { endpoint } = el.dataset;

return new Vue({
  el,
  name: 'MyComponentRoot',
  render(createElement) {
    return createElement('my-component', {
      props: {
        endpoint
      },
    });
  },
});
note
Vue 앱을 마운트하기 위해 id 속성을 추가할 때, 이 id가 코드 베이스 전체에서 고유한지 확인하세요.

Vue 앱으로 전달되는 데이터를 명시적으로 선언하는 이유에 대해 더 알아보려면 Vue 스타일 가이드를 참조하세요.

Vue 애플리케이션에 Rails 폼 필드 제공

Rails로 폼을 작성할 때, 폼 입력의 name, id, value 속성은 백엔드와 일치하도록 생성됩니다. Rails 폼을 Vue로 변환하거나 컴포넌트를 통합할 때, 생성된 속성에 접근하는 것이 도움이 됩니다. 생성된 폼 입력 속성을 Vue 애플리케이션에 전달할 수 있도록 parseRailsFormFields 유틸리티를 사용할 수 있습니다. 이를 통해 폼 제출 방식을 변경하지 않고도 Vue 컴포넌트를 통합할 수 있습니다.

-# form.html.haml
= form_for user do |form|
  .js-user-form
    = form.text_field :name, class: 'form-control gl-form-input', data: { js_name: 'name' }
    = form.text_field :email, class: 'form-control gl-form-input', data: { js_name: 'email' }

js_name 데이터 속성은 결과 JavaScript 객체의 키로 사용됩니다. 예를 들어 = form.text_field :email, data: { js_name: 'fooBarBaz' }{ fooBarBaz: { name: 'user[email]', id: 'user_email', value: '' } }로 변환됩니다.

// index.js
import Vue from 'vue';
import { parseRailsFormFields } from '~/lib/utils/forms';
import UserForm from './components/user_form.vue';

export const initUserForm = () => {
  const el = document.querySelector('.js-user-form');
  
  if (!el) {
    return null;
  }
  
  const fields = parseRailsFormFields(el);
  
  return new Vue({
    el,
    name: 'UserFormRoot',
    render(h) {
      return h(UserForm, {
        props: {
          fields,
        },
      });
    },
  });
};
<script>
// user_form.vue
import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';

export default {
  name: 'UserForm',
  components: { GlButton, GlFormGroup, GlFormInput },
  props: {
    fields: {
      type: Object,
      required: true,
    },
  },
};
</script>

<template>
  <div>
    <gl-form-group :label-for="fields.name.id" :label="__('Name')">
      <gl-form-input v-bind="fields.name" width="lg" />
    </gl-form-group>
    
    <gl-form-group :label-for="fields.email.id" :label="__('Email')">
      <gl-form-input v-bind="fields.email" type="email" width="lg" />
    </gl-form-group>
    
    <gl-button type="submit" category="primary" variant="confirm">{{ __('Update') }}</gl-button>
  </div>
</template>

gl 객체에 접근

애플리케이션의 라이프사이클 동안 변경되지 않는 데이터를 DOM과 동일한 위치에서 gl 객체를 쿼리합니다. 이렇게 함으로써 gl 객체를 가로지르지 않고 테스트를 쉽게 할 수 있습니다. Vue 인스턴스를 초기화할 때 수행하고, 데이터를 주요 컴포넌트에 props로 제공해야 합니다.

return new Vue({
  el: '.js-vue-app',
  name: 'MyComponentRoot',
  render(createElement) {
    return createElement('my-component', {
      props: {
        avatarUrl: gl.avatarUrl,
      },
    });
  },
});

능력에 접근

능력을 프론트엔드에 푸시한 후에는 Vue에서 provide and inject 메커니즘을 사용하여 능력을 자식 컴포넌트에서 사용할 수 있도록합니다. glAbilities 객체는 이미 commons/vue.js에서 제공되므로 믹스인만 사용하면 됩니다.

// 임의의 자식 컴포넌트

import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin';

export default {
  // ...
  mixins: [glAbilitiesMixin()],
  // ...
  created() {
    if (this.glAbilities.someAbility) {
      // ...
    }
  },
}

피처 플래그에 접근

피처 플래그를 프론트엔드에 푸시한 후에는 Vue에서 provide and inject 메커니즘을 사용하여 피처 플래그를 자식 컴포넌트에서 사용할 수 있도록합니다. glFeatures 객체는 이미 commons/vue.js에서 제공되므로 믹스인만 사용하면 됩니다.

// 임의의 자식 컴포넌트

import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';

export default {
  // ...
  mixins: [glFeatureFlagsMixin()],
  // ...
  created() {
    if (this.glFeatures.myFlag) {
      // ...
    }
  },
}

이 방식은 몇 가지 이점이 있습니다:

  • 중간 컴포넌트가 알지 못하고도 임의의 깊이에 중첩된 컴포넌트에서 플래그를 선택하여 액세스할 수 있습니다 (예: 플래그를 props를 통해 하향식으로 전달하지 않음).
  • vue-test-utilsmount/shallowMount에서 플래그를 prop으로 제공하여 좋은 테스트 가능성을 제공할 수 있습니다.

    import { shallowMount } from '@vue/test-utils';
      
    shallowMount(component, {
      provide: {
        glFeatures: { myFlag: true },
      },
    });
    
  • 페이지가 새로 고쳐질 때 기본적으로 알림이 지워집니다. 페이지에서 알림을 유지하려면 persistOnPages 키를 Rails 컨트롤러 작업 배열로 설정할 수 있습니다. Rails 컨트롤러 작업을 확인하려면 콘솔에서 document.body.dataset.page를 실행하세요.

예시:

visitUrlWithAlerts('/dashboard/groups', [
  {
    id: 'resource-building-in-background',
    message: 'Resource is being built in the background.',
    variant: 'info',
    persistOnPages: ['dashboard:groups:index'],
  },
])

지속적인 알림을 매뉴얼으로 제거해야 하는 경우 removeGlobalAlertById 유틸을 사용할 수 있습니다.

컴포넌트용 폴더

이 폴더에는 이 새로운 기능에 특화된 모든 컴포넌트가 포함됩니다. 어떤 곳에서든 사용될 가능성이 높은 컴포넌트를 사용하거나 생성하려면 vue_shared/components를 참조하세요.

컴포넌트를 만들어야 하는 시점을 아는 좋은 가이드라인은 해당 컴포넌트가 다른 곳에서 재사용될 수 있는지를 고려하는 것입니다.

예를 들어, 테이블은 GitLab 전역에서 많은 곳에서 사용되는데, 테이블이 컴포넌트에 잘 맞을 것입니다. 반대로, 한 테이블에서만 사용되는 테이블 셀은 이 패턴을 사용하는 데 좋은 사례가 아닙니다.

Vue.js 사이트의 Component System에서 컴포넌트에 대해 더 읽어볼 수 있습니다.

스토어용 폴더

Vuex

자세한 내용은 이 페이지를 확인하세요.

Vue Router

페이지에 Vue Router를 추가하려면 다음을 수행하세요:

  1. 와일드카드로 *vueroute라는 이름을 사용하여 Rails 라우트 파일에 catch-all route를 추가하세요.

    # ee/config/routes/project.rb의 예시
       
    resources :iteration_cadences, path: 'cadences(/*vueroute)', action: :index
    

    위 예시는 path의 시작 부분과 일치하는 모든 경로에 대해 iteration_cadences 컨트롤러에서 index 페이지를 제공합니다. 예를 들어, groupname/projectname/-/cadences/123/456/와 일치하는 경로입니다.

  2. 기본 경로(*vueroute 앞의 모든 것)를 프론트엔드에 전달하여 Vue Router를 초기화하는 데 사용하세요.

    .js-my-app{ data: { base_path: project_iteration_cadences_path(project) } }
    
  3. 라우터를 초기화하세요.

    Vue.use(VueRouter);
       
    export function createRouter(basePath) {
      return new VueRouter({
        routes: createRoutes(),
        mode: 'history',
        base: basePath,
      });
    }
    
  4. * 경로에 대한 인식되지 않은 경로에 대한 대체 추가. 다음 중 하나:

    • 라우트 배열 끝에 리디렉션을 추가하세요.

      const routes = [
        {
          path: '/',
          name: 'list-page',
          component: ListPage,
        },
        {
          path: '*',
          redirect: '/',
        },
      ];
      
    • 라우트 배열 끝에 대체 컴포넌트를 추가하세요.

      const routes = [
        {
          path: '/',
          name: 'list-page',
          component: ListPage,
        },
        {
          path: '*',
          component: NotFound,
        },
      ];
      
  5. 선택 사항. 자식 경로에 대한 경로 보조 프로그램을 사용할 수 있도록, 부모 컨트롤러에 controlleraction 매개변수를 추가하세요.

    resources :iteration_cadences, path: 'cadences(/*vueroute)', action: :index do
      resources :iterations, only: [:index, :new, :edit, :show], constraints: { id: /\d+/ }, controller: :iteration_cadences, action: :index
    end
    

    이렇게 하면 /cadences/123/iterations/456/edit와 같은 경로를 백엔드에서 확인할 수 있고, 예를 들어 그룹 또는 프로젝트 멤버십을 확인할 수 있습니다. 또한 _path 도우미를 사용할 수 있게 되므로, 경로의 *vueroute 부분을 매뉴얼으로 작성하지 않고도 피처 스펙에서 페이지를 로드할 수 있습니다.

Vue 및 jQuery 혼용

  • Vue와 jQuery를 혼용하는 것은 권장되지 않습니다.
  • Vue에서 특정 jQuery 플러그인을 사용하려면 그것을 감싸는 래퍼를 생성하세요.
  • jQuery 이벤트 리스너를 통해 기존의 jQuery 이벤트를 Vue가 수신하는 데는 문제가 없습니다.
  • Vue가 jQuery와 상호 작용하는 데 새로운 jQuery 이벤트를 추가하는 것은 권장되지 않습니다.

Vue 및 JavaScript 클래스 혼용(데이터 함수 내에서)

Vue 문서에서는 데이터 함수/객체를 다음과 같이 정의합니다.

Vue 인스턴스의 데이터 객체. Vue는 재귀적으로 속성을 getter/setter로 변환하여 “반응적”으로 만듭니다. 객체는 평범해야 합니다: 브라우저 API 객체나 프로토타입 속성 등은 무시됩니다. 데이터는 데이터 그 자체여야 하는데, 자체적인 상태를 가진 객체를 관찰하는 것은 권장되지 않습니다.

Vue의 가이드에 따라:

  • 데이터 함수에서 JavaScript 클래스를 사용하거나 생성하지 마세요.
  • 새로운 JavaScript 클래스 구현을 추가하지 마세요.
  • 기본 타입이나 객체를 사용할 수 없는 경우에는 GraphQL, Vuex 또는 일련의 컴포넌트를 사용하세요.
  • 해당 접근 방법을 사용하여 기존 구현을 유지하세요.
  • 컴포넌트가 상당히 변경되는 경우 순수 객체 모델로 마이그레이션하세요.
  • 비즈니스 로직을 헬퍼 또는 유틸리티에 추가하여 컴포넌트와 별도로 테스트할 수 있도록 하세요.

바이너리 큰 코드베이스에서 JavaScript 클래스를 가지고 있는 것이 유지 관리에 문제가 되는 이유:

  • 클래스를 만든 후에는 Vue 반응성과 최고의 관행을 침해할 수 있게 확장할 수 있습니다.
  • 클래스는 추상화 계층을 추가하여 컴포넌트 API와 내부 작업을 덜 명확하게 만듭니다.
  • 테스트하기 어려워집니다. 클래스는 컴포넌트 데이터 함수에서 인스턴스화되기 때문에 컴포넌트와 클래스를 따로 ‘관리’하는 것이 어려워집니다.
  • 함수형 코드베이스에 객체 지향 원칙(OOP)을 추가하면 코드를 작성하는 또 다른 방법이 추가되어 일관성과 명확성이 줄어듭니다.

스타일 가이드

Vue 컴포넌트 및 템플릿을 작성하고 테스트할 때의 최고의 관행에 대해서는 스타일 가이드의 Vue 섹션을 참조하세요.

Composition API

Vue 2.7에서는 Vue 컴포넌트 및 독립형 composables에서 Composition API를 사용할 수 있습니다.

<script>보다 <script setup> 선호

Composition API를 <script> 섹션에 로직을 배치하거나 전용 <script setup> 섹션을 가질 수 있습니다. setup() 속성을 사용하여 컴포넌트에 Composition API를 추가해야 합니다.

<script>
  import { computed } from 'vue';
  
  export default {
    name: 'MyComponent',
    setup(props) {
      const doubleCount = computed(() => props.count*2)
    }
  }
</script>

v-bind 제한사항

절대 필요한 경우가 아니라면 v-bind="$attrs"를 사용하지 마세요. 이것은 네이티브 컨트롤 래퍼를 개발할 때 필요할 수 있습니다. (이것은 gitlab-ui 컴포넌트에 적합한 후보입니다.) 다른 경우에는 항상 props 및 명시적인 데이터 흐름을 사용하세요.

v-bind="$attrs" 사용은 다음을 야기시킵니다:

  1. 컴포넌트의 계약이 손실됩니다. props는 특히 이 문제를 해결하기 위해 설계되었습니다.
  2. 트리 내 각 컴포넌트의 높은 유지보수 비용. v-bind="$attrs"는 데이터 흐름을 이해하기 위해 컴포넌트 트리 전체를 스캔해야 하므로 특히 디버깅이 어렵습니다.
  3. Vue 3로의 마이그레이션 중 문제. Vue 3의 $attrs에는 이벤트 리스너가 포함되어 있어서 Vue 3 마이그레이션이 완료된 후 예상치 못한 부작용을 일으킬 수 있습니다.

컴포넌트당 하나의 API 스타일을 목표로

Vue 컴포넌트에 setup() 속성을 추가할 때, 가독성과 유지보수성을 위해 이를 Composition API로 전환할 수 있도록 고려해야 합니다. 특히 큰 컴포넌트에서는 항상 그러한 것은 아니지만, 각 컴포넌트당 하나의 API 스타일을 목표로 해야 합니다.

Composables

Composition API를 사용하면 반응적 상태를 포함한 로직을 추상화하는 새로운 방법으로 _composables_를 사용할 수 있습니다. Composable은 매개변수를 받아 반응적 속성과 메서드를 반환하는 함수입니다.

// useCount.js
import { ref } from 'vue';

export function useCount(initialValue) {
  const count = ref(initialValue)
  
  function incrementCount() {
    count.value += 1
  }
  
  function decrementCount() {
    count.value -= 1
  }
  
  return { count, incrementCount, decrementCount }
}
// MyComponent.vue
import { useCount } from 'useCount'

export default {
  name: 'MyComponent',
  setup() {
    const { count, incrementCount, decrementCount } = useCount(5)
    
    return { count, incrementCount, decrementCount }
  }
}

use 접두어와 파일 이름

Vue에서 composables의 보편적인 네이밍 규칙은 그들을 use로 접두어를 붙이고 간단히 composable functionality를 참조하는 것입니다 (useBreakpoints, useGeolocation 등). 이 규칙은 .js 파일에도 적용됩니다 - 파일에 하나 이상의 composable이 포함되어 있더라도 use_로 시작해야 합니다.

라이프사이클 함정 피하기

컴포저블을 작성할 때 가능한 한 간단하게 유지하도록 노력해야 합니다. 라이프사이클 훅은 composable에 복잡성을 추가하고 예상치 못한 부작용을 일으킬 수 있습니다. 이를 피하기 위해 다음 원칙을 준수해야 합니다:

  • 가능한 경우 라이프사이클 훅 사용을 최소화하고 대신 콜백을 받거나 반환하는 것을 선호해야 합니다.
  • Composable에 라이프사이클 훅이 필요한 경우, 해당 composable 내에서 onMounted에 리스너를 추가한 경우 onUnmounted에서 제거해야 합니다.
  • 항상 즉시 라이프사이클 훅을 설정해야 합니다:
// 나쁨
const useAsyncLogic = () => {
  const action = async () => {
    await doSomething();
    onMounted(doSomethingElse);
  };
  return { action };
};

// 괜찮음
const useAsyncLogic = () => {
  const done = ref(false);
  onMounted(() => {
    watch(
      done,
      () => done.value && doSomethingElse(),
      { immediate: true },
    );
  });
  const action = async () => {
    await doSomething();
    done.value = true;
  };
  return { action };
};

탈출로 방지하기

Vue에서 제공하는 탈출로를 사용하여 블랙박스처럼 모든 작업을 수행하는 composable을 작성하는 것이 유혹스러울 수 있습니다. 그러나 대부분의 경우 이는 너무 복잡하고 유지 관리하기 어렵게 만들 수 있습니다. getCurrentInstance 메서드가 하나의 탈출로입니다. 이 메서드는 현재 렌더링 컴포넌트의 인스턴스를 반환합니다. 이 메서드 대신 composable로 데이터나 메서드를 전달하는 것을 선호해야 합니다.

const useSomeLogic = () => {
  doSomeLogic();
  getCurrentInstance().emit('done'); // 안 좋음
};
const done = () => emit('done');

const useSomeLogic = (done) => {
  doSomeLogic();
  done(); // 좋음, composable이 지나치게 똑똑하게 되려고 하지 않음
}

Composables와 Vuex

Composable에서 Vuex 상태를 사용하는 것을 피하는 것이 좋습니다. 사용이 불가피한 경우 props를 사용하여 해당 상태를 받고, setup에서 Vuex 상태를 업데이트하기 위해 이벤트를 emit해야 합니다. 부모 컴포넌트는 해당 상태를 Vuex에서 가져와서 자식 컴포넌트에서 emit된 이벤트를 통해 해당 상태를 변경해야 합니다. prop으로 내려오는 상태를 변이해서는 절대로 안 됩니다. Composable이 Vuex 상태를 변이해야 하는 경우 이벤트를 emit하기 위해 콜백을 사용해야 합니다.

const useAsyncComposable = ({ state, update }) => {
  const start = async () => {
    const newState = await doSomething(state);
    update(newState);
  };
  return { start };
};

const ComponentWithComposable = {
  setup(props, { emit }) {
    const update = (data) => emit('update', data);
    const state = computed(() => props.state); // Vuex에서 가져온 상태
    const { start } = useAsyncComposable({ state, update });
    start();
  },
};

Composables 테스트

Vue 컴포넌트 테스트

Vue 컴포넌트의 경우 각각 고유한 출력을 가지고 있습니다. 이 출력은 항상 렌더 함수에 존재합니다.

Vue 컴포넌트의 각각의 메소드를 개별적으로 테스트할 수 있지만, 우리의 목표는 항상 상태를 나타내는 렌더 함수의 출력을 테스트하는 것입니다.

도움이 될 수 있는 예시는 아래와 같습니다.

올바르게 구조화된 유닛 테스트를 위한 예시:

import { GlLoadingIcon } from '@gitlab/ui';
// 중략

하위 컴포넌트

  1. 자식 컴포넌트를 렌더링하는 방법을 정의하는 지시문을 테스트하세요 (예: v-if, v-for).
  2. 자식 컴포넌트에 전달하는 props를 테스트하세요 (특히 테스트 중인 컴포넌트에서 계산되는 경우 computed 속성을 사용하는 props). .vm.someProp이 아닌 .props()를 사용하는 것을 기억하세요.
  3. 자식 컴포넌트에서 발송된 이벤트에 올바르게 반응하는지 테스트하세요.
  const checkbox = wrapper.findByTestId('checkboxTestId');
  
  expect(checkbox.attributes('disabled')).not.toBeDefined();
  
  findChildComponent().vm.$emit('primary');
  await nextTick();
  
  expect(checkbox.attributes('disabled')).toBeDefined();
  1. 자식 컴포넌트의 내부 구현을 테스트해서는 안 됩니다.
  // 나쁨
  expect(findChildComponent().find('.error-alert').exists()).toBe(false);
  
  // 좋음
  expect(findChildComponent().props('withAlertContainer')).toBe(false);

이벤트

우리는 컴포넌트에서의 액션에 대한 응니으로 발생하는 이벤트를 테스트해야 합니다. 이 테스트는 올바른 이벤트가 올바른 매개변수와 함께 발생하는지를 검증합니다.

원시 DOM 이벤트의 경우 trigger를 사용하여 이벤트를 발생시켜야 합니다.

// SomeButton이 <button>Some button</button>을 렌더링한다고 가정합니다.
wrapper = mount(SomeButton);

...
it('클릭 이벤트를 발생해야 합니다', () => {
  const btn = wrapper.find('button')
  
  btn.trigger('click');
  ...
})

Vue 이벤트를 발생시킬 때는 emit를 사용하세요.

wrapper = shallowMount(DropdownItem);

...

it('itemClicked 이벤트를 발생해야 합니다', () => {
  DropdownItem.vm.$emit('itemClicked');
  ...
})

우리는 이벤트가 발생했는지를 확인하기 위해 emitted() 메서드의 결과에 대한 단언을 해야 합니다.

자식 컴포넌트에서 이벤트를 발생시킬 때는 vm.$emit을 사용하는 것이 좋은 실천법입니다.

컴포넌트에서 trigger를 사용하는 것은 그것을 백 상자로 취급하는 것을 의미합니다: 우리는 자식 컴포넌트의 루트 요소가 원시 click 이벤트를 가졌다고 가정합니다. 또한, trigger를 사용하면 Vue3 모드에서 자식 컴포넌트에서 일부 테스트가 실패합니다.

   const findButton = () => wrapper.findComponent(GlButton);
   
   // bad
   findButton().trigger('click');
   
   // good
   findButton().vm.$emit('click');

Vue.js 전문가 역할

Vue.js 전문가로 지원해야 하는 것은 본인의 Merge Request 및 리뷰가 보여주는 경우에만 할 수 있습니다:

  • Vue 및 Vuex 반응성에 대한 심층적인 이해
  • Vue 및 Vuex 코드가 공식 가이드라인과 우리의 가이드라인에 따라 구조화되어 있는 것을 이해
  • Vue 및 Vuex 애플리케이션을 테스트하는 데 대한 완벽한 이해
  • Vuex 코드가 문서화된 패턴을 따르는 것
  • 기존의 Vue 및 Vuex 애플리케이션과 기존의 재사용 가능한 컴포넌트에 대한 지식

Vue 2 -> Vue 3 마이그레이션

  • 이 섹션은 코드베이스를 Vue 2.x에서 Vue 3.x로 마이그레이션하는 노력을 지원하기 위해 임시로 추가되었습니다.

기술 부채를 늘리지 않기 위해 코드베이스에 특정 기능을 추가하는 것을 최소화하는 것을 권장합니다:

  • 필터;
  • 이벤트 버스;
  • 기능 템플릿
  • slot 속성

VUE 3로의 마이그레이션에서 자세한 내용을 찾을 수 있습니다.

부록 - 테스트 대상 Vue 컴포넌트

Vue 컴포넌트 테스트 섹션에서 테스트된 예제 컴포넌트의 템플릿입니다:

<template>
  <div class="content">
    <gl-loading-icon v-if="isLoading" />
    <template v-else>
      <div
        v-for="todo in todos"
        :key="todo.id"
        :class="{ 'gl-strike': todo.isDone }"
        data-testid="todo-item"
      >{{ todo.text }}</div>
      <footer class="gl-border-t-1 gl-mt-3 gl-pt-3">
        <gl-form-input
          type="text"
          v-model="todoText"
          data-testid="text-input"
        >
        <gl-button
          variant="confirm"
          data-testid="add-button"
          @click="addTodo"
        >Add</gl-button>
      </footer>
    </template>
  </div>
</template>