- 예시
- Vue 애플리케이션을 추가해야 할 때
- 페이지에 여러 Vue 애플리케이션 추가 피하기
- Vue 아키텍처
- 스타일 가이드
- 컴포지션 API
- Vue 구성 요소 테스트
- Vue.js 전문가 역할
- Vue 2 -> Vue 3 마이그레이션
- 부록 - Vue 구성 요소 테스트 대상
Vue
Vue를 시작하려면, 그들의 문서를 읽어보세요.
예시
다음 섹션에서 설명한 내용은 다음 예시에서 찾을 수 있습니다:
Vue 애플리케이션을 추가해야 할 때
가끔은 HAML 페이지가 요구 사항을 충족하는 데 충분합니다. 이 문장은 주로 정적 페이지나 로직이 매우 적은 페이지에 대해 올바릅니다. 그러나 페이지에 애플리케이션 상태를 유지하고 렌더링된 페이지와 동기화해야 하는 경우에는 어떻게 알 수 있을까요?
이 질문에 대한 답은 “애플리케이션 상태를 유지하고 렌더링된 페이지를 동기화해야 할 때”입니다.
이를 더 잘 설명하기 위해, 한 가지 토글이있는 페이지를 상상해 보겠습니다. 그것을 토글하면 API 요청이 전송됩니다. 이 경우 유지해야 할 상태는 없으며 요청을 보내고 토글을 전환합니다. 그러나 첫 번째 토글과 항상 반대여야 하는 추가 토글을 추가하면 어느 쪽의 상태를 다른 쪽이 “인식”해야 합니다. 순수 JavaScript로 작성할 때 이러한 로직은 일반적으로 DOM 이벤트를 수신하고 DOM을 수정하여 반응하는 것을 포함합니다. 이러한 경우는 Vue.js로 훨씬 쉽게 처리할 수 있으므로 이곳에 Vue 애플리케이션을 만들어야 합니다.
Vue 애플리케이션이 필요한지를 나타내는 신호는 무엇인가요?
- 여러 가지 요인에 따라 정의된 복잡한 조건부를 정의하고 사용자 상호 작용에 따라 업데이트해야 할 때
- 어떤 형식의 애플리케이션 상태를 유지하고 태그/요소 사이에 공유해야 할 때
- 미래에 복잡한 로직이 추가될 것으로 예상될 때 - 기본 Vue 애플리케이션으로 시작하는 것이 JS/HAML을 다음 단계에서 Vue로 재작성해야 하는 것보다 쉽습니다.
페이지에 여러 Vue 애플리케이션 추가 피하기
과거에는 페이지에 상호 작용성을 조각조각 추가하여 렌더링된 HAML 페이지의 다른 부분에 여러 개의 작은 Vue 애플리케이션을 추가했었습니다. 그러나 이러한 접근 방식은 여러 가지 복잡성으로 이어졌습니다:
- 대부분의 경우, 이러한 애플리케이션은 상태를 공유하지 않고 독립적으로 API 요청을 수행하여 요청 수가 늘어났습니다.
- 여러 엔드포인트를 사용하여 데이터를 Rails에서 Vue로 제공해야 했습니다.
- 페이지 구조가 강제화되어 동적으로 Vue 애플리케이션을 렌더링할 수 없었으므로,
- Rails 라우팅을 대체하는 클라이언트 측 라우팅을 완전히 활용할 수 없었습니다.
- 여러 애플리케이션은 예측할 수 없는 사용자 경험, 증가된 페이지 복잡성, 더 어려운 디버깅 프로세스로 이어졌습니다.
- 애플리케이션이 서로 통신하는 방식은 웹 핵심 숫자에 영향을 미칩니다.
이러한 이유로, 이미 존재하는 Vue 애플리케이션이 있는 페이지에 새로운 Vue 애플리케이션을 추가하기 전에 신중해야 합니다(이전 또는 새로운 탐색은이에 해당하지 않음). 새로운 앱을 추가하기 전에 기존 애플리케이션을 확장하여 원하는 기능을 구현하는 것이 절대 힘들다는 것을 확신하십시오. 의심스러울 때는 #frontend
또는 #frontend-maintainers
Slack 채널에서 구조적인 조언을 요청할 수 있습니다.
새로운 애플리케이션을 추가해야 하는 경우, 꼭 기존 애플리케이션과 로컬 상태를 공유하는지 확인하십시오. 읽기: 어떤 상태 관리자를 사용해야 알아야하나요?
Vue 아키텍처
Vue 아키텍처를 통해 달성하려는 주요 목표는 하나의 데이터 흐름과 하나의 데이터 입력만 있어야 합니다. 이 목표를 달성하기 위해 Pinia 또는 Apollo Client를 사용합니다.
Vue 문서에서 상태 관리 및 단방향 데이터 흐름에 대해 이 아키텍처에 대해 자세히 읽을 수도 있습니다.
컴포넌트 및 저장소
Vue.js로 구현 된 일부 기능, 예를 들어 이슈 보드 나 환경 테이블과 같은 기능에서는 관심사의 분리가 명확하게 나타납니다:
new_feature
├── components
│ └── component.vue
│ └── ...
├── store
│ └── new_feature_store.js
├── index.js
일관성을 위해 위와 동일한 구조를 따르는 것을 권장합니다.
각각을 살펴보겠습니다:
index.js
파일
이 파일은 새 기능의 색인 파일입니다. 새 기능의 루트 Vue 인스턴스가 여기에 있어야 합니다.
Store와 Service는 이 파일에서 가져와 초기화되어 주요 구성 요소에 대한 속성으로 제공되어야 합니다.
페이지별 JavaScript에 대해 자세히 읽어보세요.
부트스트래핑의 함정
HAML에서 자바 스크립트로 데이터 제공
Vue 애플리케이션을 마운트하는 동안 Rails에서 JavaScript로 데이터를 제공해야 할 수 있습니다. 이를 위해 HTML 요소의 data
속성을 사용하여 애플리케이션을 마운트하는 동안 HTML 요소 내의 데이터를 쿼리할 수 있습니다.
애플리케이션을 초기화하는 동안에만 수행해야 합니다, because Vue에서 생성된 DOM을 사용하여 마운트된 요소로 대체합니다.
data
속성은 문자열 값만 허용할 수 있으므로 다른 변수 유형을 문자열로 캐스팅하거나 변환해야 합니다.
주요 Vue 구성 요소 내에서 DOM을 쿼리하는 대신 props
또는 render
함수의 provide
를 사용하여 DOM에서 Vue 인스턴스로 데이터를 제공하는 것의 장점은 유닛 테스트에서 fixture 또는 HTML 요소를 만들지 않도록 피하게 됩니다.
initSimpleApp
도우미
initSimpleApp
은 Vue.js에서 구성 요소를 마운트하는 프로세스를 스트림라인으로 만드는 도우미 함수입니다. 셀렉터 문자열과 Vue 구성 요소를 나타내는 두 개의 인수를 받습니다.
initSimpleApp
을 사용하려면:
- 페이지에 ID 또는 고유 클래스가 있는 HTML 요소를 포함합니다.
- 유효한 CSS 선택기 문자열과 함께 원하는 Vue 구성 요소를 가져와
initSimpleApp
를 사용합니다. 이 문자열은 지정된 위치에 구성 요소를 마운트합니다.
initSimpleApp
은 data-view-model
속성의 내용을 JSON 객체로 가져와 마운트된 Vue 구성 요소에 props으로 전달합니다. 이를 사용하여 구성 요소를 데이터로 미리 채울 수 있습니다.
예시:
//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)
provide
및 inject
Vue는 provide
및 inject
을 통해 의존성 주입을 지원합니다.
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>
의존성 주입을 사용하여 HAML에서 값 제공하는 경우에 이상적입니다:
- 주입된 값이 데이터 유형 또는 내용에 대한 명시적인 유효성 검사가 필요하지 않은 경우
- 값이 반응적이 필요하지 않은 경우
- 동일한 속성이 계층 구조의 모든 구성 요소를 통과하여 실제로 사용하는 구성 요소에 이르기까지 속성을 전달해야 하는 번거로움을 피해야 하는 경우
의존성 주입은 다음 두 가지 조건이 모두 참인 경우에는 하위 구성 요소(즉, 즉시 하위 또는 여러 수준 이하)를 손상시킬 수 있습니다:
-
inject
구성에서 선언된 값에 기본값이 정의되어 있지 않은 경우 - 부모 구성 요소가
provide
구성을 사용하여 값을 제공하지 않은 경우
기본 값은 적절한 문맥에서 유용할 수 있습니다.
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
},
});
},
});
참고:
Vue 애플리케이션을 마운트하기 위해 id
속성을 추가하는 경우 코드베이스 전체에서 이 id
가 고유한지 확인하세요.
Vue 앱으로 전달되는 데이터를 명시적으로 선언하는 이유에 대한 자세한 내용은 Vue 스타일 가이드를 참조하세요.
Rails 양식 필드를 Vue 애플리케이션에 제공
Rails로 양식을 작성할 때, 양식 입력의 name
, id
, 및 value
속성은 백엔드와 일치하도록 생성됩니다.
Rails 양식을 Vue로 변환하거나 구성 요소를 통합할 때 이러한 생성된 속성에 액세스하는 것이 유용할 수 있습니다.
parseRailsFormFields
유틸리티를 사용하여 생성된 양식 입력 속성을 구문 분석한 후 Vue 애플리케이션으로 전달될 수 있습니다.
이를 통해 양식이 전송되는 방식을 변경하지 않고 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
객체에 액세스
Vue 앱 생명 주기 동안 변경되지 않는 데이터에 대해 gl
객체를 쿼리합니다.
이러한 관행을 따르면 gl
객체를 가장 쉽게 모의(mock)화할 수 있으며, 테스트를 쉽게 만들 수 있습니다.
Vue 인스턴스를 초기화할 때 수행하고 데이터를 주요 구성요소에 props
로 제공해야 합니다.
return new Vue({
el: '.js-vue-app',
name: 'MyComponentRoot',
render(createElement) {
return createElement('my-component', {
props: {
avatarUrl: gl.avatarUrl,
},
});
},
});
능력에 액세스하기
능력을 프론트엔드에 추가한 후에는 Vue 애플리케이션에서 자손 컴포넌트에서 능력을 사용할 수 있도록 Vue의 provide
및 inject
메커니즘을 사용합니다. commons/vue.js
에서 glAbilties
객체가 이미 제공되므로 플래그를 사용하려면 믹스인만 필요합니다:
// 임의의 자손 컴포넌트
import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin';
export default {
// ...
mixins: [glAbilitiesMixin()],
// ...
created() {
if (this.glAbilities.someAbility) {
// ...
}
},
}
기능 플래그에 액세스하기
기능 플래그를 프론트엔드에 추가한 후에는 Vue 애플리케이션에서 자손 컴포넌트에서 기능 플래그를 사용할 수 있도록 Vue의 provide
및 inject
메커니즘을 사용합니다. commons/vue.js
에서 glFeatures
객체가 이미 제공되므로 플래그를 사용하려면 믹스인만 필요합니다:
// 임의의 자손 컴포넌트
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
// ...
mixins: [glFeatureFlagsMixin()],
// ...
created() {
if (this.glFeatures.myFlag) {
// ...
}
},
}
이 접근 방식에는 몇 가지 이점이 있습니다:
- 중간 컴포넌트가 인식하지 않고 임의로 깊게 중첩된 컴포넌트가 플래그를 선택하고 액세스할 수 있습니다 (예: 플래그를 props를 통해 하향식으로 전달하지 않음).
-
vue-test-utils
에서mount
/shallowMount
로 플래그를 제공할 수 있기 때문에 좋은 테스트 가능성을 가집니다.import { shallowMount } from '@vue/test-utils'; shallowMount(component, { provide: { glFeatures: { myFlag: true }, }, });
- `진입점에 액세스하는 경우를 제외하고 전역 변수에 액세스할 필요가 없습니다.
페이지로 리디렉션 및 알림 표시하기
다른 페이지로 리디렉션하고 알림을 표시해야 하는 경우 visitUrlWithAlerts
유틸을 사용할 수 있습니다.
이는 새로 생성된 리소스로 리디렉션하고 성공 알림을 표시할 때 유용할 수 있습니다.
기본적으로 페이지를 다시로드할 때 알림이 지워집니다. 페이지에 알림을 유지하려면 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 사이트에서 컴포넌트에 대해 더 읽을 수 있습니다. 컴포넌트 시스템.
Pinia
Vuex
Vuex는 사용 중지되었습니다, 마이그레이션을 고려하십시오.
Vue Router
페이지에 Vue Router를 추가하려면 다음 단계를 수행하십시오:
-
와일드카드인
*vueroute
를 사용하여 레일 라우트 파일에 catch-all route를 추가하십시오.# ee/config/routes/project.rb 예시 resources :iteration_cadences, path: 'cadences(/*vueroute)', action: :index
위 예시는
경로
의 시작 부분과 일치하는 모든 경로에 대해iteration_cadences
컨트롤러의index
페이지를 제공합니다. 예를 들어groupname/projectname/-/cadences/123/456/
. -
초기화할 Vue Router의
base
매개변수로 사용하기 위해 프론트엔드에 기본 경로를 전달하십시오..js-my-app{ data: { base_path: project_iteration_cadences_path(project) } }
-
라우터를 초기화하십시오.
Vue.use(VueRouter); export function createRouter(basePath) { return new VueRouter({ routes: createRoutes(), mode: 'history', base: basePath, }); }
-
path: '*'
를 사용하여 인식되지 않은 경로에 대한 대체를 추가하십시오. 아래 중 하나를 수행하십시오:-
라우트 배열의 끝에 리디렉션을 추가하십시오.
const routes = [ { path: '/', name: 'list-page', component: ListPage, }, { path: '*', redirect: '/', }, ];
-
라우트 배열의 끝에 대체 컴포넌트를 추가하십시오.
const routes = [ { path: '/', name: 'list-page', component: ListPage, }, { path: '*', component: NotFound, }, ];
-
-
선택 사항. 자식 경로에 대한 경로 도우미를 사용하도록 허용하려면
controller
및action
매개변수를 추가하십시오.resources :iteration_cadences, path: 'cadences(/*vueroute)', action: :index do resources :iterations, only: [:index, :new, :edit, :show], constraints: { id: /\d+/ }, controller: :iteration_cadences, action: :index
이는 예를 들어 그룹 또는 프로젝트 멤버십을 확인할 수 있도록 백엔드에서 경로를 확인할 수 있으며,
_path
도우미를 사용할 수 있음을 의미합니다. 즉,*vueroute
경로 부분을 수동으로 빌드하지 않고도 특징 사양에서 페이지를로드할 수 있음을 의미합니다.
Vue 및 jQuery 혼합
- Vue와 jQuery를 혼합하는 것은 권장되지 않습니다.
- Vue에서 특정 jQuery 플러그인을 사용하려면 그 주변에 래퍼를 만드세요.
- Vue가 기존의 jQuery 이벤트를 jQuery 이벤트 리스너를 사용하여 수신하는 것은 허용됩니다.
- Vue가 jQuery와 상호 작용하기 위해 새로운 jQuery 이벤트를 추가하는 것은 권장되지 않습니다.
Vue 및 JavaScript 클래스 혼합(데이터 기능 내)
Vue 문서에 따르면, 데이터 기능/객체는 다음과 같이 정의됩니다:
Vue 인스턴스의 데이터 객체. Vue는 속성을 getter/setter로 재귀적으로 변환하여 “반응적”으로 만듭니다. 객체는 단순해야 합니다: 브라우저 API 객체나 prototype 속성 같은 네이티브 객체는 무시됩니다. 데이터는 데이터에 불과하며, 자체적인 상태적 행동을 갖는 객체를 관찰하는 것은 권장되지 않습니다.
Vue의 지침을 기반으로:
- JavaScript 클래스를 사용하거나 생성하지 않아야 합니다. 데이터 기능에서.
- 새로운 JavaScript 클래스 구현을 추가해서는 안 됩니다.
- 복잡한 상태 관리를 응집된 분리된 컴포넌트나 상태 관리자로 캡슐화해야 합니다.
- 기존의 구현은 이러한 방법을 사용하여 유지해야 합니다.
- 변경 사항이 큰 경우 컴포넌트를 순수 객체 모델로 이관해야 합니다.
- 비즈니스 로직을 도와주거나 유틸리티에 추가하여 컴포넌트와는 별도로 테스트할 수 있도록 해야 합니다.
왜
거대한 코드베이스에서 자바스크립트 클래스를 사용하면 유지 관리에 문제가 발생하는 추가적인 이유:
- 클래스가 생성된 후에는 Vue의 반응성 및 최선의 방법을 침해할 수 있는 방식으로 확장될 수 있습니다.
- 클래스는 컴포넌트 API 및 내부 작업을 덜 명확하게 만드는 추상화 계층을 추가합니다.
- 테스트하기 어려워집니다. 클래스가 컴포넌트 데이터 기능에서 인스턴스화되기 때문에 컴포넌트와 클래스를 별도로 ‘관리’하기가 더 어렵습니다.
- 함수형 코드베이스에 객체 지향 원칙(OOP)을 추가하면 코드를 작성하는 또 다른 방법이 더해져 일관성과 명확성이 감소합니다.
스타일 가이드
Vue 컴포넌트 및 템플릿을 작성하고 테스트하는 가장 좋은 방법은 스타일 가이드의 Vue 섹션을 참조하세요.
컴포지션 API
Vue 2.7부터, Vue 구성 요소 및 독립형 사용자 구성 요소에 컴포지션 API를 사용할 수 있습니다.
<script setup>
보다는 <script>
를 선호하세요
컴포지션 API를 사용하면 로직을 구성 요소의 <script>
섹션에 배치하거나 전용 <script setup>
섹션을 갖도록할 수 있습니다. 우리는 <script>
을 사용하고 setup()
속성을 사용하여 컴포넌트에 컴포지션 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"
사용으로 인해 다음과 같은 문제가 발생합니다:
- 컴포넌트의 계약 상실.
props
는 특히 이 문제를 해결하기 위해 설계되었습니다. - 트리 안의 각 컴포넌트에 대한 높은 유지 보수 비용.
v-bind="$attrs"
은 데이터 흐름을 이해하려면 컴포넌트 계층 구조 전체를 검사해야 해서 특히 디버깅하기 어렵습니다. - Vue 3으로의 마이그레이션 중 문제. Vue 3에서는
$attrs
에 이벤트 리스너가 포함되어 있어 예기치 않은 부작용을 일으킬 수 있습니다.
컴포넌트 당 하나의 API 스타일을 목표로 합니다
Vue 컴포넌트에 setup()
속성을 추가할 때, 가독성과 유지보수성을 위해 그것을 전적으로 컴포지션 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에서 composable의 일반적인 명명 규칙은 use
로 접두어를 붙인 다음 composable 기능을 간단하게 참조하는 것입니다 (useBreakpoints
, useGeolocation
등). composable을 포함하는 .js
파일에도 하나 이상의 composable이 포함되어 있더라도 use_
로 시작해야 합니다.
라이프사이클 함정 피하기
composable을 작성할 때 가능한 한 간단하게 유지하는 것이 좋습니다. 라이프사이클 후크는 composable에 복잡성을 추가하고 예기치 못한 부작용을 일으킬 수 있습니다. 이를 피하기 위해 다음 원칙을 따르는 것이 좋습니다:
- 가능한 경우 라이프사이클 후크 사용을 최소화하고 대신 콜백을 허용/반환하는 것이 좋습니다.
- composable이 라이프사이클 후크가 필요한 경우, 해당 composable 안에서 동일한 composable 내에서
onUnmounted
에 있는 리스너를 추가하는지 확인하세요. - 항상 라이프사이클 후크를 즉시 설정하세요.
회피 방법
어떤 경우에는 Vue가 제공하는 탈출 구멍 중 일부를 사용하여 모든 것을 블랙 박스로 처리하는 조립체를 작성하려는 유혹이 있을 수 있습니다. 그러나 대부분의 경우 이렇게 하면 너무 복잡하고 유지 보수가 어려워집니다. 탈출구 중 하나는 getCurrentInstance
메소드입니다. 이 메소드는 현재 렌더링 중인 컴포넌트의 인스턴스를 반환합니다. 이 메소드 대신에 데이터나 메소드를 조립체로 인수를 통해 전달하는 것이 좋습니다.
const useSomeLogic = () => {
doSomeLogic();
getCurrentInstance().emit('done'); // bad
};
const done = () => emit('done');
const useSomeLogic = (done) => {
doSomeLogic();
done(); // good, composable doesn't try to be too smart
}
조립체 테스트
Vue 구성 요소 테스트
Vue 컴포넌트에 대한 가이드 라인과 최상의 실천 방법에 대해서는 Vue testing style guide를 참조하십시오.
각 Vue 컴포넌트는 고유한 출력을 가지고 있습니다. 이 출력물은 항상 렌더 함수에 존재합니다.
Vue 컴포넌트의 각 메소드를 개별적으로 테스트할 수 있지만, 우리의 목표는 언제나 상태를 나타내는 렌더 함수의 출력을 테스트하는 것입니다.
도움이 필요하면 Vue testing guide를 방문하십시오.
이 Vue 컴포넌트에 대한 잘 구조화된 단위 테스트 예시는 다음과 같습니다.
import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
import App from '~/todos/app.vue';
const TEST_TODOS = [{ text: 'Lorem ipsum test text' }, { text: 'Lorem ipsum 2' }];
const TEST_NEW_TODO = 'New todo title';
const TEST_TODO_PATH = '/todos';
describe('~/todos/app.vue', () => {
let wrapper;
let mock;
beforeEach(() => {
// 중요: axios API 요청을 스텁하기 위해 axios-mock-adapter를 사용합니다.
mock = new MockAdapter(axios);
mock.onGet(TEST_TODO_PATH).reply(200, TEST_TODOS);
mock.onPost(TEST_TODO_PATH).reply(200);
});
afterEach(() => {
// 중요: axios mock adapter를 정리합니다.
mock.restore();
});
// 컴포넌트 및 협력자(Vuex 및 axios와 같은)를 설정하는 것을 분리하는 것이 매우 도움이 됩니다.
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(App, {
propsData: {
path: TEST_TODO_PATH,
...props,
},
});
};
// 도우미 메소드는 테스트 유지 관리 및 가독성을 매우 도와줍니다.
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findAddButton = () => wrapper.findByTestId('add-button');
const findTextInput = () => wrapper.findByTestId('text-input');
const findTodoData = () =>
wrapper
.findAllByTestId('todo-item')
.wrappers.map((item) => ({ text: item.text() }));
describe('마운트되고 로딩 중인 경우', () => {
beforeEach(() => {
// 해결되지 않는 요청을 만듭니다.
mock.onGet(TEST_TODO_PATH).reply(() => new Promise(() => {}));
createWrapper();
});
it('로딩 상태를 렌더링해야 합니다.', () => {
expect(findLoader().exists()).toBe(true);
});
});
describe('할 일이 로드된 경우', () => {
beforeEach(() => {
createWrapper();
return wrapper.vm.$nextTick();
});
it('로딩되지 않아야 합니다.', () => {
expect(findLoader().exists()).toBe(false);
});
it('할 일을 렌더링해야 합니다.', () => {
expect(findTodoData()).toEqual(TEST_TODOS);
});
it('할 일이 추가되면 새 할 일을 게시해야 합니다.', async () => {
findTextInput().vm.$emit('update', TEST_NEW_TODO);
findAddButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(mock.history.post.map((x) => JSON.parse(x.data))).toEqual([{ text: TEST_NEW_TODO }]);
});
});
});
자식 컴포넌트
- 자식 컴포넌트들이 렌더링되는 방식을 정의하는 지시문들(예:
v-if
와v-for
)을 테스트합니다. - 자식 컴포넌트에 전달하는 프롭스(특히 테스트 중인 컴포넌트에서 계산된 프롭스인 경우
computed
속성을 사용하는 경우)를 테스트합니다..props()
를 사용하고.vm.someProp
을 사용하지 마십시오. -
자식 컴포넌트에서 발생하는 이벤트에 대해 정확하게 반응하는지를 테스트합니다.
const checkbox = wrapper.findByTestId('checkboxTestId'); expect(checkbox.attributes('disabled')).not.toBeDefined(); findChildComponent().vm.$emit('primary'); await nextTick(); expect(checkbox.attributes('disabled')).toBeDefined();
-
자식 컴포넌트의 내부 구현을 테스트하면 안됩니다.
// bad expect(findChildComponent().find('.error-alert').exists()).toBe(false); // good 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('아이템 클릭 이벤트를 발생시켜야 합니다.', () => {
DropdownItem.vm.$emit('itemClicked');
...
})
이벤트가 발생했는지 확인하기 위해 emitted()
메소드의 결과를 단언해야 합니다.
자식 컴포넌트에서 이벤트를 발생시킬 때는 vm.$emit
사용을 선호해야 합니다.
컴포넌트에서 trigger
를 사용하면 이 컴포넌트를 외부에서 정확히 동작한다고 가정합니다. 또한 Vue3 모드에서 일부 테스트가 실패할 수 있습니다.
const findButton = () => wrapper.findComponent(GlButton);
// bad
findButton().trigger('click');
// good
findButton().vm.$emit('click');
Vue.js 전문가 역할
Vue.js 전문가로 지원하려면 개인적인 병합 요청과 리뷰에서 다음을 보여주어야 합니다:
- Vue 반응성에 대한 깊은 이해
- Vue 및 Pinia 코드가 공식 가이드 및 저희 가이드에 따라 구조화됨
- Vue 구성 요소 및 Pinia 스토어를 테스트하는 전체 이해
- 기존 Vue 및 Pinia 애플리케이션 및 기존 재사용 가능한 구성 요소에 대한 지식
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>