💻 Frontend
Vue Composition 리서치 및 TodoApp 구현
[목차]
1. Composition API 개요2. Composition 특징 1. Vue 컴포넌트 내부 그룹핑2-1) option API의 코드 구성 해체2-2) Composition 사용법A) setup에 다 때려박기B) data 선언은 reactive 혹은 ref로, methods는 그냥 함수 선언하듯이C) lifecycle은 on을 붙이자D) computed와 watch는 그대로 쓸 수 있다. (watchEffect라는 추가기능은 덤)E) props 받을 때는 인자로 넘겨주자 (context도 있대)F) 기타 (provide,inject)3. Composition 특징 2. 외부 그룹핑 파일 사용 3-1) Mixin3-2) HOC(Higher-Order Components)3-3) Composition4. Example (To do App)
1. Composition API 개요
Composition API의 핵심 기능은 그룹핑을 통한 유지보수 용이성 향상이다.
Vue3로 버전업하면서 생긴 신규 기능이다.
구성해서 할려고 하다보니 너무 어려워졌다.
여기서 그룹핑은 두 가지로 분리해서 생각해볼 수 있다.
1) 기존 Options API 구조의 해체를 통한 Vue 컴포넌트 내부 코드의 그룹핑
2) 외부에서 그룹핑한 파일을 Vue 컴포넌트로 읽어올 때 가독성 향상
2. Composition 특징 1. Vue 컴포넌트 내부 그룹핑
2-1) option API의 코드 구성 해체
기존 Vue 2의 Options API는 데이터 선언 및 조작을 위한 개별 메서드로 분산 작성된다.
한 컴포넌트에서 데이터와 메서드 등의 Option이 많아질수록 코드의 가독성이 떨어진다.
Composition API는 setup(){} 이라는 메서드에서 option의 구분 없이 코드를 사용하고, 기능별로 묶을 수 있다
Options API와의 비교
export default {
data() {
return {
books: []
};
},
methods: {
addBook(title, author) {
this.books.push({ title, author });
}
},
computed: {
formattedBooks() {
return this.books.map(book => `${book.title}은 ${book.author}가 썻다`);
}
}
};
2-2) Composition 사용법
구체적으로 Composition을 통해 어떻게 사용하는지는 아래의 사항들을 살펴보면 알 수 있다.
A) setup에 다 때려박기
내용
what is setup?
- setup hook은 컴포지션 API의 진입점 역할을 하는 함수.
- 해당 함수호출을 통해 별도의 빌드 과정 없이 composition api 사용을 할 수 있음.
- 추가로 Options API 구성과 더불어 Composition API 기반 코드와 통합할 수 있다.
- vue2의 lifecycle인 beforeCreate보다도 먼저 구성된다.
샘플코드
//setup 사용
<script>
export default {
name: "HOME",
setup() {
// 반응형 아님
let name = "nkh";
let age = 29;
const handleClick = () => {
console.log(1);
};
return { name, age, handleClick };
}
};
</script>
// <script setup> 사용
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>B) data 선언은 reactive 혹은 ref로, methods는 그냥 함수 선언하듯이
내용
먼저 알아둬야할 것!
- vue는 상태 비교에 Proxy 객체를 사용해서 setter값의 변화를 추적해 업데이트한다.
- 따라서 객체가 아닌 원시타입인 문자열이나 숫자 등을 상태로 넣어줄 때는 값을 추적하기 어렵다.
Reactive 방식
외부에서 data 선언을 할 때는 reactive라는 메서드로 감싸주고 해당 값을 return 해야한다.
//reactive.js 파일
import { reactive, computed, ref } from "vue";
const compositionReactiveSample = () => {
const samples = reactive({
number1: 0,
number2: 0,
sampleResult: computed(() => samples.number1 + samples.number2),
});
return toRefs(sample);
};
export { compositionReactiveSample };
// reactive.vue 파일
let { numbers, incremented } = compositionReactiveSample();Ref 방식
// ref
import { reactive, computed, ref } from "vue";
const number1 = ref(0);
const number2 = ref(0);
const sampleResult = ref(computed(() => number1.value + number2.value));
const incremented = () => number1.value++;
return { number1, number2, sampleResult, incremented };
};
export { compositionReactiveSample };
// ref.vue 파일
let { number1, number2, sampleResult, incremented } = compositionReactiveSample();Reactive vs Ref
Ref는 최종적으로 원시 값에 대해서 reactive를 씌워주는 역할을 한다. 그렇기 때문에 코드의 통일성을 위해 Ref로 통일을 해서 하나로 쭉 쓰는게 좋다고 판단됨.
C) lifecycle은 on을 붙이자
내용
<script>
export default {
// 사용할 props를 배열내에 정의합니다.
props: ["posts"],
setup(props) {
onMounted(() => console.log("component mounted"));
onUnmounted(() => console.log("component onUnmounted"));
onUpdated(() => console.log("component onUpdated"));
console.log(props.posts); // 받은 prop 사용가능
}
};
</script>D) computed와 watch는 그대로 쓸 수 있다. (watchEffect라는 추가기능은 덤)
내용
<script>
import { computed, ref, watch, watchEffect } from "vue";
export default {
name: "HOME",
setup() {
const search = ref("");
const names = ref(["qq", "aa", "zz", "dd"]);
const matchingNames = computed(() => {
return names.value.filter(name => name.includes(search.value));
});
watch(search, () => {
"search 값이 바뀔 때 마다 실행되는 함수";
});
watchEffect(() => {
console.log(
"search value가 정의됬기에 search가 바뀔때마다 실행된다",
search.value
);
});
return { names, search, matchingNames };
}
};
</script>watchEffect는 useEffect와 거의 동일하다고 보면 된다
(sideEffect cleanup function 등 추후 공부해보기)
E) props 받을 때는 인자로 넘겨주자 (context도 있대)
내용
부모요소에서 넘길 때
<template>
<dlv class="home">
<!-- child 컴포넌트에게 props 내림 -->
<PostList :posts="posts" />
</div>
</template>
<script>
// 사용할 컴포넌트 import
import PostList from '../components/PostList.vue'
import { ref } from 'vue';
export default {
name: 'Home',
// 사용할 컴포넌트를 넣어줍니다.
components: { PostList },
setup() {
const posts = ref([
{ title: '1번 타이틀', body: '1번 제목', id: 1 },
{ title: '2번 타이틀', body: '2번 제목', id: 2 },
]);
return { posts }
}
}
</script>자식요소에서 넘길 때
<template>
<div>
{{ post.title }}
{{ post.body }}
</div>
</template>
<script>
export default {
// 사용할 props를 배열내에 정의합니다.
props: ["posts"],
setup(props) {
console.log(props.posts); // 받은 prop 사용가능
}
};
</script>F) 기타 (provide,inject)
내용
- react의 context api같은 역할을 함. props drilling을 막기 위해 사용함

