vue-property-decorator 첫 걸음 (vue js nuxt 대응)

이 글은 Vue를 공부하면서 TypeScript 클래스 기반 Vue 앱을 만들고 싶다는 마음으로 작성했습니다. 여러 예시를 통해 vue-property-decorator 기능을 기본, 적용, 고급 섹션 3개로 나누어 설명합니다.


기본에선 Vue로 앱을 만드는데 필수인 기능, 응용에선 편리한 데코레이터, 고급에선 일반적으로 사용하지 않는 기능을 설명합니다.

일단 기본만 이해하면 어려운 것은 없을 겁니다.


또한, 마지막으로 nuxt-property-decorator 독자적인 데코레이터를 소개합니다.

테스트 버전



기본

구성 요소(기능) 정의

@Component는 정의된 클래스를 Vue가 인식할 수 있는 형식으로 변환합니다.


아래 2개는 같은 의미입니다.


<script>

export default {

  name: 'SampleComponent'

};

</script>


<script lang="ts">

import { Component, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {}

</script>


이때 vue-property-decorator의 Vue 클래스 상속을 잊지 않도록 조심하세요.


Data

Data는 클래스 멤버로 정의하여 사용할 수 있습니다.


다음 예제에선 이름과 나이를 Data가 갖고 있습니다.


<script>

export default {

  data() {

    return {

      name: 'simochee',

      age: 21

    }

  }

};

</script>


<script lang="ts">

import { Component, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  name = 'simochee';

  age = 21;

}

</script>


Data를 템플릿에서 사용할 때는 플레인 Vue 처럼 참조할 수 있습니다.


<template>

  <!-- simochee (21) -->

  <p>{{name}} ({{age}})</p>

</template>


Computed

계산 속성(Computed)은 클래스 Getter로 정의하여 사용할 수 있습니다.


다음 예제는 Data에 정의된 점수를 3배로 계산하는 속성을 정의합니다.


<script>

export default {

  data() {

    return {

      score: 55

    }

  },

  computed: {

    triple() {

      return this.score * 3;

    }

  }

};

</script>


<script lang="ts">

import { Component, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  score = 55;


  get triple() {

    return this.score * 3;

  }

}

</script>


Computed를 템플릿에서 사용할 때는 플레인 Vue 처럼 참조할 수 있습니다.


<template>

  <!-- Triple score: 163! -->

  <p>Triple score: {{triple}}!</p>

</template>


메소드

메소드는 클래스의 메소드로 정의하면 사용할 수 있습니다.


다음 예제에서는 버튼을 누를 때 onClickButton 메서드를 호출합니다.


<template>

  <button @click="onClickButton">Click Me!</button>

</template>


이런 템플릿이 있을 때 onClickButton는 다음과 같이 정의할 수 있습니다.


<script>

export deafult {

  methods: {

    onClickButton() {

      // 버튼 눌렸을 때 처리

    }

  }

};

</script>


<script lang="ts">

import { Component, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  onClickButton() {

    // 버튼 눌렸을 때 처리

  }

}

</script>


React 처럼 메소드에서 this 바인딩할 필요가 없습니다.


라이프 사이클 훅

라이프 사이클 훅은 클래스 수명 주기(라이프 사이클)의 이름으로 메소드를 정의하면 사용할 수 있습니다.


<script>

export default {

  mounted() {

    // 컴포넌트가 마운트 되었을 때 처리

  },

  beforeDestroy() {

    // 컴포넌트가 삭제되기 직전 처리

  }

}

</script>


<script lang="ts">

import { Component, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  mounted() {

    // 컴포넌트 마운트되었을 때 처리

  }


  beforeDestroy() {

    // 컴포넌트 삭제되기 직전 처리

  }

}

</script>


vue-property-decorator는 라이프 사이클 훅과 메소드가 같은 공간에서 정의되므로 라이프 사이클의 이름으로 메소드를 정의하지 않도록 주의하세요.

@Component

@Component 인수로 Vue 객체를 지정할 수 있습니다.


이후 각종 데코레이터를 소개합니다만, 거기서 정의할 수 없습니다. components, filters, mixins 같은 속성은 @Component 인수로 지정합니다.


<script>

export deafult {

  components: {

    AppButton,

    ProductList

  },

  directives: {

    resize

  },

  filters: {

    dateFormat

  },

  mixins: [

    PageMixin

  ]

};

</script>


<script lang="ts">

import { Component, Vue } from 'vue-property-decorator';


@Component({

  components: {

    AppButton,

    ProductList

  },

  directives: {

    resize

  },

  filters: {

    dateFormat

  },

  mixins: [

    PageMixin

  ]

})

export default class SampleComponent extends Vue {

}

</script>


그 밖에도 아래와 같은 속성을 지정할 수 있습니다.


  • 옵션 / 데이터
  • 옵션 / DOM
  • 옵션 / 라이프 사이클 후크
  • 옵션 / asset
  • 옵션 / 구성
  • 옵션 / 기타


@Prop

@Prop은 정의한 멤버들을 props로 사용할 수 있도록 지원합니다.


부모 컴포넌트에서 정의한 멤버 이름을 props로 지정합니다.


<script>

export deafult {

  props: {

    userName: {

      type: String,

      required: true

    },

    isVisible: {

      type: Boolean,

      default: false

    }

  }

};

</script>


<script lang="ts">

import { Component, Prop, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  @Prop({ type: String, required: true })

  userName: string;


  @Prop({ type: Boolean, defualt: false })

  isVisible: boolean;

}

</script>


@Watch

@Watch는 첫번째 인수로 모니터링(감시)할 값의 경로, 두번째 인수는 왓쳐의 옵션으로 지정합니다.


다음 예제는 하나의 Data와 Object 속성 값을 모니터링(감시)하는 방법입니다.


또한, immediate: true는 컴포넌트 초기화 시에도 실행할지를 지정하는 옵션입니다.


<script>

export deafult {

  data() {

    isLoading: false,

    profile: {

      name: 'simochee',

      age: 21

    }

  },

  watch: {

    isLoading() {

      // 로딩 상태가 바뀌었을 때의 처리 

    },

    'profile.age': {

      handler: function() {

        // 프로필의 나이가 변경되었을 때의 처리

      },

      immediate: true

    } 

  }

};

</script>


<script lang="ts">

import { Component, Watch, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  isLoading = false;

  profile = {

    name: 'simochee',

    age: 21

  };


  @Watch('isLoading')

  onChangeLoadingStatus() {

    // 로딩 상태가 바뀌었을 때의 처리

  }


  @Watch('profile.age', { immediate: true })

  onChangeProfileAge() {

    // 프로필의 나이가 변경되었을 때의 처리

  }

}

</script>


Vue 사양에서도 알 수 있듯이 @Watch는 동일한 경로를 여러번 지정할 수 없습니다.

여러번 지정할 경우 앞선 정의는 사라지기 때문입니다. (뒤에 정의한 내용만 존재함)



응용

@SyncProp

Vue.js는 props를 지정할 때 .sync 수식자를 부여하여 자식 컴포넌트에서 부모 컴포넌트의 값을 변경할 수 있습니다.


@update:<Prop 이름>라는 이벤트를 받으면 Data에 대입하는 처리를 암시적으로 실시합니다.


// 부모 컴포넌트

<template>

  <!-- 아래 2개는 같은 의미 -->

  <ChildComponent

   :childValue.sync="value"

  />

  <ChildComponent

    :childValue="value"

    @update:childValue="value = $event"

  />

</template>


이때, 자식 컴퍼넌트 이후 .sync 프로퍼티를 릴레이(전달)할 때 편리한 것이 @PropSync 데코레이터입니다.


이 데코레이터를 사용하지 않는다면 아래와 같이 코딩해야 합니다.


// 자식 컴포넌트

<script lang="ts">

import { Component, Prop, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  @Prop({ type: String })

  childValue: string;


  // value를 변경하고 싶을때 호출

  updateValue(newValue) {

    this.$emit('update:childValue', newValue);

  }

}

</script>


이때, @PropSync로 정의하면 멤버에 값을 할당하는 것만으로 동일한 처리가 가능합니다.


<script lang="ts">

import { Component, PropSync, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  @PropSync({ type: String })

  childValue: string;


  // value를 변경하고 싶을 때 호출

  updateValue(newValue) {

    this.childValue = newValue;

  }

}

</script>


대입하면 값의 변경을 알릴 수 있고, .sync에서 손자 컴포넌트로 값을 전달하는 등 매우 간편하게 활용할 수 있습니다.


<template>

  <SunComponent

    :sunValue.sync="childValue"

  />

</template>

@Emit

Vue에서는 컴포넌트끼리 값을 양방향으로 주고받을 수 있습니다.


부모에서 자식으로 값 전달할 땐 Prop을 지정하고, 자식에서 부모로 값 전달할 땐 이벤트를 호출해 액션, 값을 전달합니다.


이 때 자식에서 부모로 값을 전달할 때 실행하는 이벤트가 $emit 입니다.


다음 예제에서는 자식 컴포넌트와 부모 컴포넌트가 데이터를 주고 받습니다. submit 이벤트로 부모 컴포넌트가 받았음을 통지합니다.


// 자식 컴포넌트

<template>

  <form @submit="onSubmit">

    <input v-model="value">

    <button type="submit">Submit</button>

  </submit>

</template>


<script lang="ts">

import { Component, Vue } from 'vue-property-decorator';


@Component

export default class ChildComponent extends Vue {

  value = '';


  // 값 보내기 처리

  onSubmit() {

    this.$emit('submit', this.value);

  }

}

</script>


// 부모 컴포넌트

<template>

  <ChildComponent

    @submit="onReceiveSubmit"

  />

</template>


<script lang="ts">

import { Component, Vue } from 'vue-property-decorator';

import ChildComponent from './ChildComponent.vue';


@Component({

  components: {

    ChildComponent

  }

})

export default class ParentComponent extends Vue {

  async onReceiveSubmit(newValue: string) {

    // $emit에서 2번째 인자를 받을 수 있도록 처리

    await this.$request.post(newValue);

  }

}

</script>


@Emit에서는 $emit 처리를 미리 정의할 수 있습니다.


이벤트 이름은 @Emit 첫번째 인수에 명시적으로 지정했거나 생략한 경우 앞에서 정의한 메소드 이름을 사용합니다.


또한, 메소드에서 값을 리턴할 때 $emit에서 그 값을 보내게 됩니다.


위 예제의 자식 컴퍼넌트를 @Emit으로 다시 작성하면 다음과 같습니다.


<template>

  <form @submit="submit">

    <input v-model="value">

    <button type="submit">Submit</button>

  </submit>

</template>


<script lang="ts">

import { Component, Emit, Vue } from 'vue-property-decorator';


@Component

export default class ChildComponent extends Vue {

  value = '';


  // 값 보내기 처리

  // 이벤트 이름을 지정하지 않는 경우에도 ()는 생략할 수 없음

  @Emit()

  submit() {

    return this.value;

  }

}

</script>


이밖에도 @Emit 비동기 메소드를 설정할 수 있습니다.


또한 카멜 케이스로 이벤트 이름, 메소드 이름을 지정한 경우 부모 컴포넌트에서 받을 때 케밥 케이스로 변환되므로 주의가 필요합니다.


// 자식 컴포넌트

@Emit()

submitForm() {}


// 부모 컴포넌트

<ChildComponent

  @submit-form="onSubmit"

  @submitForm"onSubmit" // 활성화

/>


@Ref

@Ref는 $refs에서 참조할 수 있는 요소, 컴포넌트 형식을 정의합니다.

사전에 정의해 둠으로써 오타 및 수정에 대응하기 쉬워집니다.


<template>

  <ChildComponent ref="childComponent" />

  <button ref="submitButton">Submit</button>

</template>


<script lang="ts">

import { Component, Vue } from 'vue-property-decorator';

import ChildComponent from '@/component/ChildComponent.vue';


@Component({

  components: {

    ChildComponent

  }

});

export default class SampleComponent extends Vue {

  @Ref() childComponent: ChildComponent;

  @Ref() submitButton: HTMLButtonElement;


  mounted() {

    // 자식 요소의 메소드 실행

    this.childComponent.updateValue();


    // 버튼에 포커스

    this.submitButton.focus();

  }

}

</script>



상급

이후는 상급자를 위한 것이라 설명이 자세하지 않습니다. 필요하다면 공식 문서를 참조하세요.


@Model

Vue의 Model을 정의합니다. Vue에서는 Model을 지정할 때 Prop을 정의하고 거기에 여러 정보 등을 기재하지 않았지만, 데코레이터는 @Model에 함께 정의할 수 있습니다.


다음의 2개는 같은 의미입니다.


<script>

export deafult {

  props: {

    value: {

      type: String,

      required: true

    }

  },

  model: {

    prop: 'value',

    event: 'update'

  }

};

</script>


<script lang="ts">

import { Component, Model, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  @Model('update', { type: String, required: true })

  value: string;

}

</script>


이 때 암묵적으로 value가 Prop으로 정의되므로 value라는 data나 버튼 methods를 정의할 수 없습니다.


@Provide / @Inject

Vue에서는 부모 provide로 정의된 값을 하위 요소(부모와 자식이 아니어도 좋다)에서 inject로 참조할 수 있습니다.


다음의 2개는 같은 의미입니다.


<!-- Parent.vue -->

<script>

export deafult {

  provide: {

    foo: 'foo',

    bar: 'bar'

  }

};

</script>


<!-- Child.vue -->

<script>

export deafult {

  inject: {

    foo: 'foo',

    bar: 'bar',

    optional: { from: 'optional', default: 'default' }

  }

};

</script>


<!-- Parent.vue -->

<script lang="ts">

import { Component, Provide, Vue } from 'vue-property-decorator';


@Component

export default class ParentComponent extends Vue {

  @Provide() foo = 'foo';

  @Provide('bar') baz = 'bar';

}

</script>


<!-- Child.vue -->

<script lang="ts">

import { Component, Inject, Vue } from 'vue-property-decorator';


@Component

export default class ChildComponent extends Vue {

  @Inject() foo: string;

  @Inject('bar') bar: string;

  @Inject({ from: 'optional', default: 'default' }) optional: string;

  @Inject(symbol) baz: string;

}

</script>

@ProvideReactive / @ProvideInject

@Provide/ @Inject 확장입니다. 부모 컴포넌트에서 @ProvideReactive로 제공된 값이 변경되면 자식 컴포넌트에서 알 수 있습니다.


<!-- Parent.vue -->

<script lang="ts">

import { Component, ProvideReactive, Vue } from 'vue-property-decorator';


@Component

export default class ParentComponent extends Vue {

  @ProvideReactive() foo = 'foo';

}

</script>


<!-- Child.vue -->

<script lang="ts">

import { Component, InjectReactive, Vue } from 'vue-property-decorator';


@Component

export default class ChildComponent extends Vue {

  @InjectReactive() foo: string;

}

</script>


readonly와 !, ? 에 대해서

vue-property-decorator 샘플 코드에 readonly, 버튼 prop!: String과 같은 !가 등장합니다.


모두 TypeScript 기능입니다.


readonly 한정자는 멤버 변수를 쓰기 전용으로 선언하는 것입니다. (사용하기 위한 용도)

Vue에선 Prop, Model에 직접 할당하면 오류입니다.


잘못된 할당을 사전에 방지하기 위해 @Prop@Model에서 정의한 멤버 변수는 readonly 한정자로 선언하는 것을 추천합니다.


<script lang="ts">

import { Component, Prop, PropSync, Watch, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  @Prop({ type: String }) readonly name: string;

  @Model('update', { type: Object }) readonly profile: IProfile;

  @PropSync({ type: String }) value: string; // 대입 가능

}

</script>


또한, 데코레이터에 정의된 모든 멤버 변수에 붙어있는 ! 가 NonNullAssertion 오퍼레이터라는 기능입니다.

! 가 붙은 속성이 Null / Undefined가 아님을 명시합니다.


그러나 !는 required: true 또는 기본값이 설정된 속성만 지정하는 것을 추천합니다.

반대로, 필수 항목이 아니며 기본값도 지정되지 않은 경우 ?를 추천합니다.


? 속성이 임의 항목이며, undefined 가능성이 있음을 명시합니다.


<script lang="ts">

import { Component, Prop, Vue } from 'vue-property-decorator';


@Component

export default class SampleComponent extends Vue {

  @Prop({ type: String, required: true })

  readonly name!: string;


  @Prop({ type: Array, default: () => [] })

  readonly items!: string[];


  @Prop({ type: Object });

  readonly profile?: IProfile;


  mounted() {

    // undefined 가능성이 있는 객체 속성을

    // 참조할 경우 오류가 발생한다

    profile.age;

  }

}

</script>


보다 안전하게 구현하고 싶다면, 조심스럽게 개발하세요.


nuxt-property-decorator

nuxt-property-decorator는 nuxt-community가 유지보수하는 vue-property-decorator 래퍼입니다.

vue-property-decorator 기능 뿐만 아니라 Nuxt 독자적인 라이프 사이클 메소드나 속성의 지원 등이 추가되었습니다.


또한, nuxt-property-decorator에는 vue-property-decorator에 없는 독자적인 데코레이터가 구현되어 있습니다.


@On/ @Off/@Once

Vue에선 this.$emit 이벤트를 this.$on이 감지할 수 있습니다.


다음 소스는 mountedfoo 이벤트를 대기시키는 $on/ $once를 장착하고, onClick 메소드에서 foo 이벤트를 활성화합니다.

onClick 메소드가 실행되면 반드시 callback 메소드도 실행되고 처음 한 번만 onceCallback 메소드도 실행됩니다.


또한, clearCallback 메소드를 호출하면 foo 이벤트에 설정된 콜백이 $off에 의해 해제되고 이후에는 활성화되지 않습니다.


<script>

export default {

  name: 'SampleComponent',

  created() {

    // foo 이벤트 대기

    this.$on('foo', this.callback);

    // foo 이벤트 한 번만 대기

    this.$once('foo', this.onceCallback);

  },

  methods: {

    callback(name) { ... },

    onceCallback(name) { ... },

    onClick(user) {

      this.$emit('foo', user.name);

    },

    clearCallback() {

      this.$off('foo', this.callback);

    }

  },

};

</script>


이것을 nuxt-property-decorator@On/ @Off/ @Once 데코레이터에서 다시 작성하면 다음과 같습니다.


<script lang="ts">

import { Component, Emit, On, Off, Once, Vue } from 'nuxt-property-decorator';


@Component

export default class SampleComponent extends Vue {

  @On('foo')

  callback(name: string): void { ... }


  @Once('foo')

  onceCallback(name: string): void { ... }


  @Off('foo', 'callback')

  clearCallback(): void { }


  @Emit('foo')

  onClick(user: IUser): string {

    return user.name;

  }

}

</script>


Vue API와 다른 점은 @Off에서 지정하는 것이 콜백 함수 자체가 아니라 그 이름이라는 점입니다.

위의 예에서는 this.callback 메소드 이벤트 리스너를 해제하고 싶어서 'callback'을 지정했습니다.



관련 글

타입스크립트: vue-awesome-swiper import 에러

모듈 vue-native-websocket에 대한 선언 파일을 찾을 수 없습니다

TypeScript 강좌 5. Node.js 환경 체험하기




댓글(2)

Designed by JB FACTORY