Vue.js 스타일 가이드
GitLab v19.4요약
GitLab은 기본적으로 eslint-vue-plugin의 plugin:vue/recommended를 사용합니다. Vue 템플릿에는 .vue를 사용합니다. Vue 앱에 전달되는 데이터를 명시적으로 정의합니다 코드베이스를 명시적이고 찾기 쉬우며 검색 가능한 상태로 유지하기 위해 이 경우에는 전개 연산자 사용을 권장하지 않습니다.
린팅#
GitLab은 기본적으로 eslint-vue-plugin의 plugin:vue/recommended를 사용합니다.
자세한 내용은 규칙 문서를 참고합니다.
기본 규칙#
-
Vue 템플릿에는
.vue를 사용합니다. HAML에서%template을 사용하지 않습니다. -
Vue 앱에 전달되는 데이터를 명시적으로 정의합니다
// bad return new Vue({ el: '#element', name: 'ComponentNameRoot', components: { componentName }, provide: { ...someDataset }, props: { ...anotherDataset }, render: createElement => createElement('component-name'), })); // good const { foobar, barfoo } = someDataset; const { foo, bar } = anotherDataset; return new Vue({ el: '#element', name: 'ComponentNameRoot', components: { componentName }, provide: { foobar, barfoo }, props: { foo, bar }, render: createElement => createElement('component-name'), }));코드베이스를 명시적이고 찾기 쉬우며 검색 가능한 상태로 유지하기 위해 이 경우에는 전개 연산자 사용을 권장하지 않습니다. 이는 Vuex 상태 초기화처럼 위와 같은 이점이 있는 모든 곳에 적용됩니다. 위 패턴을 사용하면 인스턴스를 만들 때 스칼라가 아닌 값도 쉽게 파싱할 수 있습니다.
return new Vue({ el: '#element', name: 'ComponentNameRoot', components: { componentName }, props: { foo, bar: parseBoolean(bar) }, render: createElement => createElement('component-name'), }));
템플릿 내 컴포넌트 사용#
-
템플릿에서 컴포넌트를 사용할 때는 다른 표기법보다 kebab-case 이름을 우선합니다
// bad <MyComponent /> // good <my-component />
컴포넌트 props#
-
범용 CSS 클래스를 전달하는 prop보다 의도를 설명하는 의미 있는 prop을 우선합니다
// bad - hands styling ownership to every caller <user-name :username-css-classes="'gl-truncate'" /> // good - the component owns its styling, callers express intent <user-name :truncate-username="true" />불리언
truncate-username같은 의미 있는 prop은 스타일 소유권을 컴포넌트 안에 유지하므로, 호출하는 쪽이 내부 스타일을 바꿀 수 없고 컴포넌트는 사용되는 모든 곳에서 시각적으로 일관됩니다. 그러면 테스트도 내부 CSS 클래스가 아니라 공개 prop을 기준으로 검증할 수 있습니다.
번역 문자열#
-
번역 호출은
<template>에 직접 작성합니다. 템플릿과 메서드 양쪽에서 재사용하는 경우처럼 분명한 이유가 있을 때만 문자열을$options.i18n으로 옮깁니다.// bad - a single-use string moved away from where it is rendered <gl-button>{{ $options.i18n.buttonLabel }}</gl-button> // good <gl-button>{{ s__('Plan|Button label') }}</gl-button>번역 헬퍼는 템플릿과 컴포넌트 JavaScript 양쪽에서 사용할 수 있고
gettext추출기가<template>블록을 읽으므로, 인라인 문자열도pot파일에 반영됩니다.$options.i18n사용이 정당한 경우의 전체 목록은 Vue 단일 파일 컴포넌트를 참고합니다. 외부화 헬퍼는 번역을 위한 페이지 준비를 참고합니다.
컴포넌트 name 속성#
모든 Vue 컴포넌트에는 name 속성이 있어야 합니다. 파일 이름에서 파생한 PascalCase를 사용합니다.
예를 들어 board_app.vue는 name: 'BoardApp'이 됩니다.
app.vue나 index.vue 같은 범용 파일 이름에는 디렉터리 경로의 컨텍스트를 접두사로 붙여
고유한 이름을 만듭니다. 예를 들어 admin/users/components/app.vue는
name: 'AdminUsersApp'이 됩니다.
EE 컴포넌트가 CE 컴포넌트와 이름이 같으면 EE 컴포넌트 이름에 EE 접미사를
붙입니다. 예를 들어 app/와 ee/app/ 양쪽에 BranchSelector 컴포넌트가 있다면
EE 버전은 name: 'BranchSelectorEE'를 사용합니다.
<style> 태그#
GitLab은 다음 몇 가지 이유로 Vue 컴포넌트에서 <style> 태그를 사용하지 않습니다.
- SCSS 변수와 믹스인, Tailwind CSS의
@apply지시문을 사용할 수 없습니다. - 이 스타일은 런타임에 삽입됩니다.
- CSS를 정의하는 다른 방법이 이미 몇 가지 있습니다.
<style> 태그 대신 Tailwind CSS 유틸리티 클래스나 페이지별 CSS를 사용합니다.
Vue 테스트#
Vue 컴포넌트를 효과적으로 테스트하려는 과정에서 여러 프로그래밍 패턴과 스타일 선호가 자리 잡았습니다. 다음 가이드는 그중 일부를 설명합니다. 엄격한 지침이라기보다는 GitLab이 Vue 테스트를 어떻게 작성하는지 이해하는 데 도움이 되는 제안과 좋은 사례 모음입니다.
컴포넌트 마운트#
일반적으로 Vue 컴포넌트를 테스트할 때는 모든 테스트 블록에서 컴포넌트를 "다시 마운트"해야 합니다.
이를 위해 다음을 수행합니다.
- 최상위
describe블록 안에 변경 가능한wrapper변수를 만듭니다. mount또는shallowMount로 컴포넌트를 마운트합니다.- 그 결과로 나온
Wrapper인스턴스를wrapper변수에 다시 할당합니다.
전역의 변경 가능한 래퍼를 만들면 다음을 포함해 여러 이점이 있습니다.
-
컴포넌트나 DOM 요소를 찾는 공통 함수를 정의할 수 있습니다.
import MyComponent from '~/path/to/my_component.vue'; describe('MyComponent', () => { let wrapper; // this can now be reused across tests const findMyComponent = wrapper.findComponent(MyComponent); // ... }) -
beforeEach블록으로 컴포넌트를 마운트할 수 있습니다(자세한 내용은createComponent팩토리를 참고합니다). -
테스트 실행 후
enableAutoDestroy로 컴포넌트를 자동으로 제거할 수 있습니다. 이 설정은shared_test_setup.js에 있습니다.
비동기 자식 컴포넌트#
shallowMount는 비동기 자식 컴포넌트의 컴포넌트 스텁을 만들지 않습니다. 비동기 자식 컴포넌트를 올바르게 스텁하려면 stubs 옵션을 사용합니다. 비동기 자식 컴포넌트에 name 옵션이 정의되어 있는지 확인합니다. 정의되어 있지 않으면 wrapper의 findComponent 메서드가 올바르게 동작하지 않을 수 있습니다.
createComponent 팩토리#
마운트 로직의 중복을 피하려면 각 테스트 블록에서 재사용할 수 있는 createComponent 팩토리
함수를 정의하는 것이 좋습니다. 이 함수는 mount와
shallowMount의 결과를 wrapper 변수에 다시 할당하는
클로저입니다.
import MyComponent from '~/path/to/my_component.vue';
import { shallowMount } from '@vue/test-utils';
describe('MyComponent', () => {
// Initiate the "global" wrapper variable. This will be used throughout our test:
let wrapper;
// Define our `createComponent` factory:
function createComponent() {
// Mount component and reassign `wrapper`:
wrapper = shallowMount(MyComponent);
}
it('mounts', () => {
createComponent();
expect(wrapper.exists()).toBe(true);
});
it('`isLoading` prop defaults to `false`', () => {
createComponent();
expect(wrapper.props('isLoading')).toBe(false);
});
})
마찬가지로 beforeEach 블록에서 createComponent를 호출해 테스트의 중복을 더 줄일 수 있습니다.
import MyComponent from '~/path/to/my_component.vue';
import { shallowMount } from '@vue/test-utils';
describe('MyComponent', () => {
// Initiate the "global" wrapper variable. This will be used throughout our test
let wrapper;
// define our `createComponent` factory
function createComponent() {
// mount component and reassign `wrapper`
wrapper = shallowMount(MyComponent);
}
beforeEach(() => {
createComponent();
});
it('mounts', () => {
expect(wrapper.exists()).toBe(true);
});
it('`isLoading` prop defaults to `false`', () => {
expect(wrapper.props('isLoading')).toBe(false);
});
})
createComponent 모범 사례#
-
인수를 여러 개 두기보다 객체 인수 하나(또는 제한된 개수)를 사용하는 방안을 검토합니다.
props같은 공통 데이터에 단일 파라미터를 정의하는 것은 괜찮지만, JavaScript 스타일 가이드를 염두에 두고 파라미터 개수 제한을 지킵니다.// bad function createComponent(props, stubs, mountFn, foo) { } // good function createComponent({ props, stubs, mountFn, foo } = {}) { } // good function createComponent(props = {}, { stubs, mountFn, foo } = {}) { } -
같은 테스트 묶음에서
mount와shallowMount를 둘 다 사용해야 한다면, 컴포넌트를 마운트할 때 사용할 마운트 함수(mount또는shallowMount)를 받는mountFn파라미터를createComponent팩토리에 정의하면 유용합니다.import { shallowMount } from '@vue/test-utils'; function createComponent({ mountFn = shallowMount } = {}) { } -
wrapper.findByTestId()를 노출하려면mountExtended와shallowMountExtended헬퍼를 사용합니다.import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { SomeComponent } from 'components/some_component.vue'; let wrapper; const createWrapper = () => { wrapper = shallowMountExtended(SomeComponent); }; const someButton = () => wrapper.findByTestId('someButtonTestId'); -
data,methods등 컴포넌트 내부를 확장하는 마운트 옵션은 사용하지 않습니다.import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { SomeComponent } from 'components/some_component.vue'; let wrapper; // bad :( - This circumvents the actual user interaction and couples the test to component internals. const createWrapper = ({ data }) => { wrapper = shallowMountExtended(SomeComponent, { data }); }; // good :) - Helpers like `clickShowButton` interact with the actual I/O of the component. const createWrapper = () => { wrapper = shallowMountExtended(SomeComponent); }; const clickShowButton = () => { wrapper.findByTestId('show').trigger('click'); }
컴포넌트 상태 설정#
-
가능하면 컴포넌트 상태를 설정할 때
setProps를 사용하지 않습니다. 대신 컴포넌트를 마운트할 때 컴포넌트의propsData를 설정합니다.// bad wrapper = shallowMount(MyComponent); wrapper.setProps({ myProp: 'my cool prop' }); // good wrapper = shallowMount({ propsData: { myProp: 'my cool prop' } });예외는 컴포넌트의 반응성을 어떤 방식으로든 테스트하려는 경우입니다. 예를 들어 특정 watcher가 실행된 뒤 컴포넌트의 출력을 테스트할 수 있습니다. 이런 동작을 테스트할 때
setProps를 사용하는 것은 괜찮습니다. -
컴포넌트의 내부 상태를 설정해 컴포넌트의 실제 I/O 테스트를 우회하는
setData는 사용하지 않습니다. 대신 컴포넌트의 자식에서 이벤트를 발생시키거나 다른 부수 효과로 상태 변경을 유도합니다.
컴포넌트 상태 접근#
-
props나 속성에 접근할 때는
wrapper.props().myProp이나wrapper.vm.myProp보다wrapper.props('myProp')구문을 우선합니다.// good expect(wrapper.props().myProp).toBe(true); expect(wrapper.attributes().myAttr).toBe(true); // better expect(wrapper.props('myProp').toBe(true); expect(wrapper.attributes('myAttr')).toBe(true); -
여러 prop을 검증할 때는
toEqual로props()객체의 깊은 동등성을 확인합니다.// good expect(wrapper.props('propA')).toBe('valueA'); expect(wrapper.props('propB')).toBe('valueB'); expect(wrapper.props('propC')).toBe('valueC'); // better expect(wrapper.props()).toEqual({ propA: 'valueA', propB: 'valueB', propC: 'valueC', }); -
일부 prop에만 관심이 있다면
toMatchObject를 사용할 수 있습니다.expect.objectContaining보다toMatchObject를 우선합니다.// good expect(wrapper.props()).toEqual(expect.objectContaining({ propA: 'valueA', propB: 'valueB', })); // better expect(wrapper.props()).toMatchObject({ propA: 'valueA', propB: 'valueB', });
props 유효성 검사 테스트#
컴포넌트 props를 확인할 때는 assertProps 헬퍼를 사용합니다. props 유효성 검사 실패는 오류로 던져집니다.
import { assertProps } from 'helpers/assert_props'
// ...
expect(() => assertProps(SomeComponent, { invalidPropValue: '1', someOtherProp: 2 })).toThrow()