// 부모 컴포넌트에서 provide 설정
setup(){
components:{
CompositionAPIInject
}
provide('title', 'Vue.js 프로젝트') // {title:'Vue.js 프로젝트'}를 넘기게 됨.
}
// 자식 컴포넌트에서 inject 받기
setup(){
const title = inject('title')
return {title}
}
3. Composition 특징 2. 외부 그룹핑 파일 사용
vue3가 나오기 전에 외부의 그룹핑 파일을 불러오는 것은 Mixin을 활용했다.
Mixin과 유사한 Composition의 비교를 통해 어떤 장점이 있는지 살펴보자.
3-1) Mixin
Mixin은 말 그대로 넣고 섞는 작업이다. 공식 문서에는 Option Merging이라고 이야기한다.
즉, 특정 데이터와 메서드들을 그룹핑(모듈화)해서, 기존 Vue 파일에 합치는 option 기능 중 하나.
주요특징 및 샘플코드
주요 특징
- option형태로 js파일에서 객체 안에 data,methods를 넣는다.
- 사용하고자 하는 vue 파일에서 mixins key에 value로 배열값으로 넣어준다.
- vue 파일에서 해당 파일들을 불러올 수 있다.
장점
- import 작업 없이 원하는 데이터들을 Mixin으로 불러올 수 있다.
단점
- 다중 믹스인, 즉 한 개의 vue 파일에서 여러개의 믹스인을 사용하게 되었을 때 네이밍이 명확하지 않을 때 다 들어가봐야 해당 로직이 어떤 작업을 하는지 알 수 있다.
// mixinSample.js
let mixinSample = {
data() {
return {
sample: "hi",
};
},
created() {
console.log("mixin");
},
methods: {
onClick() {
console.log("hi");
},
},
};
export default mixinSample;
// mixinVue.vue
<div class="mixin_component">
<div>mixin 예제 : {{ sample }}</div>
<button @click="onClick">mixinButton</button>
</div>
export default {
mixins: [mixinSample], // mixin으로 합쳐진 값은 기존 data,method와 동일하게 접근 가능
data(){...},
methods:{...}
};3-2) HOC(Higher-Order Components)
HOC, 고차 컴포넌트는 컴포넌트 자체를 모듈화하여 재사용하는 패턴이다.
(별도의 Option이나 Methods 아님)
주요 특징 및 샘플코드
일반적으로 HOC를 이용하여 컴포넌트를 구현하게 되면 다음과 같이 컴포넌트 관계에서 층이 하나 더 생긴다.
- 일반 : 상위 - 하위
- HOC : 상위 - HOC - 하위
장점
- 컴포넌트 간의 역할이 완전히 분리된 상태로 기능 확장 가능
단점
- 컴포넌트 레이어가 깊어지고, 이에 따른 props와 event 주입 처리가 귀찮아짐
// CreateListComponent.js
import bus from './bus.js'
import ListComponent from './ListComponent.vue';
export default function createListComponent(componentName) {
return {
name: componentName,
mounted() {
bus.$emit('off:loading');
},
render(h) {
return h(ListComponent);
}
}
}
// router.js
import createListComponent from './createListComponent.js';
new VueRouter({
routes: [
{
path: 'products',
component: createListComponent('ProductList')
},
{
path: 'users',
component: createListComponent('UserList')
},
...
]3-3) Composition
Composition은 파일의 변수명을 각각 불러와 사용하기 때문에
해당 변수명이 어디에서 작성된 코드인지 보다 직관적으로 알 수 있다는 것이 가장 큰 특징이다.
주요 특징 및 샘플코드
<template lang="">
<div>
<span>composition 컴포넌트 외부 선언 : </span>
<input type="number" v-model="numbers.number1" />
<span>+</span>
<input type="number" v-model="numbers.number2" />
<span>=</span>
<span>{{ numbers.sampleResult }}</span>
<button @click="incremented">increment</button>
</div>
</template>
<script>
import { compositionReactiveSample } from "../components/composition/compositionExample";
export default {
setup() {
let { numbers, incremented } = compositionReactiveSample();
return {
incremented
numbers,
};
}
};
</script>4. Example (To do App)

example sample
<template lang="">
<div class="todos_wrapper">
<h1>TO-DO IT!</h1>
<div class="input_todo">
<b-form-input
v-model="todoText"
placeholder="Enter your todos"
v-on:keyup.enter="addTodo"
></b-form-input>
<b-button variant="success" @click="addTodo">ADD</b-button>
</div>
<b-list-group class="list_todo">
<b-list-group-item
v-for="item in todoList"
:key="item.id"
class="list_todo_container"
>
<s v-if="item.completed" class="list_todo_text">{{ item.text }}</s>
<div v-else>{{ item.text }}</div>
<div>
<b-button
variant="outline-primary"
size="sm"
@click="completeTodo(item)"
>DONE</b-button
>
<b-button
variant="outline-danger"
size="sm"
@click="removeTodo(item.id)"
>DELETE</b-button
>
</div>
</b-list-group-item>
</b-list-group>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
let todoText = ref("");
let todoList = ref([]);
const addTodo = () => {
if (!todoText.value) {
alert("Please enter some text");
return;
} else {
todoList.value.push({
id: Math.floor(Math.random() * 1000 + 1),
text: todoText.value,
completed: false,
});
todoText.value = "";
}
};
const completeTodo = (todoItem) => {
todoItem.completed = !todoItem.completed;
};
const removeTodo = (todoItemId) => {
todoList.value = todoList.value.filter((item) => item.id !== todoItemId);
};
return {
todoText,
todoList,
addTodo,
completeTodo,
removeTodo,
};
},
};
</script>
