[Vue.js 3.x]재사용과 컴포지션

컴포지션 API

Composition API?

기존의 Vue의 문제 : 프로젝트 규모가 커질수록 관리가 힘들다

  • 또한 대규모 어플리케이션일수록 코드 공유와 재사용이 특히 중요
  • mixin을 통해 코드를 재사용하면 오버라이딩 문제, 다중 mixin인 경우 코드 관리가 어려움
  • data, computed, watch, methods등 규모가 커질수록, 컴포넌트의 계층이 복잡해질수록 코드에 대한 추적 및 관리가 어려움

Composition API

상세 가이드 : https://v3.ko.vuejs.org/guide/composition-api-introduction.html

  • vue 3.x에 추가된 함수 기반의 API

Setup

Setup

  • 컴포지션 API를 구현하는 곳

기존 방법과 비교

예시 - 사용자로부터 숫자 2개를 입력받고 더한값을 출력

숫자 이벤트(keyup) 발생할 때마다 메서드실행해서 result로 반환
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
26
27
28
29
30
<template>
<div>
<h2>Calculator</h2>
<div>
<input type="text" v-model="num1" @keyup="plusNumbers"/>
<span> + </span>
<input type="text" v-model="num2" @keyup="plusNumbers"/>
<span> = </span>
<span>{{result}}</span>
</div>
</div>
</template>

<script>
export default {
name: "Calculator",
data() {
return {
num1: 0,
num2: 0,
result: 0
};
},
methods: {
plusNumbers(){
this.result = parseInt(this.num1) + parseInt(this.num2);
}
}
}
</script>
컴포지션API 사용 1단계 - 아직은 큰차이가 없어보임
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
26
27
28
29
30
31
32
33
34
35
36
<template>
<div>
<h2>Calculator - Composition API</h2>
<div>
<input type="text" v-model="state.num1" @keyup="plusNumbers"/>
<span> + </span>
<input type="text" v-model="state.num2" @keyup="plusNumbers"/>
<span> = </span>
<span>{{ state.result }}</span>
</div>
</div>
</template>

<script>
import {reactive} from "vue"; // reactive 추가

export default {
name: "CompositionAPI",
setup() {
let state = reactive({ //reactive를 이용해 num1, num2, result를 실시간 변경사항에 대한 반응형 적용
num1: 0,
num2: 0,
result: 0
});

function plusNumbers() {
state.result = parseInt(state.num1) + parseInt(state.num2);
}

return { //reactive로 선언된 state와 plusNumbers함수 반환 → 기존 data, methods 옵션처럼 사용 가능해짐
state,
plusNumbers
}
}
}
</script>
input에 바인딩된 keyup이벤트를 없애고 코드 간결, 현재 컴포넌트내에서만 사용가능한 형태
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
26
27
28
29
30
31
32
<template>
<div>
<h2>Calculator - Composition API2</h2>
<div>
<input type="text" v-model="state.num1"/>
<span> + </span>
<input type="text" v-model="state.num2"/>
<span> = </span>
<span>{{ state.result }}</span>
</div>
</div>
</template>

<script>
import {computed, reactive} from "vue";

export default {
name: "CompositionAPI2",
setup() {
let state = reactive({
num1: 0,
num2: 0,
// computed를 이용해서 num1, num2가 변경시 result 갱신
result: computed(() => parseInt(state.num1) + parseInt(state.num2))
});

return {
state
}
}
}
</script>

계산기에서 덧셈 연산 여러번 반복해서 사용 가능한 것 처럼 재사용 코드 만든다면?

  • 현재 컴포넌트에서만 사용하는 코드를 여러 컴포넌트로 재사용 가능하도록 함수 분리 필요

setup 코드를 분리해서 별도의 function처리

setup에서 작성된 코드를 별도의 function
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
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<div>
<h2>Calculator - Composition API3</h2>
<div>
<input type="text" v-model="num1"/>
<span> + </span>
<input type="text" v-model="num2"/>
<span> = </span>
<span>{{ result }}</span>
</div>
</div>
</template>

<script>
import {computed, reactive, toRefs} from "vue";

function plusCalculator() {
let state = reactive({
num1: 0,
num2: 0,
result: computed(() => parseInt(state.num1) + parseInt(state.num2))
});
//반응형으로 선언된 num1,num2,result가 외부 func에서 정상동작하기 위해서는
// toRefs를 사용해야 한다
return toRefs(state);
}

