Vue.js 스타일 가이드

린트

우리는 plugin:vue/recommended와 함께 eslint-vue-plugin를 기본으로 설정합니다. 자세한 설명은 rules에서 확인하세요.

기본 규칙

  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'),
    });
    

    위와 같은 특정 경우에는 스프레드 연산자의 사용을 권장하지 않습니다. 코드베이스를 명확하고 검색 가능하게 유지하기 위해 해당 패턴을 사용하세요. 또한, 위의 예시와 같은 경우에는 스프레드 연산자 대신에 이러한 패턴을 사용하면 비스칼라 값을 쉽게 파싱할 수 있게 됩니다.

    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. 프롭스 네이밍: DOM 컴포넌트 프롭 이름을 사용하지 마세요.
  4. 프롭스 네이밍: 템플릿에서 프롭을 제공할 때 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. 템플릿 내에서는 항상 이중 따옴표 "를 사용하고, 다른 JavaScript 코드에는 작은 따옴표 '를 사용하세요.

    // 안 좋은 예
    template: `
      <button :class='style'>Button</button>
    `
       
    // 좋은 예
    template: `
      <button :class="style">Button</button>
    

프롭스

  1. 프롭스는 객체로 선언되어야 합니다.

    // 안 좋은 예
    props: ['foo']
       
    // 좋은 예
    props: {
      foo: {
        type: String,
        required: false,
        default: 'bar'
      }
    }
    
  2. 프롭 선언 시 항상 required 키를 제공해야 합니다.

    // 안 좋은 예
    props: {
      foo: {
        type: String,
      }
    }
       
    // 좋은 예
    props: {
      foo: {
        type: String,
        required: false,
        default: 'bar'
      }
    }
    
  3. 프롭이 필요하지 않은 경우 default 키를 제공해야 합니다. 프로퍼티의 존재 여부를 확인해야 하는 시나리오도 있습니다. 이러한 경우에는 default 키를 제공해서는 안 됩니다.

    // 좋은 예
    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. Shorthand @v-on보다 선호됩니다.

    // 안 좋은 예
    <component v-on:click="eventHandler"/>
       
    // 좋은 예
    <component @click="eventHandler"/>
    
  2. Shorthand :v-bind보다 선호됩니다.

    // 안 좋은 예
    <component v-bind:class="btn"/>
       
    // 좋은 예
    <component :class="btn"/>
    
  3. Shorthand #v-slot보다 선호됩니다.

    // 안 좋은 예
    <template v-slot:header></template>
       
    // 좋은 예
    <template #header></template>
    

닫는 태그

  1. 가능한 경우 self-closing 컴포넌트 태그를 사용하세요.

    // 안 좋은 예
    <component></component>
       
    // 좋은 예
    <component />
    

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

  1. 템플릿에서 컴포넌트의 kebab-case 이름을 다른 스타일보다 선호하세요.

    // 안 좋은 예
    <MyComponent />
       
    // 좋은 예
    <my-component />
    

순서

  1. .vue 파일의 태그 순서

    <script>
      // ...
    </script>
       
    <template>
      // ...
    </template>
       
    // 우리는 scoped 스타일을 사용하지 않지만 이와 같은 경우가 몇 군데 있습니다
    <style>
      // ...
    </style>
    
  2. Vue 컴포넌트의 속성 순서: 컴포넌트 속성의 순서에 대해 order of properties in components rule를 확인하세요.

:key

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

  1. 배열의 요소에 고유한 id가 있는 경우 해당 id를 사용하는 것이 좋습니다:

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

    <div
      v-for="(item, index) in items"
      :key="index"
    >
      <!-- 내용 -->
    </div>
    
  3. v-fortemplate과 함께 사용하고 여러 자식 요소가 있는 경우, :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"
      >
        <!-- 내용 -->
      </span>
    </div>
    

유용한 링크:

  1. 상태 유지
  2. Vue 스타일 가이드: Keyed v-for

Vue 테스팅

시간이 지남에 따라 Vue 컴포넌트를 효과적으로 테스트하기 위해 몇 가지 프로그래밍 패턴과 스타일 기호가 발전해왔습니다. 다음 가이드에서는 이러한 패턴 중 일부를 설명합니다. 이것들은 엄격한 지침이 아니라 Vue 테스트를 GitLab에서 어떻게 작성하는지에 대한 통찰을 제공하기 위한 제안과 좋은 관행의 모음입니다.

컴포넌트 마운팅

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

이를 달성하기 위해:

  1. 최상위 describe 블록 내부에 변경 가능한 wrapper 변수를 생성합니다.
  2. mount 또는 shallowMount를 사용하여 컴포넌트를 마운트합니다.
  3. 결과로 나온 Wrapper 인스턴스를 wrapper 변수에 재할당합니다.

글로벌하고 변경 가능한 wrapper를 만드는 것은 컴포넌트/DOM 요소를 찾기 위한 공통 함수를 정의하는 등 여러 이점을 제공합니다.

  import MyComponent from '~/path/to/my_component.vue';
  describe('MyComponent', () => {
    let wrapper;
    
    // 이제 이것은 테스트 전체에서 재사용할 수 있습니다
    const findMyComponent = wrapper.findComponent(MyComponent);
    // ...
  })

createComponent 팩터리

마운팅 로직을 중복해서 작성하는 것을 피하기 위해 createComponent 팩터리 함수를 정의하는 것이 유용합니다. 이는 각 테스트 블록에서 재사용할 수 있는 클로저이며, mountshallowMount의 결과를 wrapper 변수에 재할당해야 합니다.

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('마운트됩니다', () => {
    createComponent();
    
    expect(wrapper.exists()).toBe(true);
  });
  
  it('`isLoading` 프롭이 기본적으로 `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('마운트됩니다', () => {
    expect(wrapper.exists()).toBe(true);
  });
  
  it('`isLoading` 프롭이 기본적으로 `false`입니다', () => {
    expect(wrapper.props('isLoading')).toBe(false);
  });
})

