vue-property-decorator 정리

현재 진행 중인 Vue.js + Typescript 기반의 프로젝트에서 컴포넌트 구성에 필요한 부분을 정리한 것으로 Typescript로 구성하면 자바스크립트로 어떻게 구성되는지를 비교하면서 진행하도록 한다.

특이한 코드 설정 (readonly, !)

Typescript를 사용하면서 ‘?’ 는 생략 가능 또는 null / undefined도 가능하다라는 코드는 많이 봤을테지만 vue-property-decorator 샘플들을 보면 ‘readonly, !:’ 와 같은 생소한 코드를 보게 된다.

readonly

대상 멤버를 읽기 전용으로 한정하겠다는 한정자 (OOP 언어에서는 많이 사용)로 Vue에서 Prop이나 Model 등에 readonly를 한정했을 떄 할당을 하면 오류가 발생하게 된다. 따라서 데코레이터들을 사용할 때는 @Prop이나 @Model 등에 readonly 한정자를 선언하는 것이 좋다.

Typescript - reaonly 한정자 선언
 1...
 2<script lang="ts">
 3import { Component, Model, Prop, PropSync, Vue } from 'vue-property-decorator';
 4
 5@Component
 6export default class SampleComponent extends Vue {
 7  @Prop(String) readonly name: string;
 8  @Model('update', { type: Object }) readonly profile: IProfile;
 9  @PropSync(String) value: string;    // 할당 가능
10}
11</script>
12...

!

대상 멤버에 대한 NonNullAssersion 오퍼레이터로 “!“가 붙은 경우는 null / Undefined를 설정할 수 없음을 나타내는 것이다. 그러나 너무 남용하면 특정 형식에 대한 값을 확정하고 처리하는 것이 때문에 확장성에 제한을 받는 상황이 될 수 있으므로 가능하면 “required: true” 이거나 “기본 값”을 설정하는 경우에 지정하는 것이 좋다.

Typescript - reaonly 한정자 선언
 1...
 2<script lang="ts">
 3import { Component, Prop, Vue } from 'vue-property-decorator';
 4
 5@Component
 6export default class SampleComponent extends Vue {
 7  @Prop({ type: String, required: true }) readonly name!: string;
 8  @Prop({ type: Array, default: () => [] }) readonly items!: string[];
 9  @Prop(Object) readonly profile?: IProfile;    // null / undefined 가능
10}
11</script>
12...

@Component (vue-class-component)

@Component 는 정의한 클래스를 Vue 가 인식할 수 있는 형태로 변환하는 것을 의미한다.

Typescript - @Component 선언
1...
2<script lang="ts">
3import { Component, Vue } from 'vue-property-decorator';
4
5@Component
6export default class SampleComponent extends Vue {}
7</script>
8...
Javascript 변환 - Vue Component
1...
2<script>
3export default {
4  name: 'SampleComponent'
5};
6</script>
7...

컴포넌트의 내부 구성을 처리하기 전에 컴포넌트 자체의 옵션들을 설정할 수 있으며 이를 통해서 각 종 Vue 객체들과 연동 정보를 구성할 수 있다. 많이 사용되는 것들은 아래와 같다.

  • Child Components
  • Directives
  • Filters
  • Mixins
  • Data
  • DOM
  • Life-cycle Hooks
  • Asset
  • Configuration
Typescript - @Component 옵션 설정
 1...
 2<script lang="ts">
 3import { Component, Vue } from 'vue-property-decorator';
 4
 5@Component({
 6  components: {
 7    AppButton,
 8    ProductList
 9  },
10  directives: {
11    resize
12  },
13  filters: {
14    dateFormat
15  },
16  mixins: [
17    PageMixin
18  ]
19})
20export default class SampleComponent extends Vue {}
21</script>
22...
Javascript 변환 - Vue Component
 1...
 2<script>
 3export default {
 4  name: 'SampleComponent',
 5  components: {
 6    AppButton,
 7    ProductList
 8  },
 9  directives: {
10    resize
11  },
12  filters: {
13    dateFormat
14  },
15  mixins: [
16    PageMixin
17  ]
18};
19</script>
20...