export default {
name: "CompositionAPI3",
setup() {
let {num1, num2, result} = plusCalculator(); //외부 func
return {
num1, num2, result
}
}
}
</script>
  • 외부 func에서 반응형 변수를 사용하기 위해서 toRefs추가
    • 컴포넌트 내부에서는 v-model 디렉티브로 바인딩된 변수가 사용자 입력값에 따라 반응형 처리
    • 이 함수를 컴포넌트 밖으로 빼면 사용자가 입력한 값에 대한 반응형 처리 불가능
    • toRefs를 사용하여 컴포넌트 밖에서도 반응형 처리가 가능하게 만듬

컴포넌트에서 정의된 코드를 다른 컴포넌트에서 사용가능하도록 외부 분리

common.js
1
2
3
4
5
6
7
8
9
10
11
12
import {computed, reactive, toRefs} from "vue";
const plusCalculator = () => {
let state = reactive({
num1: 0,
num2: 0,
result: computed(() => parseInt(state.num1) + parseInt(state.num2))
});
return toRefs(state);
};
export {
plusCalculator
}
CompositionAPI4 common.js를 import해서 사용. 가독성 향상
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
26
27
<template>
<div>
<h2>Calculator - Composition API4</h2>
<div>
<input type="text" v-model="num1"/>
<span> + </span>
<input type="text" v-model="num2"/>
<span> = </span>
<span>{{ result }}</span>
</div>
</div>
</template>

<script>

import {plusCalculator} from "../common";

export default {
name: "CompositionAPI4",
setup() {
let {num1, num2, result} = plusCalculator(); //외부 func
return {
num1, num2, result
}
}
}
</script>

특정기능을 갖는 함수를 컴포지션API로 이용하고 개발해서 공통스크립트 처리

  • 뷰 컴포넌트 내에서 반응형으로 처리 가능

라이프사이클 훅

컴포지션 API내에서 사용할 수 있는 컴포넌트 라이프사이클 훅

Options API setup 내부 Hook
beforeCreate 필요하지 않음*
created 필요하지 않음*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
  • 컴포지션 API에서 setup()은 beforeCreate와 created 사이에서 실행된다
  • 따라서 해당 라이프사이클의 훅은 필요하지 않은 것
예제)setup()에서 onMounted 훅을 적용한 코드
1
2
3
4
5
6
7
8
export default {
setup() {
//mounted
onMounted(() =>{
console.log('Component is mounted!')
})
}
}

튜토리얼의 컴포지션 API 기초 예제 소개

1. 앱에서 특정 사용자 레포지터리

예시) : 특정 사용자의 레포지토리목록을 보여주는 검색과 필터 기능을 가진 컴포넌트

src/components/UserRepositories.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
26
27
28
29
30
31
// src/components/UserRepositories.vue

