[Vue.js 3.x]컴포넌트 심화

컴포넌트 심화

컴포넌트 in 컴포넌트

부모와 자식

컴포넌트에서 다른 컴포넌트 사용하는 방법

  • 사용할 컴포넌트를 import한다
  • 현재 컴포넌트의 템플릿에서 사용할 컴포넌트를 components에 등록한다
  • 지역등록 형태
    예시
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import ComponentA from './ComponentA'
    import ComponentC from './ComponentC'

    export default {
    components: {
    ComponentA,
    ComponentC
    }
    }

예제)

PageTitle.vue
1
2
3
4
5
6
7
8
<template>
<h2>Page Title</h2>
</template>
<script>
export default {
name: "PageTitle"
}
</script>
NestedComponent.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<page-title/>
</div>
</template>

<script>
import PageTitle from "../components/PageTitle"; //컴포넌트 import
export default {
name: "NestedComponent",
components: {PageTitle} // 현재 컴포넌트에서 사용할 컴포넌트 등록
}
</script>

summary

  • 개발자 각각 자기가 맡은 화면을 구성하면 개발자마 다른 화면으로 통일성이 없어짐
  • 위의 경우 PageTitle 컴포넌트를 개발자들이 import해서 사용한다면 이후 해당 변경 필요시
    해당 컴포넌트만 수정하면 됨
  • 컴포넌트는 단위도 되며 페이지 하나의 전체가 될 수도 있음 - 설계가 중요

부모 → 자식 : Props

예제수정)

PageTitle.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<h2>{{title}}</h2>
</template>

<script>
export default {
name: "PageTitle",
props: {
title: {
type: String, //저장할 데이터 의 타입을 한정 가능
default: "페이지 제목 기본값" //부모 데이터가 없을때의 기본값 정의
}
}
}
</script>
NestedComponent.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<page-title title="부모가 주는 데이터"/> <!-- 정적 값 전달 -->
</div>
</template>

<script>
import PageTitle from "../components/PageTitle"; //컴포넌트 import
export default {
name: "NestedComponent",
components: {PageTitle} // 현재 컴포넌트에서 사용할 컴포넌트 등록
}
</script>

부모 → 자식 데이터 전달

  • 속성 정의
  • 정의된 속성과 동일한 이름의 속성명을 자식 컴포넌트의 props에 정의

동적 props 전달

v-bind를 사용한 동적 props전달
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<page-title :title="title" />
</div>
</template>

<script>
import PageTitle from "../components/PageTitle"; //컴포넌트 import
export default {
name: "NestedComponent",
components: {PageTitle},
data() {
return {
title: '동적 페이지 타이틀'
}
}
}
</script>

숫자형 전달

  • 숫자값을 props로 전달하기 위해선 무조건 v-bind를 사용해야 한다
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- v-bind 사용하지 않으면 숫자가 아닌 문자 -->
    <blog-post likes="42"/>

    <!-- "42"는 정적이지만 v-bind를 사용함으로써 전달되는 데이터가 자바스크립트 표현식이 된다 -->
    <blog-post :likes="42"/>

    <!-- 변수 값 동적 할당 -->
    <blog-post likes="post.likes"/>

논리형(Boolean) 전달

  • 숫자형과 마찬가지로 v-bind를 사용하지 않으면 문자열로 전달된다
    1
    2
    3
    4
    5
    <!-- "true"는 정적이지만 v-bind를 사용함으로써 전달되는 데이터가 자바스크립트 표현식이 된다 -->
    <blog-post :is-published="true"/>

    <!-- 변수 값 동적 할당 -->
    <blog-post :is-published="isShow"/>

그밖에

  • 배열(Array) 역시 마찬가지로 v-bind를 사용하지 않으면 문자열로 전달 된다
  • 객체(Object) 역시 마찬가지로 v-bind를 사용하지 않으면 문자열로 전달 된다
  • 객체의 속성(attribute) 역시 마찬가지로 v-bind를 사용하지 않으면 문자열로 전달 된다

유효성 검사

props 정의시 다음과 같은 옵션을 정할 수 있다

  • 전달 받는 데이터 타입(type)
  • 기본 값 (default)
  • 필수값 여부 (required)
  • 유효성 검사 함수 (validator)
    1
    2
    3
    4
    5
    props: {
    validator: function(value) {
    return ~~~; // 유효성을 만족하면 true를 반환
    }
    }

부모 컴포넌트에서 자식 컴포넌트의 이벤트 직접 발생 시키기

ChildComponent.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<button type="button" v-on:click="childFunc" ref="btn">click</button>
</template>
<script>
export default {
name: "ChildComponent",
methods: {
childFunc() {
console.log('부모컴포넌트에서 직접 발생시킨 이벤트');
}
}
}
</script>
ParentComponent.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<child-component v-on:send-message="sendMessage" ref="child_component"/>
</template>