@Prop

컴포넌트 내의 지정한 멤버들을 속성(props)으로 사용할 수 있도록 구성한다.

@Prop(options: (PropOptions | Constructor[] | Constructor) = {})

Typescript - @Prop 선언
 1...
 2<script lang="ts">
 3import { Component, Prop, Vue } from 'vue-property-decorator';
 4
 5@Component
 6export default class SampleComponent extends Vue {
 7  @Prop(Number) readonly propA: number | undefined
 8  @Prop({ default: 'default value' }) readonly propB!: string
 9  @Prop([String, Boolean]) readonly propC: string | boolean | undefined
10}
11</script>
12...
Javascript 변환 - Vue Component
 1...
 2<script>
 3export default {
 4  name: 'SampleComponent',
 5  props: {
 6    propA: {
 7      type: Number
 8    },
 9    propB: {
10      default: 'default value'
11    },
12    propC: {
13      type: [String, Boolean]
14    }
15  }
16};
17</script>
18...

@Watch

지정한 대상을 모니터링해서 변경되었을 떄 처리를 수행한다.

  • 첫번째 인수 : 모니터링 대상 값
  • 두번쨰 인수 : 모니터링 옵션

@Watch(path: string, opitons: WatchOptions = {})

Typescript - @Watch 선언
 1...
 2<script lang="ts">
 3import { Component, Watch, Vue } from 'vue-property-decorator';
 4
 5@Component
 6export default class SampleComponent extends Vue {
 7  @Watch('child')
 8  onChildChanged(val: string, oldVal: string) {}
 9
10  @Watch('person', { immediate: true, deep: true })
11  onPersonChanged1(val: Person, oldVal: Person) {}
12
13  @Watch('person')
14  onPersonChanged2(val: Person, oldVal: Person) {}
15}
16</script>
17...
Javascript 변환 - Vue Component
 1...
 2<script>
 3export default {
 4  name: 'SampleComponent',
 5  watch: {
 6    child: [
 7      {
 8        handler: 'onChildChanged',
 9        immediate: false,
10        deep: false
11      }
12    ],
13    person: [
14      {
15        handler: 'onPersonChanged1',
16        immediate: true,
17        deep: true
18      },
19      {
20        handler: 'onPersonChanged2',
21        immediate: false,
22        deep: false
23      }
24    ]
25  },
26  methods: {
27    onChildChanged(val, oldVal) {},
28    onPersonChanged1(val, oldVal) {},
29    onPersonChanged2(val, oldVal) {}
30  }
31};
32</script>
33...

위의 코드에서 Watch 옵션으로 지정한 immediate는 컴포넌트 초기화시에도 실행할지 여부를 지정한 것이다.

@Watch는 대상 변경에 따른 이벤트 처리기와 같은 기능을 수행하기 때문에 동일한 대상을 여러 번 지정할 경우는 가장 마지막에 지정한 것만 유효하다.

@PropSync

Vue.js에서는 props를 정의할 때 .sync 를 지정해서 자식 컴포넌트에서 부모 컴포넌트의 값을 변경할 수 있도록 처리할 수 있으며 @update: 이벤트를 수신하면 Data에 적용하는 처리가 암시적으로 진행된다.

@PropSync(propName: string, options: (PropOptions | Constructor[] | Constructor) = {})

Template - 부모 컴포넌트
1<template>
2  <!-- 아래의  가지 Child 컴포넌트 설정은 동일한 의미를 가진다. 단지 명시적인 표현 여부만 다르다. -->
3  <ChildComponent :childValue.sync="value" />
4  <ChildComponent :childValue="value" @update:childValue="value = $event" />
5</template>

@PropSync는 자식 컴포넌트에서 부모 컴포넌트의 .sync 속성을 전달할때 사용하는 데코레이터로 아래와 같이 사용해서 값을 할당하는 것만으로 처리가 가능하다.

