Vue.js 스타일 가이드

린팅

기본적으로 eslint-vue-pluginplugin:vue/recommended를 사용합니다.

규칙을 확인하여 더 많은 문서를 참고하세요.

기본 규칙

  1. 서비스는 자체 파일을 가져야 합니다.

  2. 스토어는 자체 파일을 가져야 합니다.

  3. 번들 파일에서 함수를 사용하여 Vue 컴포넌트를 인스턴스화합니다:

    // 나쁨
    class {
      init() {
        new Component({})
      }
    }
    
    // 좋음
    document.addEventListener('DOMContentLoaded', () => new Vue({
      el: '#element',
      components: {
        componentName
      },
      render: createElement => createElement('component-name'),
    }));
    
  4. 서비스나 스토어에 대해 싱글톤을 사용하지 마세요.

    // 나쁨
    class Store {
      constructor() {
        if (!this.prototype.singleton) {
          // 무언가를 하다
        }
      }
    }
    
    // 좋음
    class Store {
      constructor() {
        // 무언가를 하다
      }
    }
    
  5. Vue 템플릿에는 .vue를 사용하세요. HAML에서는 %template을 사용하지 마세요.

  6. Vue 앱에 전달되는 데이터를 명시적으로 정의하세요.

    // 나쁨
    return new Vue({
      el: '#element',
      name: 'ComponentNameRoot',
      components: {
        componentName
      },
      provide: {
        ...someDataset
      },
      props: {
        ...anotherDataset
      },
      render: createElement => createElement('component-name'),
    }));
    
    // 좋음
    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'),
    }));
    

