Vue는 프로젝트 규모가 커질수록 관리하기 힘들었다
data, computed, watch, methods
등 컴포넌트의 계층구조가 복잡할수록 코드에 대한 추적 및 관리가 힘들었다
컴포지션 API는 setup
메소드 안에서 한 덩어리로 연관성 있는 로직을 코드로 구현할 수 있어 코드 관리가 쉬워진다
mixin
을 활용했으나 오버라이딩, 다중 믹스인 등의 문제로 관리가 어려웠다setup
은 컴포지션 API를 구현하는 곳이다
기존 개발 방법과 컴포지션 API를 비교해보자
기존 개발 방법 > src/views/Calculator.vue
<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 > src/views/CompositionAPI.vue
<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>
import { reactive } from "vue";
export default {
name: "calculator",
setup() {
let state = reactive({
num1: 0,
num2: 0,
result: 0,
});
function plusNumbers() {
state.result = parseInt(state.num1) + parseInt(state.num2);
}
return {
state,
plusNumbers,
};
},
};
</script>
setup()
메소드에서 반환된 값들은 data
옵션과 동일하게 사용이 가능하다reactive
를 이용해서 코드를 작성했으나, 이전과 크게 다를 바가 없다src/views/CompositionAPI2.vue
<template>
<div>
<h2>Calculator</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 { reactive, computed } from "vue"; // computed 추가
export default {
name: "calculator",
setup() {
let state = reactive({
num1: 0,
num2: 0,
// computed를 이용해서 num1, num2가 변경이 일어나면 즉시 result로 더한 값을 반환
result: computed(() => parseInt(state.num1) + parseInt(state.num2)),
});
return {
state,
};
},
};
</script>
작성한 코드를 여러 컴포넌트에서 재사용할 수 있도록 함수를 분리해본다
src/views/CompositionAPI3.vue
<template>
<div>
<h2>Calculator</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 { reactive, computed, toRefs } from "vue"; // toRefs 추가
function plusCalculator() {
let state = reactive({
num1: 0,
num2: 0,
result: computed(() => parseInt(state.num1) + parseInt(state.num2)),
});
return toRefs(state); // 반응형으로 선언된 num1, num2, result가 외부에서 정상적으로 동작하기 위해선 toRefs를 사용해야 함
}
export default {
name: "calculator",
setup() {
let { num1, num2, result } = plusCalculator(); // 외부 function
return {
num1,
num2,
result,
};
},
};
</script>
toRefs
를 사용했다v-model
디렉티브를 통해 바인딩된 변수가 사용자의 입력값에 따라 반응형으로 처리가 되었지만, plusCalculator
함수를 밖으로 빼면 반응형 처리가 불가능해 toRefs
를 사용하여 컴포넌트 밖에서도 반응형 처리가 가능하도록 할 수 있다Vue 컴포넌트에서는 common.js 등으로부터 import 해서 사용한다
src/views/CompositionAPI4.vue
<template>
<div>
<h2>Calculator</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.js";
export default {
name: "calculator",
setup() {
let { num1, num2, result } = plusCalculator(); // 외부 function
return {
num1,
num2,
result,
};
},
};
</script>
컴포지션 API의 setup()
은 beforeCreate
와 create
훅 사이에서 실행되기 때문에 onBeforeCreate
, onCreate
는 필요가 없고, 나머지 라이프사이클 훅 메소드의 앞에 on
을 붙여 사용한다
export default {
setup() {
// mounted()
onMounted(() => {
console.log("Component is mounted!");
});
},
};
컴포지션 API에서 Provide/Inject를 사용하려면 각각 별도로 import를 해야한다
부모 컴포넌트에서는 provide 함수를 통해서 전달할 값에 대한 키, 값을 설정한다
src/views/CompositionAPIProvide.vue
<template>
<CompositionAPIInject />
</template>
<script>
import { provide } from "vue";
import CompositionAPIInject from "./CompositionAPIInject";
export default {
components: {
CompositionAPIInject,
},
setup() {
provide("title", "Vue.js 프로젝트");
},
};
</script>
src/views/CompositionAPIInject.vue
<template>
<h1>{{ title }}</h1>
</template>
<script>
import { inject } from "vue";
export default {
setup() {
const title = inject("title");
return { title };
},
};
</script>
일반적인 공통 모듈처럼 메소드를 정의해서 사용할 수 있고, Vue의 라이프사이클 훅까지 사용할 수 있다
믹스인(mixin)은 기능을 따로 구현하고, 필요시 믹스인 파일을 컴포넌트에 결합해서 사용한다
예를 들어, 애플리케이션의 모든 컴포넌트에서 사용자가 접근할 때 마다 접근 권한이 있는지를 체크한다고 가정해 보면,
axios를 이용해서 서버 데이터를 호출했던 메소드를 믹스인으로 만들어 보자
src 폴더에 api.js 파일을 생성하고 axios 패키지를 이용해서 서버와의 데이터 통신을 위한 공통 함수를 작성한다
src/api.js
import axios from "axios";
export default {
methods: {
async $callAPI(url, method, data) {
return await axios({
method: method,
url,
data,
}).catch((e) => {
console.log(e);
}).data;
},
},
};
$callAPI()
: $
prefix는 믹스인 파일을 사용하는 컴포넌트 내에 동일한 메소드명이 있어서 오버라이딩 되는 것을 방지하기 위함이다다음과 같이 mixins 프로퍼티에 사용할 믹스인 파일을 정의해서 사용한다
src/views/Mixins.vue
<template>
<div>
<div>{{ htmlString }}</div>
<div v-html="htmlString"></div>
</div>
</template>
<script>
import ApiMixin from "../api.js";
export default {
mixins: [ApiMixin],
data() {
return {
productList: [],
};
},
async mounted() {
this.productList = await this.$callAPI(
"https://3d5348c0-4158-4724-ad6f-352e7ef79ceb.mock.pstmn.io/list",
"get"
);
console.log(this.productList);
},
};
</script>
애플리케이션 사용자의 페이지 방문 시간을 기록하는 코드를 작성한다고 가정하자
믹스인에서는 단순히 메소드만 정의해서 사용하는 것이 아닌, 컴포넌트의 라이프사이클 훅을 그대로 이용할 수 있다
즉, 믹스인 파일에 mounted, unmounted
마다 방문 시간과 종료 시간을 기록하는 코드를 작성하면, 해당 믹스인 파일을 사용하는 모든 컴포넌트에서는 자동으로 컴포넌트가 mounted, unmounted
될 때 방문 기록을 저장할 수 있다
mixins.js
mounted() {
console.log("믹스인 mounted");
},
unmounted() {
console.log("믹스인 unmounted");
}
Component.vue
mounted() {
console.log("컴포넌트 mounted");
// 믹스인 mounted
// 컴포넌트 mounted
},
unmounted() {
console.log("컴포넌트 unmounted");
// 믹스인 unmounted
// 컴포넌트 unmounted
},
api를 호출하는 기능은 애플리케이션 내의 거의 모든 컴포넌트에서 사용하는 기능이므로 전역으로 등록해서 각 컴포넌트에서 별도의 mixin 추가 없이 사용할 수 있도록 한다
src/mixins.js
import axios from "axios";
export default {
methods: {
async $api(url, method, data) {
return (
await axios({
method: method,
url,
data,
}).catch((e) => {
console.log(e);
})
).data;
},
},
};
src/main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import mixins from "./mixins";
const app = createApp(App);
app.use(router);
app.mixin(mixins);
app.mount("#app");
Vue 에서는 v-model, v-show와 같은 기본 디렉티브 외에도 개발자가 직접 디렉티브를 정의해서 사용할 수 있다
커스텀 디렉티브는 전역에서 사용하도록 등록할 수도, 특정 컴포넌트 안에서만 사용하도록 등록할 수도 있다
사용자가 컴포넌트에 접속했을 때 지정된 입력 필드로 포커스를 위치시킬 수 있는 커스템 디렉티브를 만들어 보자
src/main.js
app.directive("focus", {
mounted(el) {
el.focus();
},
});
v-focus
디렉티브를 적용한 HTML 객체로 포커스(el.focus()
)를 위치시키도록 작성하였다<input type="text" v-focus />
전역이 아닌, 컴포넌트 내에 등록해서 사용할 땐, 해당 컴포넌트의 directives 속성을 이용한다
directives {
focus: {
mounted(el) {
el.focus()
}
}
}
커스텀 디렉티브 사용 시에도 데이터 바인딩이 가능하다
<template>
<div>
<div style="height: 1000px">
<p v-pin="position">페이지 고정 영역</p>
</div>
</div>
</template>
<script>
export default {
directives: {
pin: {
mounted(el, binding) {
el.style.position = "fixed";
el.style.top = binding.value.top + "px";
el.style.left = binding.value.left + "px";
},
},
},
data: function () {
return {
position: { top: 50, left: 100 },
};
},
};
</script>
v-pin
디렉티브에 data 옵션의 position을 바인딩한다v-pin
디렉티브가 지정된 HTML 객체의 position을 고정한다Vue에서는 직접 플러그인을 제작해서 전역으로 사용할 수 있게 해준다
다국어(i18n)을 처리해 주는 플러그인을 만들어 보자
src/plugins/i18n.js
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 키로 다국어 데이터 전달
},
};
$translate
로 바로 접근해서 사용할 수 있다또한, provide로 다국어 데이터를 전달하고 각 컴포넌트에서 inject를 이용해 사용 가능하다
src/main.js
import i18nPlugin from "./plugins/i18n";
const i18nStrings = {
en: {
hi: "Hello!",
},
ko: {
hi: "안녕하세요!",
},
};
app.use(i18nPlugin, i18nStrings);
src/views/Plugins.vue
<template>
<!-- $translate 으로 사용 -->
<h2>{{ $translate("ko.hi") }}</h2>
<!-- inject로 사용 -->
<h2>{{ i18n.ko.hi }}</h2>
</template>
<script>
export default {
inject: ["i18n"],
};
</script>
출처: 고승원 저, 『Vue.js 프로젝트 투입 일주일 전』, 비제이퍼블릭(2021)