<script>
import ChildComponent from "./ChildComponent";

export default {
name: "ParentComponent",
components: {ChildComponent},
mounted() {
this.$refs.child_component.$refs.btn.click();
}
}
</script>
  • 부모에서 자식 컴포넌트에 ref를 지정하여 $refs로 접근 가능

부모 컴포넌트에서 자식 컴포넌트의 함수 직접 호출하기

ChildComponent2.vue
1
2
3
4
5
6
7
8
9
10
<script>
export default {
name: "ChildComponent2",
methods: {
callFromParent() {
console.log('부모 컴포넌트에서 직접 호출한 함수');
}
}
}
</script>
ParentComponent2.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<child-component2 v-on:send-message="sendMessage" ref="child_component"/>
</template>

<script>
import ChildComponent2 from "./ChildComponent2";
export default {
name: "ParentComponent2",
components: {ChildComponent2},
mounted() {
this.$refs.child_component.callFromParent()
}
}
</script>
  • 부모에서 자식 컴포넌트를 $refs를 사용해서 접근하면 자식 컴포넌트의 모든 함수 호출 가능

부모 컴포넌트에서 자식 컴포넌트의 데이터 옵션 값 직접 변경

ChildComponent3.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h1>{{msg}}</h1>
</template>
<script>
export default {
name: "ChildComponent3",
data() {
return {
msg: '원래 msg data'
};
}
}
</script>
ParentComponent3.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<child-component3 v-on:send-message="sendMessage" ref="child_component"/>
<button type="button" v-on:click="changeChildData">Change Child Data</button>
</template>
<script>
import ChildComponent3 from "./ChildComponent3";
export default {
name: "ParentComponent3",
components: {ChildComponent3},
methods: {
changeChildData() {
this.$refs.child_component.msg = '부모가 컴포넌트가 변경한 데이터';
}
}
}
</script>

자식 → 부모 컴포넌트로 이벤트 전달

  • data는 부모에서 자식으로만 가능
  • 자식에서 부모로 전달하려면 이벤트를 이용해서 이벤트의 파라미터로 데이터 전달이 가능
  • $emit을 사용한다
ChildComponent4.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
export default {
name: "ChildComponent4",
data() {
return {
msg: '자식 컴포넌트 data의 msg'
};
},
mounted() {
// 자식 컴포넌트가 mount가 되면 $emit을 통해
// 부모 컴포넌트의 send-message 이벤트를 호출한다
// 이때 msg 데이터가 파라미터로 전송가능
this.$emit('send-message', this.msg);
}
}
</script>
ParentComponent4.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 자식 컴포넌트를 import하고 커스텀이벤트 send-message를 정의
// 자식의 $emit으로 호출됨
<template>
<ChildComponent4 v-on:send-message="sendMessage"/>
</template>

<script>
import ChildComponent4 from "./ChildComponent4";
export default {
name: "ParentComponent4",
components: {ChildComponent4},
methods: {
sendMessage(value) {
console.log(value);
}
}
}
</script>

부모에서 자식 컴포넌트의 데이터 옵션값 sync

  • 부모에서 computed를 이용하면 자식 컴포넌트에 정의된 데이터 옵션 값의 변경사항을 항상 동기화 가능
  • $emit을 통해 변경된 데이터를 전송하지 않아도 변경된 데이터값을 항상 확인 가능하므로 유용할 것 같음
ChildComponent5.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<button type="button" @click="childFunc" ref="child_btn">자식 컴포넌트 데이터 변경</button>
</template>

<script>
export default {
name: "ChildComponent5",
data() {
return {
msg: '원래 메시지'
};
},
methods: {
childFunc() {
this.msg = '변경된 메시지';
console.log('데이터 변경');
}
}
}
</script>
ParentComponent5.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<button type="button" v-on:click="checkChild">자식 컴포넌트 데이터 조회</button>
<child-component5 ref="child_component"/>
</template>

<script>
import ChildComponent5 from "./ChildComponent5";
export default {
name: "ParentComponent5",
components: {ChildComponent5},
computed: {
msg(){
return this.$refs.child_component.msg;
}
},
methods: {
checkChild() {
alert(this.msg)
}
}
}
</script>
  • computed에 $refs를 참조한 자식 컴포넌트의 msg가 오리지널 데이터
  • computed는 참조하고 있는 데이터 변경사항을 바로 감지해서 반영

Slot