Typescript - 자식 컴포넌트에서 @PropSync 선언
 1...
 2<script lang="ts">
 3import { Component, PropSync, Vue } from 'vue-property-decorator';
 4
 5@Component
 6export default class SampleComponent extends Vue {
 7  @PropSync({ type: String }) childValue: string;
 8  ...
 9  // 값 변경 적용
10  updateValue(newVal: string) {
11    this.childValue = newVal;   // 이 시점에서 부모 컴포넌트로 전달된다.
12  }
13}
14</script>
15...

@PropSync를 사용하지 않는다면 아래와 같이 이벤트 호출을 처리해 줘야 한다.

Typescript - 자식 컴포넌트에서 직접 처리하는 경우
 1...
 2<script lang="ts">
 3import { Component, Prop, Vue } from 'vue-property-decorator';
 4
 5@Component
 6export default class SampleComponent extends Vue {
 7  @Prop({ type: String }) childValue: string;
 8  ...
 9  // 값 변경 적용
10  updateValue(newVal: string) {
11    this.$emit('update:childValue', newVal);  // 값 설정 및, 부모 컴포넌트로 이벤트 전달
12  }
13}
14</script>
15...

@Emit

Vue.js에서는 컴포넌트간의 데이터 연동이 가능하다.

  • 부모에서 자식으로 전달은 props 사용
  • 자식에서 부모로 전달은 event 사용

자식에서 부모로 값을 전달하는 event 처리에 사용하는 것이 @Emit다.

@Emit(event?: string)

Typescript - 자식 컴포넌트
 1<template>
 2  <form @submit="onSubmit">
 3    <input v-model="value">
 4    <button type="submit">Submit</button>
 5  </form>
 6</template>
 7
 8<script lang="ts">
 9import { Component, Vue } from 'vue-property-decorator';
10
11@Component
12export default class ChildComponent extends Vue {
13  value = '';
14
15  // 부모로 값 전달
16  onSubmit() {
17    this.$emit('submit', this.value);
18  }
19}
20</script>
Typescript - 부모 컴포넌트
 1<template>
 2  <ChildComponent @submit="onReceiveSubmit" />
 3</template>
 4
 5<script lang="ts">
 6import { Component, Vue } from 'vue-property-decorator';
 7import ChildComponent form '@/components/childcomponent.vue';
 8
 9@Component({
10  components: {
11    ChildComponent
12  }
13})
14export default class ParentComponent extends Vue {
15  async onReceiveSubmit(newVal: string) {
16    // $emit을 통해서 전달된 값 수신
17    await this.$request.post(newVal);
18  }
19}
20</script>

위의 예제에서 ChildComponent는 직접 $emit 처리를 했지만 이를 @Emit 사용하는 버전으로 바꾸면 좀 더 단순하게 코드를 구성할 수 있다. 처리되는 이벤트의 이름은 @emit의 옵션을 설정해서 구분할 수도 있지만 생략할 경우는 메서드의 이름을 그대로 이벤트 이름으로 사용한다.

Typescript - 자식 컴포넌트 @Emit 선언
 1<template>
 2  <form @submit="onSubmit">
 3    <input v-model="value">
 4    <button type="submit">Submit</button>
 5  </form>
 6</template>
 7
 8<script lang="ts">
 9import { Component, Emit, Vue } from 'vue-property-decorator';
10
11@Component
12export default class ChildComponent extends Vue {
13  value = '';
14
15  // 부모로 값 전달, 옵션이 없더라도 '()'를 생략할 수 없다.
16  // return으로 반환된 값이 전달되며, 메서드 이름을 사용할 경우는 'on' 접두사가 붙어서 처리된다.
17  @Emit()
18  submit() {
19    return this.value;
20  }
21}
22</script>

@Ref

@Ref는 $refs에서 참조할 수 있는 요소 또는 컴포넌트를 정의하는 것으로 사전에 정의함으로서 오타나 수정에 대응하기 쉽도록 하는 역할을 담당한다.

@Ref(refKey?: string)

