컴포지션 API
Composition API?
기존의 Vue의 문제 : 프로젝트 규모가 커질수록 관리가 힘들다
- 또한 대규모 어플리케이션일수록 코드 공유와 재사용이 특히 중요
- mixin을 통해 코드를 재사용하면 오버라이딩 문제, 다중 mixin인 경우 코드 관리가 어려움
- data, computed, watch, methods등 규모가 커질수록, 컴포넌트의 계층이 복잡해질수록 코드에 대한 추적 및 관리가 어려움
Composition API
상세 가이드 : https://v3.ko.vuejs.org/guide/composition-api-introduction.html
Setup
Setup
기존 방법과 비교
예시 - 사용자로부터 숫자 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";
export default { name: "CompositionAPI", setup() { let state = reactive({ num1: 0, num2: 0, result: 0 });
function plusNumbers() { state.result = parseInt(state.num1) + parseInt(state.num2); }
return { 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, result: computed(() => parseInt(state.num1) + parseInt(state.num2)) });
return { state } } } </script>
|
계산기에서 덧셈 연산 여러번 반복해서 사용 가능한 것 처럼 재사용 코드 만든다면?
- 현재 컴포넌트에서만 사용하는 코드를 여러 컴포넌트로 재사용 가능하도록 함수 분리 필요
setup 코드를 분리해서 별도의 function처리
setup에서 작성된 코드를 별도의 function1 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)) }); return toRefs(state); }
export default { name: "CompositionAPI3", setup() { let {num1, num2, result} = plusCalculator(); return { num1, num2, result } } } </script>
|
- 외부 func에서 반응형 변수를 사용하기 위해서 toRefs추가
- 컴포넌트 내부에서는 v-model 디렉티브로 바인딩된 변수가 사용자 입력값에 따라 반응형 처리
- 이 함수를 컴포넌트 밖으로 빼면 사용자가 입력한 값에 대한 반응형 처리 불가능
- toRefs를 사용하여 컴포넌트 밖에서도 반응형 처리가 가능하게 만듬
컴포넌트에서 정의된 코드를 다른 컴포넌트에서 사용가능하도록 외부 분리
common.js1 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(); 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() { onMounted(() =>{ console.log('Component is mounted!') }) } }
|
튜토리얼의 컴포지션 API 기초 예제 소개
1. 앱에서 특정 사용자 레포지터리
예시) : 특정 사용자의 레포지토리목록을 보여주는 검색과 필터 기능을 가진 컴포넌트
src/components/UserRepositories.vue1 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
|
export default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String } }, data () { return { repositories: [], filters: { ... }, searchQuery: '' } }, computed: { filteredRepositories () { ... }, repositoriesMatchingSearchQuery () { ... }, }, watch: { user: 'getUserRepositories' }, methods: { getUserRepositories () { }, updateFilters () { ... }, }, mounted () { this.getUserRepositories() } }
|
- 컴포넌트 to do
- 사용자 이름에 대한 외부 API로 레포지토리 가져오기와 사용자가 변경 될 때마다 갱신하기
- searchQuery 문자열을 사용하여 레포지토리 검색하기
- 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 부분 읽어둘 것
컴포지션 API에서 Provide / inject 사용
컴포지션 API에서 Provide / inject 사용을 위해선?
→ provide, inject를 별도로 import해야 사용 가능
CompositionAPIProvide.vue1 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('title', 'Vue.js 프로젝트'); } } </script>
|
CompositionAPIInject.vue1 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'); return {title}; } } </script>
|
믹스인(Mixins)
개발의 공통 모듈
- 자주 사용하는 기능 혹은 공통으로 사용하는 기능에 대해 메서드로 등록
- 개발자들이 각자 개발하는 것이 아닌 해당 메서드를 사용
믹스인 : Vue의 공통 모듈
- 이름에서 알 수 있듯이 믹스인 파일을 컴포넌트 안에 (in)삽입, 합쳐서(mix) 사용
- 일반 언어의 공통 모듈처럼 메서드를 정의해서 사용 가능
- 이외에도 Vue의 라이프 사이클 훅까지 사용 가능
- 이벤트 훅까지 사용할 수 이?ㅆ다는 것이 큰 장점
- 기능을 별도 구현후, 필요시 믹스인 파일을 컴포넌트에 결합해서 사용하는 방법을 사용
믹스인 예제 ) 각각의 컴포넌트에서 사용자의 권한을 체크
- 믹스인을 사용하지 않는 경우
- 모든 컴포넌트에 사용자 권한 체크 코드 삽입 - 중복 코드 양산
- 유지 보수가 힘듬
- 믹스인을 사용한 경우
- 믹스인을 이용해 권한체크 로직 구현후 , 각각의 컴포넌트에 해당 믹스인 파일을 추가하면 끝
- 컴포넌트에 동일 로직 사용할 필요가 있을 때 매우 유용
- 믹스인 사용시의 유지보수
- 특정 기능 캡슐화
- 코드의 수가 줄고 재사용성 증가
- 해당 관심사 코드가 단일화되어 해당 부분만 수정하면 참조하고 있는 모든 컴포넌트에 반영
믹스인 파일 생성
공톰함수작성
api.js1 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.vue1 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.js1 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); } }
|
- 플로그인은
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";
const i18nStrings = { en: { hi: 'Hello!' }, ko: { hi: '안녕하세요!' } }
const app = createApp(App); app.use(router); app.use(i18nPlugin, i18nStrings);
|
컴포넌트에서 사용하는 방법
Plugins.vue1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <template> <div> <h2>{{$translate("ko.hi")}}</h2> <h2>{{i18n.ko.hi}}</h2> </div> </template>
<script> export default { name: "Plugins", inject: ['i18n'], mounted() { console.log(this.i18n); } } </script>
|
Related POST