상황

  • 지금까지의 컴포넌트는 재활용 가능하며, 여러개를 자식으로 import해서 사용 가능했음
  • 프로젝트시 맞딱트리는 곤란한 상황
    • 다들 굉장히 비슷한 UI와 기능을 가지고 있으면서 아주 일부만 전부 각각 다른 경우
    • 예) 모달 팝업
      • 모달 팝업은 제목이 있는 header , 내용이 있는 바디, 하단에 버튼이 있는 footer로 이루어짐
      • 좋은 어플리케이션은 단순한 팝업이라도 모든 팝업창의 디자인을 동일하게 유지 → 동일한 UX를 줄 수 있도록
      • 여러 개발자가 각각 개발하면 일관성이 없어짐

SLOT

  • 슬롯 : 컴포넌트 내에서 다른 컴포넌트를 사용시 컴포넌트의 마크업을 재정의/확장 가능
  • 기본 틀을에 해당하는 컴포넌트를 Slot을 이용해 만들고 개발자들에게 제공
  • 개발자들은 자신의 화면을 만들면서 해당 컴포넌트의 컨텐츠만 작성하면 됨
modal-layout 컴포넌트
1
2
3
4
5
6
7
8
9
10
11
<div class="modal-container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
  • ‘Named Slots’ : name을 지정해서 사용하는 Slot
  • 기존 부모→자식에게 Props로 데이터 전달하는 예제도 Slot을 처리하면 훨씬 간단히 처리 가능
    • 단순값을 위해 props를 정의할 필요가 없음
    • 부모에서 자식 컴포넌트로 props데이터를 전달할 필요도 없음
    • 코드가 훨씬 간결/직관적
    • v-slot:대신에 단축어로 # 사용 가능

실무팁

  • 프로젝트 개발 초기에 개발팀은 어플리케이션 전체에서 사용될 slot 기반 컴포넌트를 구현해서 개발자들에게 제공해야 한다
  • 팝업, 페이지 타이틀등 다수 컴포넌트에서 공통으로 사용해야 하는 공통 UI 요소를 slot 기반의 컴포넌트로 만들어서 제공 필요
  • 그러면 전체 개발 생산성 향상 및 통일된 디자인을 통한 UX향상 가능
  • 프로젝트 초기에 이루어져야 한다
  • 한번 개발된 slot 기반 컴포넌트는 다른 어플리케이션에서도 사용 가능 → 개발팀의 자산으로 지속적 관리 필요

8.3 Provide / Inject

  • 부모 → 자식 : props를 이용해 데이터 전달
  • 컴포넌트 계층 구조 복잡하면?
    • 예) 부모에서 자식, 그리고 그 자식의 자식 컴포넌트로 데이터 전달
    • 이 경우 props를 통한 데이터 전달로 구현하면 굉장히 복잡한 코드 양산
  • Provide/Inject : 계층 구조 상관없이 부모는 Provide, 자식은 inject 옵션을 통해 데이터를 쉽게 전달 가능

다음과 같은 컴포넌트 구조에서 ParentComponent 에서 ChildChildComponent2로 데이터를 전달한다고 했을 때

1
2
3
4
5
6
Root
└─ ParentComponent
├─ ChildComponent1
└─ ChildComponent2
├─ ChildChildComponent1
└─ ChildChildComponent2

props 사용 한다면? : ParentComponent → ChildComponent2 → ChildChildComponent2 이렇게 3단계를 거쳐서 전달해야한다

하지만 provide/inject를 사용한다면?

ProvideInject.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<ProvideInjectChild/>
</template>

<script>
import ProvideInjectChild from "./ProvideInjectChild";
export default {
name: "ProvideInject",
components: {ProvideInjectChild},
data() {
return {
items: ['A', 'B']
};
},
provide() {
return {
itemLength: this.items.length
};
}
}
</script>

<style scoped>

</style>
ProvideInjectChild.vue
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div></div>
</template>
<script>
export default {
name: "ProvideInjectChild",
inject: ['itemLength'],
mounted() {
console.log(this.itemLength);
}
}
</script>

재정리

  • 부모에서 자식 컴포넌트로 전달하고자 하는 데이터를 provide에 정의
  • 자식에서 부모로부터 전달받고자 하는 데이터와 동일한 속성 이름으로 inject에 문자열 배열로 정의

장단점

  • 장점 : 아무리 컴포넌트 계층구조가 복잡해도 원하는 자식에게 데이터를 한 번에 전달 가능
  • 단점 : 자식은 전달받은 데이터가 어떤 부모로부터 전달되었는지 확인이 불가능하다.

Template refs

  • HTML객체에 바로 접근해서 코드를 구현하는 경우는 Vue개발시 거의 없음
  • 만약 어쩔수 없이 자바스크립트에서 HTML객체에 바로 접근해야한다면?
  • HTML태그에 id 대신 ref를 사용한다
  • this.$refs를 이용해 ref속성에 지정된 이름으로 HTML 객체에 접근 가능

Related POST

공유하기