현재 진행 중인 Vue.js + Typescript 기반의 프로젝트에서 컴포넌트 구성에 필요한 부분을 정리한 것으로 Typescript로 구성하면 자바스크립트로 어떻게 구성되는지를 비교하면서 진행하도록 한다.
Typescript를 사용하면서 ‘?’ 는 생략 가능 또는 null / undefined도 가능하다라는 코드는 많이 봤을테지만 vue-property-decorator 샘플들을 보면 ‘readonly, !:’ 와 같은 생소한 코드를 보게 된다.
대상 멤버를 읽기 전용으로 한정하겠다는 한정자 (OOP 언어에서는 많이 사용)로 Vue에서 Prop이나 Model 등에 readonly를 한정했을 떄 할당을 하면 오류가 발생하게 된다. 따라서 데코레이터들을 사용할 때는 @Prop이나 @Model 등에 readonly 한정자를 선언하는 것이 좋다.
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” 이거나 “기본 값”을 설정하는 경우에 지정하는 것이 좋다.
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 가 인식할 수 있는 형태로 변환하는 것을 의미한다.
1...
2<script lang="ts">
3import { Component, Vue } from 'vue-property-decorator';
4
5@Component
6export default class SampleComponent extends Vue {}
7</script>
8...
1...
2<script>
3export default {
4 name: 'SampleComponent'
5};
6</script>
7...
컴포넌트의 내부 구성을 처리하기 전에 컴포넌트 자체의 옵션들을 설정할 수 있으며 이를 통해서 각 종 Vue 객체들과 연동 정보를 구성할 수 있다. 많이 사용되는 것들은 아래와 같다.
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...
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...
컴포넌트 내의 지정한 멤버들을 속성(props)으로 사용할 수 있도록 구성한다.
@Prop(options: (PropOptions | Constructor[] | Constructor) = {})
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...
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(path: string, opitons: WatchOptions = {})
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...
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는 대상 변경에 따른 이벤트 처리기와 같은 기능을 수행하기 때문에 동일한 대상을 여러 번 지정할 경우는 가장 마지막에 지정한 것만 유효하다.
Vue.js에서는 props를 정의할 때 .sync 를 지정해서 자식 컴포넌트에서 부모 컴포넌트의 값을 변경할 수 있도록 처리할 수 있으며 @update:
@PropSync(propName: string, options: (PropOptions | Constructor[] | Constructor) = {})
1<template>
2 <!-- 아래의 두 가지 Child 컴포넌트 설정은 동일한 의미를 가진다. 단지 명시적인 표현 여부만 다르다. -->
3 <ChildComponent :childValue.sync="value" />
4 <ChildComponent :childValue="value" @update:childValue="value = $event" />
5</template>
@PropSync는 자식 컴포넌트에서 부모 컴포넌트의 .sync 속성을 전달할때 사용하는 데코레이터로 아래와 같이 사용해서 값을 할당하는 것만으로 처리가 가능하다.
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를 사용하지 않는다면 아래와 같이 이벤트 호출을 처리해 줘야 한다.
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...
Vue.js에서는 컴포넌트간의 데이터 연동이 가능하다.
자식에서 부모로 값을 전달하는 event 처리에 사용하는 것이 @Emit다.
@Emit(event?: string)
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>
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의 옵션을 설정해서 구분할 수도 있지만 생략할 경우는 메서드의 이름을 그대로 이벤트 이름으로 사용한다.
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는 $refs에서 참조할 수 있는 요소 또는 컴포넌트를 정의하는 것으로 사전에 정의함으로서 오타나 수정에 대응하기 쉽도록 하는 역할을 담당한다.
@Ref(refKey?: string)
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>
Vue.js의 Model을 정의하는 것으로 여러 가지 옵션을 정의할 수 있다. @Model이 정의되면 해당 변수가 @Prop 선언을 동반하게 된다.
@Model(event?: string, options: (PropOptions | Constructor[] | Constructor) = {})
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...
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(key?: string | Symbole) @Inject(options?: { from?: InjectKey, default?: any } | InjectKey)
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...
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...
”@Provide / @Inject” 의 확장 기능으로 부모 컴포넌트에서 @ProvideReactive로 제공된 대상이 변경되면 자식 컴포넌트에서 인식할 수 있다.
@ProvideReactive(key?: string | symbol) @InjectReactive(options?: { from?: InjectKey, default?: any } | InjectKey )
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...