네이밍

  1. 확장자: Vue 컴포넌트에는 .vue 확장자를 사용하세요. 파일 확장자로 .js를 사용하지 마세요.

    (#34371).

  2. 참조 네이밍: 기본 수입에 대해 PascalCase를 사용하세요:

    // 나쁨
    import cardBoard from 'cardBoard.vue'
    
    components: {
      cardBoard,
    };
    
    // 좋음
    import CardBoard from 'cardBoard.vue'
    
    components: {
      CardBoard,
    };
    
  3. Props 네이밍: DOM 컴포넌트 prop 이름을 사용하지 마세요.

  4. Props 네이밍: 템플릿에서 props를 제공할 때 camelCase 대신 kebab-case를 사용하세요.

    // 나쁨
    <component class="btn">
    
    // 좋음
    <component css-class="btn">
    
    // 나쁨
    <component myProp="prop" />
    
    // 좋음
    <component my-prop="prop" />
    

정렬

  1. 템플릿 메서드에 대해 다음 정렬 스타일을 따르십시오:

    1. 여러 특성이 있는 경우 모든 특성은 새 줄에 있어야 합니다:

      // 나쁨
      <component v-if="bar"
          param="baz" />
      
      <button class="btn">Click me</button>
      
      // 좋음
      <component
        v-if="bar"
        param="baz"
      />
      
      <button class="btn">
        Click me
      </button>
      
    2. 특성이 하나만 있는 경우 태그는 인라인일 수 있습니다:

      // 좋음
        <component bar="bar" />
      
      // 좋음
        <component
          bar="bar"
          />
      
      // 나쁨
       <component
          bar="bar" />
      

인용부호

  1. 템플릿 내에서는 항상 이중 인용부호 "를 사용하고, 다른 모든 JS에는 단일 인용부호 '를 사용하십시오.

    // 나쁨
    template: `
      <button :class='style'>Button</button>
    `
    
    // 좋음
    template: `
      <button :class="style">Button</button>
    `
    

소품

  1. 소품은 객체로 선언해야 합니다.

    // 나쁨
    props: ['foo']
    
    // 좋음
    props: {
      foo: {
        type: String,
        required: false,
        default: 'bar'
      }
    }
    
  2. 소품을 선언할 때 필수 키는 항상 제공되어야 합니다.

    // 나쁨
    props: {
      foo: {
        type: String,
      }
    }
    
    // 좋음
    props: {
      foo: {
        type: String,
        required: false,
        default: 'bar'
      }
    }
    
  3. 소품이 필수가 아닐 경우 기본 키가 제공되어야 합니다.

    특정 시나리오에서는 속성의 존재 여부를 확인해야 합니다. 이 경우 기본 키는 제공되지 않아야 합니다.

    // 좋음
    props: {
      foo: {
        type: String,
        required: false,
      }
    }
    
    // 좋음
    props: {
      foo: {
        type: String,
        required: false,
        default: 'bar'
      }
    }
    
    // 좋음
    props: {
      foo: {
        type: String,
        required: true
      }
    }
    

데이터

  1. data 메서드는 항상 함수여야 합니다.

    // 나쁨
    data: {
      foo: 'foo'
    }
    
    // 좋음
    data() {
      return {
        foo: 'foo'
      };
    }
    

지시어

  1. 단축키 @v-on보다 선호됩니다.

    // 나쁨
    <component v-on:click="eventHandler"/>
    
    // 좋음
    <component @click="eventHandler"/>
    
  2. 단축키 :v-bind보다 선호됩니다.

    // 나쁨
    <component v-bind:class="btn"/>
    
    // 좋음
    <component :class="btn"/>
    
  3. 단축키 #v-slot보다 선호됩니다.

    // 나쁨
    <template v-slot:header></template>
    
    // 좋음
    <template #header></template>
    

닫는 태그

  1. 자기 닫힘 컴포넌트 태그를 선호하십시오.

    // 나쁨
    <component></component>
    
    // 좋음
    <component />
    

템플릿 내에서의 컴포넌트 사용

  1. 템플릿에서 사용할 때 컴포넌트의 케밥 표기법 이름을 다른 스타일보다 선호하십시오.

    // 나쁨
    <MyComponent />
    
    // 좋음
    <my-component />
    

<style> 태그

Vue 컴포넌트에서 <style> 태그를 사용하지 않는 몇 가지 이유가 있습니다:

  1. SCSS 변수와 믹스인 또는 Tailwind CSS @apply 지시어를 사용할 수 없습니다.

  2. 이러한 스타일은 런타임에 삽입됩니다.

  3. 우리는 이미 CSS를 정의하는 다른 방법이 몇 가지 있습니다.

<style> 태그 대신 Tailwind CSS 유틸리티 클래스 또는 페이지 특정 CSS를 사용해야 합니다.

정렬

  1. .vue 파일의 태그 순서

    <script>
      // ...
    </script>
    
    <template>
      // ...
    </template>
    
    // 우리는 `<style>` 태그를 사용하지 않지만  경우가  가지 있습니다.
    <style>
      // ...
    </style>
    
  2. Vue 컴포넌트의 속성: 컴포넌트의 속성 순서 규칙을 확인하세요.

:key

v-for를 사용할 때 각 항목에 대해 고유한 :key 속성을 제공해야 합니다.

  1. 반복되는 배열의 요소에 고유한 id가 있다면, 이를 사용하는 것이 좋습니다:

    <div
      v-for="item in items"
      :key="item.id"
    >
      <!-- content -->
    </div>
    
  2. 반복되는 요소에 고유한 ID가 없을 경우, 배열 인덱스를 :key 속성으로 사용할 수 있습니다.

    <div
      v-for="(item, index) in items"
      :key="index"
    >
      <!-- content -->
    </div>
    
  3. template와 함께 v-for를 사용하고 자식 요소가 둘 이상인 경우, :key 값들은 고유해야 합니다. kebab-case 네임스페이스를 사용하는 것이 좋습니다.

    <template v-for="(item, index) in items">
      <span :key="`span-${index}`"></span>
      <button :key="`button-${index}`"></button>
    </template>
    
  4. 중첩된 v-for를 다룰 때는 위의 지침을 따르세요.

    <div
      v-for="item in items"
      :key="item.id"
    >
      <span
        v-for="element in array"
        :key="element.id"
      >
        <!-- content -->
      </span>
    </div>
    

유용한 링크:

  1. 상태 유지

  2. Vue 스타일 가이드: Keyed v-for

Vue 테스트

시간이 지나면서 Vue 컴포넌트를 효과적으로 테스트하기 위한 여러 프로그래밍 패턴과 스타일 선호가 나타났습니다. 다음 가이드는 이러한 것들 중 일부를 설명합니다.

이들은 엄격한 지침이 아닙니다, 오히려 GitLab에서 Vue 테스트를 작성하는 방법에 대한 통찰력을 제공하기 위한 제안과 좋은 관행의 모음입니다.

컴포넌트 마운트

일반적으로 Vue 컴포넌트를 테스트할 때, 각 테스트 블록에서 컴포넌트를 “다시 마운트”해야 합니다.

이를 달성하기 위해:

  1. 최상위 describe 블록 내에 가변 wrapper 변수를 만듭니다.

  2. mount 또는 shallowMount를 사용하여 컴포넌트를 마운트합니다.

  3. 결과 Wrapper 인스턴스를 wrapper 변수에 재할당합니다.

전역 가변 래퍼 생성은 다음과 같은 여러 가지 이점을 제공합니다:

  • 컴포넌트/DOM 요소를 찾기 위한 공통 함수를 정의할 수 있습니다:

    import MyComponent from '~/path/to/my_component.vue';
    describe('MyComponent', () => {
      let wrapper;
    
      // 이제 이를 테스트 간에 재사용할 수 있습니다.
      const findMyComponent = wrapper.findComponent(MyComponent);
      // ...
    })
    
  • 컴포넌트를 마운트하기 위한 beforeEach 블록을 사용할 수 있습니다 (자세한 내용은 the createComponent 팩토리를 참조하십시오).

  • enableAutoDestroyshared_test_setup.js 에 설정하여 테스트가 실행된 후 자동으로 컴포넌트를 파괴할 수 있습니다.

비동기 자식 컴포넌트

shallowMount비동기 자식 컴포넌트에 대한 컴포넌트 스텁을 생성하지 않습니다. 비동기 자식 컴포넌트를 올바르게 스텁하기 위해서는 stubs 옵션을 사용하세요. 비동기 자식 컴포넌트에 name 옵션이 정의되어 있는지 확인하세요. 그렇지 않으면 wrapperfindComponent 메소드가 제대로 작동하지 않을 수 있습니다.

createComponent 팩토리

우리의 마운트 로직을 중복하지 않도록, 각 테스트 블록에서 재사용할 수 있는 createComponent 팩토리 함수를 정의하는 것이 유용합니다. 이는 우리의 wrapper 변수를 mountshallowMount의 결과로 재할당해야 하는 클로저입니다:

import MyComponent from '~/path/to/my_component.vue';
import { shallowMount } from '@vue/test-utils';

describe('MyComponent', () => {
  // "전역" wrapper 변수를 초기화합니다. 이는 테스트 전체에서 사용됩니다:
  let wrapper;

  // `createComponent` 팩토리를 정의합니다:
  function createComponent() {
    // 컴포넌트를 마운트하고 `wrapper`를 재할당합니다:
    wrapper = shallowMount(MyComponent);
  }

  it('mounts', () => {
    createComponent();

    expect(wrapper.exists()).toBe(true);
  });

  it('`isLoading` prop 기본값은 `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', () => {
  // "전역" wrapper 변수를 초기화합니다. 이는 테스트 전체에서 사용됩니다:
  let wrapper;

  // `createComponent` 팩토리를 정의합니다:
  function createComponent() {
    // 컴포넌트를 마운트하고 `wrapper`를 재할당합니다:
    wrapper = shallowMount(MyComponent);
  }

  beforeEach(() => {
    createComponent();
  });

  it('mounts', () => {
    expect(wrapper.exists()).toBe(true);
  });

  it('`isLoading` prop 기본값은 `false`입니다', () => {
    expect(wrapper.props('isLoading')).toBe(false);
  });
})

createComponent 모범 사례

  1. 많은 인수보다 하나(또는 제한된 수)의 객체 인수를 사용하는 것을 고려하세요. 일반 데이터에 대해 단일 매개변수를 정의하는 것은 괜찮지만, JavaScript 스타일 가이드를 염두에 두고 매개변수 수 제한 내에 있도록 하세요:

    // 나쁨
    function createComponent(props, stubs, mountFn, foo) { }
    
    // 좋음
    function createComponent({ props, stubs, mountFn, foo } = {}) { }
    
    // 좋음
    function createComponent(props = {}, { stubs, mountFn, foo } = {}) { }
    
  2. 동일한 테스트 세트 내에서 mountshallowMount 두 가지를 모두 필요로 하는 경우, 컴포넌트를 마운트하는 데 사용할 수 있는 마운트 함수를 받아들이는 mountFn 매개변수를 createComponent 팩토리에 정의하는 것이 유용할 수 있습니다:

    import { shallowMount } from '@vue/test-utils';
    
    function createComponent({ mountFn = shallowMount } = {}) { }
    
  3. mountExtendedshallowMountExtended 도우미를 사용하여 wrapper.findByTestId()를 노출하세요:

    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');
    
  4. data, methods, 또는 컴포넌트 내부를 확장하는 다른 마운트 옵션의 사용을 피하세요.

    import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
    import { SomeComponent } from 'components/some_component.vue';
    
    let wrapper;
    
    // 나쁨 :( - 이는 실제 사용자 상호 작용을 우회하고 테스트를 컴포넌트 내부와 결합시킵니다.
    const createWrapper = ({ data }) => {
      wrapper = shallowMountExtended(SomeComponent, {
        data
      });
    };
    
    // 좋음 :) - `clickShowButton`과 같은 도우미는 컴포넌트의 실제 I/O와 상호 작용합니다.
    const createWrapper = () => {
      wrapper = shallowMountExtended(SomeComponent);
    };
    const clickShowButton = () => {
      wrapper.findByTestId('show').trigger('click');
    }
    

컴포넌트 상태 설정

  1. 가능한 한 컴포넌트 상태를 설정하기 위해 setProps를 사용하지 마세요. 대신, 컴포넌트를 마운트할 때 컴포넌트의 propsData를 설정하세요:

    // 나쁨
    wrapper = shallowMount(MyComponent);
    wrapper.setProps({
      myProp: 'my cool prop'
    });
    
    // 좋음
    wrapper = shallowMount({ propsData: { myProp: 'my cool prop' } });
    

    여기서 예외는 어떤 방식으로든 컴포넌트의 반응성을 테스트하고 싶을 때입니다. 예를 들어, 특정 감시자가 실행된 후 컴포넌트의 출력을 테스트하고 싶을 수 있습니다. 이런 행동을 테스트하는 데 setProps를 사용하는 것은 괜찮습니다.

  2. 컴포넌트의 내부 상태를 설정하고 실제 I/O 테스트를 우회하는 setData의 사용을 피하세요. 대신, 상태 변화를 강제하기 위해 컴포넌트의 자식 요소나 다른 부작용에서 이벤트를 트리거하세요.

컴포넌트 상태 접근

  1. props나 속성에 접근할 때, wrapper.props('myProp') 구문을 wrapper.props().myProp 또는 wrapper.vm.myProp보다 선호하세요:

    // 좋음
    expect(wrapper.props().myProp).toBe(true);
    expect(wrapper.attributes().myAttr).toBe(true);
    
    // 더 좋음
    expect(wrapper.props('myProp')).toBe(true);
    expect(wrapper.attributes('myAttr')).toBe(true);
    
  2. 여러 개의 props를 단언할 때는 toEqual를 사용하여 props() 객체의 깊은 동등성을 확인하세요:

    // 좋음
    expect(wrapper.props('propA')).toBe('valueA');
    expect(wrapper.props('propB')).toBe('valueB');
    expect(wrapper.props('propC')).toBe('valueC');
    
    // 더 좋음
    expect(wrapper.props()).toEqual({
      propA: 'valueA',
      propB: 'valueB',
      propC: 'valueC',
    });
    
  3. 특정 props에만 관심이 있다면 toMatchObject를 사용할 수 있습니다. expect.objectContaining보다 toMatchObject를 선호하세요:

    // 좋음
    expect(wrapper.props()).toEqual(expect.objectContaining({
      propA: 'valueA',
      propB: 'valueB',
    }));
    
    // 더 좋음
    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()