createComponent의 최선의 방법

  1. 많은 매개변수 대신 단일(또는 제한된 수의) 객체 인수를 사용하는 것이 좋습니다. props와 같은 공통 데이터에 대한 단일 매개변수 정의는 괜찮지만, JavaScript 스타일 가이드를 참고하고 매개변수 수 제한 내에 유지하세요:

    // bad
    function createComponent(props, stubs, mountFn, foo) { }
       
    // good
    function createComponent({ props, stubs, mountFn, foo } = {}) { }
       
    // good
    function createComponent(props = {}, { stubs, mountFn, foo } = {}) { }
    
  2. 동일한 테스트 세트 내에서 mount 그리고 shallowMount 둘 다 필요한 경우, 컴포넌트를 마운트하는 데 사용할 마운트 함수(mount 또는 shallowMount)를 사용할 수 있는 mountFn 매개변수를 createComponent 팩터리로 정의하는 것이 유용합니다:

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

    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;
  
  // bad :( - 이는 실제 사용자 상호작용을 우회하고 테스트를 컴포넌트 내부에 결합시킵니다.
  const createWrapper = ({ data }) => {
    wrapper = shallowMountExtended(SomeComponent, {
      data
    });
  };
  
  // good :) - `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. 컴포넌트의 내부 상태를 설정하는 setData를 사용하지 마십시오. 대신, 컴포넌트의 자식요소에서 이벤트를 트리거하거나 다른 부수효과를 일으켜 상태 변경을 강제하십시오.

컴포넌트 상태 접근

  1. 속성 또는 어트리뷰트에 접근할 때, 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로 확인하십시오.

     // 우수한 예
     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. 일부 속성에만 관심이 있다면, toMatchObject를 사용하십시오. expect.objectContaining보다 toMatchObject를 선호하십시오.

     // 우수한 예
     expect(wrapper.props()).toEqual(expect.objectContaining({
       propA: 'valueA',
       propB: 'valueB',
     }));
        
     // 더 나은 예
     expect(wrapper.props()).toMatchObject({
       propA: 'valueA',
       propB: 'valueB',
     });
    

속성 유효성 검사

컴포넌트 속성을 확인할 때 assertProps 도우미를 사용하십시오. 속성 유효성 검사 오류는 오류로 throw됩니다.

import { assertProps } from 'helpers/assert_props'

// ...

expect(() => assertProps(SomeComponent, { invalidPropValue: '1', someOtherProp: 2 })).toThrow()

JavaScript/Vue 어코드

이 어코드의 목표는 우리 모두가 동일한 페이지에 있음을 확인하는 것입니다.

  1. Vue를 작성할 때, 애플리케이션에서 jQuery를 사용해서는 안 됩니다.
    1. DOM에서 데이터를 가져와야 하는 경우, 애플리케이션을 부트스트래핑하는 동안 dataset을 사용하여 데이터 속성을 가져올 수 있습니다. jQuery 없이도 가능합니다.
    2. Vue.js에서 jQuery 의존성을 사용할 수 있습니다. 예시는 문서의 이 예시를 따릅니다.
    3. Vue 애플리케이션 내부에서 외부의 jQuery 이벤트를 청취해야 하는 경우 jQuery 이벤트 청취기를 사용할 수 있습니다.
    4. 새로운 jQuery 이벤트를 추가할 필요가 없는 경우, 동일한 작업을 수행하는 다른 방법을 찾아보십시오.
  2. 애플리케이션 별 데이터를 위해 애플리케이션을 부트스트래핑하는 동안 window 객체에 1회 접근할 수 있습니다(예: scrollTo는 언제든지 접근 가능합니다). 애플리케이션을 부트스트래핑하는 동안에만 이러한 접근을 하십시오.
  3. 기술적인 빚을 만들어야 하는 경우, 그 기술적 빚을 나중에 리팩터링하기 위한 코드를 작성하십시오. 유지보수자들이 처음부터 기술적 빚에 동의해야 합니다. 해당 기술적 빚에 대한 문제를 평가하기 위해 이슈를 생성하고 더 자세히 논의해야 합니다. 앞으로 몇 개월 내에 해당 기술적 빚을 해결해야 하며, 우선순위는 유지보수자에 의해 결정되어야 합니다.
  4. 기술적 빚을 만들 때, 해당 코드에 대한 테스트를 미리 작성해야 하며, 해당 테스트를 다시 작성해서는 안 됩니다. 예를 들어, jQuery 테스트를 Vue 테스트로 변경해서는 안 됩니다.
  5. 중앙 집중화된 상태 관리로 VueX를 사용할 수 있습니다. VueX를 사용하지 않기로 선택하는 경우 Vue.js 문서에서 찾을 수 있는 store pattern을 사용해야 합니다.
  6. 중앙 집중화된 상태 관리 솔루션을 선택한 후 전체 애플리케이션에 대해 해당 솔루션을 사용해야 합니다. 상태 관리 솔루션을 혼합하여 사용해서는 안 됩니다.