export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
data () {
return {
repositories: [], // 1
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
getUserRepositories () {
// `this.user`를 사용해서 유저 레포지토리 가져오기
}, // 2
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
  • 컴포넌트 to do
    1. 사용자 이름에 대한 외부 API로 레포지토리 가져오기와 사용자가 변경 될 때마다 갱신하기
    2. searchQuery 문자열을 사용하여 레포지토리 검색하기
    3. filters 객체를 사용하여 레포지토리 필터링하기
  • 컴포넌트의 옵션들(data, computed, watch, methods)로 논리를 구성 가능함
  • 그러나 컴포넌트가 커지면 논리적 관심사가 커져서 처음 접하는 사람들은 이해가 어려운 컴포넌트가 됨
  • 논리적 관심사를 그룹화된 색상으로 표현하면 다음 그림과 같은 형태
  • 이러면 전체 파악을 위해선 같은 색상을 계속 스크롤 하면서 봐야(jump)해야 한다
  • 논리적인 관점 단위로 개발하려 해도 위의 옵션들의 규칙 때문에 코드가 커질수록 가독성이 떨어지고 유지보수가 떨어짐
  • 위에서 같은 색상을 모을 수 있다면? → 이것이 바로 Composition API가 할 수 있는 일
setup 추가하기
  • 새로운 setup 컴포넌트 옵션은 컴포넌트 생성전 props가 한번 resolved될때 실행된다
  • Composition API 진입점 역할
  • setup이 실행될 때, 컴포넌트 인스턴스가 아직 생성되지 않은 상태
  • 따라서 setup옵션 내에 this가 존재하지 않음(아직)
  • 즉, props를 제외한, 아래와 같은 컴포넌트 내 다른 속성에 접근 불가
    → local state, computed properties 또는 methods.
  • setup 옵션은 props와 context에 접근하는 func : 상세가이드
    • 상세 가이드의 props와 ,context 부분 읽어둘 것
상세 가이드 : https://v3.ko.vuejs.org/guide/composition-api-setup.html#props

컴포지션 API에서 Provide / inject 사용

컴포지션 API에서 Provide / inject 사용을 위해선?
→ provide, inject를 별도로 import해야 사용 가능

CompositionAPIProvide.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<CompositionAPIInject/>
</template>

<script>
import {provide} from "vue";
import CompositionAPIInject from "./CompositionAPIInject";

export default {
name: "CompositionAPIProvide",
components: {CompositionAPIInject},
setup() {
// 부모에서 provide 함수를 통해서 전달할 key, value 설정
provide('title', 'Vue.js 프로젝트');
}
}
</script>
CompositionAPIInject.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<h1>{{ title }}</h1>
</template>

<script>
import {inject} from "vue";

export default {
name: "CompositionAPIInject",
setup() {
const title = inject('title');
// inject를 사용해서 provide에서 정의한 key로 데이터를 전달 받아 가져옴
return {title};
}
}
</script>

믹스인(Mixins)

개발의 공통 모듈

  • 자주 사용하는 기능 혹은 공통으로 사용하는 기능에 대해 메서드로 등록
  • 개발자들이 각자 개발하는 것이 아닌 해당 메서드를 사용

믹스인 : Vue의 공통 모듈

  • 이름에서 알 수 있듯이 믹스인 파일을 컴포넌트 안에 (in)삽입, 합쳐서(mix) 사용
  • 일반 언어의 공통 모듈처럼 메서드를 정의해서 사용 가능
  • 이외에도 Vue의 라이프 사이클 훅까지 사용 가능
  • 이벤트 훅까지 사용할 수 이?ㅆ다는 것이 큰 장점
  • 기능을 별도 구현후, 필요시 믹스인 파일을 컴포넌트에 결합해서 사용하는 방법을 사용

믹스인 예제 ) 각각의 컴포넌트에서 사용자의 권한을 체크

  1. 믹스인을 사용하지 않는 경우
  • 모든 컴포넌트에 사용자 권한 체크 코드 삽입 - 중복 코드 양산
  • 유지 보수가 힘듬
  1. 믹스인을 사용한 경우
  • 믹스인을 이용해 권한체크 로직 구현후 , 각각의 컴포넌트에 해당 믹스인 파일을 추가하면 끝
  • 컴포넌트에 동일 로직 사용할 필요가 있을 때 매우 유용
  1. 믹스인 사용시의 유지보수
  • 특정 기능 캡슐화
  • 코드의 수가 줄고 재사용성 증가
  • 해당 관심사 코드가 단일화되어 해당 부분만 수정하면 참조하고 있는 모든 컴포넌트에 반영

믹스인 파일 생성

공톰함수작성

api.js
1
2
3
4
5
6
7
8
9
10
11
12
13
import axios from "axios";

export default {
methods: {
async $callAPI(url, method, data) {
return (await axios({
method: method, url, data
}).catch(reason => {
console.log(reason);
})).data;
}
}
}
  • 함수이름 : $callAPI
    • 함수이름 $ prefix 사용 : 믹스인 파일을 사용하는 컴포넌트 내에 동일 메소드명의 오버라이딩 방지
    • 일반적으로 컴포넌트 정의 메소드명에는 $등의 prefix를 사용하지 않음
    • 따라서 이렇게 작성하면 컴포넌트의 메소드명과 구분 가능

컴포넌트에서 믹스인 사용

Mixins.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
import ApiMixin from "../api";

export default {
name: 'Mixins',
mixins: [ApiMixin],
data() {
return {
productList: []
};
},
async mounted() {
this.productList = await this.$api(
"https://874001e4-d76c-453b-973d-361e018aa35d.mock.pstmn.io/list",
"get");
console.log(this.productList);
}
}
</script>

믹스인에서 라이프사이클 훅 이용

상황

  • 어플리케이션 이용 이용자가 방문한 페이지, 페이지에 머문 시간 기록하는 코드 작성 가정
  • 믹스인에 사용자가 특정 페이지에 방문하고 빠져나갈때 db에 시간을 저장하는 메서드 작성
    • 각 컴포넌트에서 mounted 훅 발생시 믹스인의 방문 시작 메서드 호출
    • unmounted 훅이 발생할 때 믹스인의 방문 종료 메소드 호출 : db에 방문 시작, 종료 시간 기록
    • 해당 기록으로 페이지에 머문 시간 계산
  • 모든 컴포넌트의 mounted, unmounted마다 믹스인 메소드 호출?
    • 간편하나 불편한 작업. 반복적 작업
    • 개발자 실수로 특정 컴포넌트에서 해당 코드 미작성시 이력기록 실패
  • 믹스인에서 단순 메서드 정의가 아닌 컴포넌트 라이프 사이클 훅 이용 가능
    • 컴포넌트가 아닌 믹스인 파일의 mounted, unmounted에 코드 작성
    • 해당 컴포넌트를 사용하는 모든 컴포넌트는 자동으로 mounted, unmounted 될 때 방문 기록 저장 가능
믹스인
1
2
3
4
export default {
mounted() {console.log('믹스인 mounted');},
unmounted() {console.log('믹스인 unmounted');}
}
로그찍어서 어느시점에 나오는지 테스트
1
2
3
4
5
6
7
8
9
10
<script>
import lifeCycleMixin from "../mixin-lifecycle-mixin";

export default {
name: 'mixinLifecycleComponent',
mixins: [lifeCycleMixin],
mounted() {console.log('컴포넌트 mounted');},
unmounted() {console.log('컴포넌트 unmounted');}
}
</script>
  • 컴포넌트 라이프 사이클 훅 시점에 믹스인 코드가 먼저 실행된다
  • 2개의 파일이 같은 프로퍼티, 같은 라이프사이클 훅끼리 코드가 합쳐지나 믹스인 코드가 먼저 실행됨

믹스인 파일 전역 등록하기

main.js에 등록

  • 예를들어 이전 작성한 api호출 기능등은 어플리케이션의 거의 모든 컴포넌트가 사용하는 기능
  • 이러한 기능은 전역 등록이 좋음 → 각 컴포넌트에서 별도의 믹스인 추가 없이 사용 가능

커스텀 디렉티브(Custom Directives)

v-show, v-model같은 기본 디렉티브 외에 사용자가 직접 정의한 디렉티브

main.js 커스텀 디렉티브 추가
1
2
3
4
5
6
const app = createApp(App);
app.directive('focus', {
mounted(el) {
el.focus();
}
});
  • 컴포넌트가 마운트 되면 v-focus 디렉티브 적용한 HTML객체로 포커스를 위치(el.focus())
  • 실제로 위의 형태의 커스텀 디렉티브는 main.js에 전역 등록해서 많이 사용

커스텀 디렉티브 사용시에도 데이터 바인딩이 가능하다

v-pin 지정 컴포넌트 사용
1
2
3
<div>
<p v-pin="position">페이지 고정영역으로 고정</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
directives: {
pin: {
mounted(el, binding) {
el.style.position = 'fixed';
el.style.top = binding.value.top + 'px';
el.style.left = binding.value..left + 'px';
}
}
},
data () {
return {
position: {top:50, left:100}
}
}

플러그인

플러그인

  • 특정 기능을 제공하는 코드
  • Vue 프로젝트 진행시 유용한 플러그인들을 설치하고 사용하고 있다
  • NPM을 통해 설치되는 패키지 역시 플러그인
  • 때로는 모듈로 때로는 패키지로 사용될 수 있다
  • 대규모 프로젝트시 해당 프로젝트에 맞게 특화된 플러그인 제작이 필요할 수 있음
i18n.js
1
2
3
4
5
6
7
8
9
10
export default {
install: (app, options) => {
app.config.globalProperties.$translate = key => {
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
app.provide('i18n', options); //i18n 키로 다국어 데이터 전달
}
}
  • 플로그인은 install옵션에서 정의해서 사용 가능
  • app.config.globalProperties를 선언해서 컴포넌트에서 $translate 바로 접근 사용 가능
  • provide로 다국어 데이터 전달 → 컴포넌트에서는 inject를 이용해서도 사용 가능
main.js: 다국어 플러그인 전역 등록
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import i18nPlugin from "./plugins/i18n"; //i18n 플러그인 추가

const i18nStrings = {
en: {
hi: 'Hello!'
},
ko: {
hi: '안녕하세요!'
}
}

const app = createApp(App);
app.use(router);
app.use(i18nPlugin, i18nStrings); //i18n 플러그인에 다국어 번역 데이터를 파라미터로 전달
//...

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

Plugins.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<h2>{{$translate("ko.hi")}}</h2> <!-- $translate로 사용 -->
<h2>{{i18n.ko.hi}}</h2> <!-- inject로 사용 -->
</div>
</template>

<script>
export default {
name: "Plugins",
inject: ['i18n'],
mounted() {
console.log(this.i18n);
}
}
</script>

Related POST

공유하기