Typescript - @Ref 선언
 1<template>
 2  <ChildComponent ref="childComponent" />
 3  <button ref="submitButton">Submit</button>
 4</template>
 5
 6<script lang="ts">
 7import { Component, Ref, Vue } from 'vue-property-decorator';
 8
 9@Component({
10  components: {
11    ChildComponent
12  }
13})
14export default class SampleComponent extends Vue {
15  @Ref() childComponent: ChildComponent;
16  @Ref() submitButton: HTMLButtomElement;
17
18  mounted() {
19    // 자식 컴포넌트 메서드 실행
20    this.childComponent.updateValue();
21    // 버튼에 포커스 설정
22    this.submitButton.focus()
23  }
24}
25</script>

@Model

Vue.js의 Model을 정의하는 것으로 여러 가지 옵션을 정의할 수 있다. @Model이 정의되면 해당 변수가 @Prop 선언을 동반하게 된다.

@Model(event?: string, options: (PropOptions | Constructor[] | Constructor) = {})

Typescript - @Model 선언
 1...
 2<script lang="ts">
 3import { Component, Model, Vue } from 'vue-property-decorator';
 4
 5@Component
 6export default class SampleComponent extends Vue {
 7  @Model('change', { type: Boolean }) readonly checked!: boolean
 8}
 9</script>
10...
Javascript 변환 - Vue Component
 1...
 2<script>
 3export default {
 4  name: 'SampleComponent',
 5  model: {
 6    prop: 'checked',
 7    event: 'change'
 8  },
 9  props: {
10    checked: {
11      type: Boolean
12    }
13  }
14};
15</script>
16...

@Provide / @Inject

부모 컴포넌트에서 @Provide로 정의된 대상을 자식 컴포넌트에서 @Inject로 참조할 수 있다.

@Provide(key?: string | Symbole)
@Inject(options?: { from?: InjectKey, default?: any } | InjectKey)

Typescript - @Provide / @Inject 선언
 1...
 2<script lang="ts">
 3import { Component, Inject, Provide, Vue } from 'vue-property-decorator';
 4
 5const symbol = Symbol('baz');
 6
 7@Component
 8export default class SampleComponent extends Vue {
 9  @Inject() readonly foo!: string;
10  @Inject('bar') readonly bar!: string
11  @Inject({ from: 'optional', default: 'default' }) readonly optional!: string
12  @Inject(symbol) readonly baz!: string
13
14  @Provide() foo = 'foo'
15  @Provide('bar') baz = 'bar'
16}
17</script>
18...
Javascript 변환 - Vue Component
 1...
 2<script>
 3const symbol = Symbol('baz')
 4
 5export default {
 6  name: 'SampleComponent',
 7  inject: {
 8    foo: 'foo',
 9    bar: 'bar',
10    optional: { from: 'optional', default: 'default' },
11    [symbol]: symbol
12  },
13  data() {
14    return {
15      foo: 'foo',
16      baz: 'bar'
17    }
18  },
19  provide() {
20    return {
21      foo: this.foo,
22      bar: this.baz
23    }
24  }
25};
26</script>
27...

@ProvideReactive / @InjectReactive

”@Provide / @Inject” 의 확장 기능으로 부모 컴포넌트에서 @ProvideReactive로 제공된 대상이 변경되면 자식 컴포넌트에서 인식할 수 있다.

@ProvideReactive(key?: string | symbol)
@InjectReactive(options?: { from?: InjectKey, default?: any } | InjectKey )

Typescript - @ProvideReactive / @InjectReactive 선언
 1...
 2<script lang="ts">
 3import { Component, InjectReactive, ProvideReactive, Vue } from 'vue-property-decorator';
 4
 5const key = Symbol()
 6
 7// 부모 컴포넌트
 8@Component
 9export default class ParentComponent extends Vue {
10  @ProvideReactive() one = 'value'
11  @ProvideReactive(key) two = 'value'
12}
13
14// 자식 컴포넌트
15@Component
16export default class ChildComponent extends Vue {
17  @InjectReactive() one!: string
18  @InjectReactive(key) two!: string
19}
20</script>
21...

참고 자료