💻 Frontend

Vue Composition 리서치 및 TodoApp 구현

date
Jan 9, 2023
slug
vue-composition
author
status
Public
tags
Vue.js
Web
summary
Vue Composition 리서치 및 TodoApp 구현
type
Post
thumbnail
스크린샷 2023-01-04 오후 5.12.19.png
category
💻 Frontend
updatedAt
Apr 12, 2023 04:23 PM
 
[목차]
 
 

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}가 썻다`);
    }
  }
};
notion image
 
 

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을 막기 위해 사용함
    • notion image

// 부모 컴포넌트에서 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)

notion image
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>