🚨1. 현재 프로젝트의 문제점
제가 하고 있는 프로젝트의 여러 컴포넌트에는 현재시간을 불러와서 조회하는 기능이 있습니다. 같은 기능인데 개발자가 다르다 보니 컴포넌트 마다 현재시간을 불러오는 방법이 각각 다르게 구현되어 있었습니다. 최근에 이 시간 조회 방식을 변경해야 했는데, 모든 컴포넌트를 돌아다니면서 수정을 했어야해서 여간 불편했습니다😥
그래서 재사용 가능 기능(여기선 현재시간을 불러오는 기능)을 여러 컴포넌트에 분산시킬 때, 어떤 방법이 좋을지 비교해보았습니다.
💡2. Vue의 코드 재사용 방법
1) EventBus
EventBus는 Vue 인스턴스를 이용하여 한 컴포넌트에서 다른 컴포넌트로 이벤트를 전달하는 방법입니다. 이를 통해 컴포넌트간에 데이터나 알림을 보낼 수 있습니다.
👍 장점: "구현하기 쉽고 간단"
👎 단점: "(현재 내가 진행하는 정도의) 대규모 어플리케이션에서는 이벤트 흐름 관리가 어렵고 디버깅이 복잡"
2) Vuex
Vuex는 Vue.js 전용 상태 관리 패턴 및 라이브러리입니다. Vuex를 사용하면 애플리케이션 내 모든 컴포넌트의 상태를 중앙에서 관리할 수 있습니다.
👍 장점: "중앙 집중식 상태 관리로 일관된 데이터 흐름을 가질 수 있어 대규모 어플리케이션에 효과적"
👎 단점: "데이터가 변경되었을 때, 그 데이터를 사용하는 모든 컴포넌트들도 이 변경사항을 반영해야 하는 경우에 쓰면 좋은데, 현재 내가 원하는 기능은 컴포넌트가 마운트 될 때 현재 시간을 딱 한번만 불러오면 되는 거라서.. 굳이(?) 라는 생각이 듬 "
3) Mixin
Mixin은 Vue 컴포넌트에 재사용 가능한 기능을 모아놓는 객체를 의미합니다. 공통 관심사를 mixin에 분리하고 필요로 하는 컴포넌트에서 가져다 쓸 수 있게 합니다.
👍 장점
"여러 컴포넌트에서 일관된 로직을 유지"
"vue의 라이프사이클 훅을 mixin에서 정의하여, 컴포넌트의 생명주기에 맞춰 필요한 로직을 실행 할 수 있다"
→ 예를 들어 mixin에서 created 라이프사이클 훅에 '유저 권한을 확인하는 함수'를 정의하면, mixin을 호출하는 모든 컴포넌트가 생성될때 유저 권한을 먼저 확인할 수 있다.
"Vue2, Vue3와의 호환성이 좋음"
👎 단점:
"네임스페이스 충돌 - 컴포넌트 안의 메서드나 데이터에 속성이 mixin과 같은 경우 충돌되어 디버깅 어려움"
"유지보수성의 저하 - 컴포넌트에서 mixin을 호출하면 해당 mixin이 함께 동작하는데, 코드의 어느부분이 Mixin에서 오는 지 직관으로 보여지지 않음"
4) Compositoin API
Compositon API는 Vue 컴포넌트의 코드를 더 잘 조직하고 관리하기 위한 도구입니다 . 관심사가 같은 로직(데이터, 메소드 등)을 한 곳에서 관리할 수 있어 코드를 더 깔끔하게 정의하고, 재사용하기 쉽게 만들어졌습니다.
👍 장점: "setup 함수를 이용한 명확한 로직 구조", "특정 기능을 쉽게 추출하고 다른 컴포넌트에서 재사용하기 편함",
" 특정 조건에 따라 라이프 사이클 훅을 실행하거나 실행하지 않는 것이 가능"
👎 단점: "기존 Vue 2 코드베이스와의 통합이 어려울 수 있음"
🚩3. Mixin vs Composition API 구현해보기
현 상황에서 단점이 명확한 EventBus와 Vuex는 제외하고, Mixin과 Compositon API를 직접 구현해보면서 어떤 방법이 더 좋은지 직접 비교해보겠습니다.😉 먼저, 각 컴포넌트에서 사용하는 현재시간을 다 모아보겠습니다. 그 다음 어떻게 통일 화할지 정의하고, Mixin과 Composition API를 각각 구현해서 비교해보겠습니다.
1) 각 컴포넌트에서 사용하는 시간 스타일 확인
1. yyyy-mm-dd ex) '2024-01-17'
SpacetimeModal.vue, BoardInfo.vue, AccidentMonitor.vue, EquipmentEvent.vue, DataReport, AllTraffic, AixsTraffic.vue, Intersection.vue, Avenue.vue (getToday(), getDate() 함수로 현재시간 불러옴)
const now = new Date();
const koreanTime = new Date(now.getTime() + now.getTimezoneOffset() * 60000 + 9 * 3600000);
const day = koreanTime.toISOString().split('T')[0];
Map2.vue
let now = new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().substr(0,10)
2. yyyy-mm-dd hh:mm:ss ex) 2024-01-17 13:08:20
- Header.vue, exportTree.vue, Master.vue, DashBoard.vue,
new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().replace("T", " ").substring(0, 19)
3. yyyy-mm-dd, hours(1시간 전 시간) ex) '2024-01-17', {hours: '10'}
LOS.vue
const now = new Date();
const koreanTime = new Date(now.getTime() + now.getTimezoneOffset() * 60000 + 9 * 3600000); // 한국 시간대로 조정
this.date = koreanTime.toISOString().split("T")[0];
koreanTime.setHours(koreanTime.getHours() - 1);
this.time = {
hours: `${("0" + koreanTime.getHours()).slice(-2)}`,
};
4. yyyy-mm-dd(오늘), yyyy-mm-dd(어제), hours ex) '2024-01-17', '2024-01-17' , {hours: '11'}
DatePickerComponents.vue
const day = koreanNow.toISOString().split('T')[0];
const yesterday = new Date(koreanNow.getTime() - (24 * 3600000)).toISOString().split('T')[0];
const time = { hours : ('0' + koreanNow.getHours()).slice(-2) };
5. yyyy-mm-dd(오늘), yyyy-mm-dd(해당 월의 첫날), yyyy-mm-dd(해당 월의 마지막날), yyyy, mm, dd
CompareIntersection.vue, CompareAvenue.vue
let today = new Date();
let year = today.getFullYear();
let month = String(today.getMonth() + 1).padStart(2, 0);
let date = String(today.getDate()).padStart(2, 0);
let first = new Date(today.getFullYear(), today.getMonth(), 1);
let firstDate = `${year}-${month}-${String(first.getDate()).padStart(2, "0")}`;
let last = new Date(today.getFullYear(), today.getMonth() + 1, 0);
let lastDate = `${year}-${month}-${String(last.getDate()).padStart(2, "0")}`;
6. yyyy-mm-dd(어제), yyyy-mm-dd(일주일전) ex) '2024-01-16', '2024-01-09'
SignReport.vue
const nownow = new Date(koreanNow.getTime());
nownow.setDate(nownow.getDate() - 1);
this.endDate = `${nownow.toISOString().split('T')[0]}`;
const yesterday = new Date(koreanNow.getTime());
yesterday.setDate(yesterday.getDate() - 8);
this.startDate = `${yesterday.toISOString().split('T')[0]}`;
7. 제외
GlobalChart.vue(excel Date, realTimeLineChart) , ValidityModal.vue, DateReport.vue
2) 시간 관련 데이터, 함수 재정의
- 오늘: today(yyyy-mm-dd)
- 어제: yesterday (yyyy-mm-dd)
- 당월의 첫날: firstDayOfMonth(yyyy-mm-dd)
- 당월의 마지막날: lastDayOfMonth(yyyy-mm-dd)
- 오늘로부터 일주일전 날짜: oneWeekAgo(yyyy-mm-dd)
- 현재날짜와시간: currentDateTime (yyyy-mm-dd hh:mm:ss)
- 현재시간: currentHour (hh)
- 1시간 전 시간: oneHourAgo (hh)
(1) Data 정의
data() {
return {
today: null, // yyyy-mm-dd
yesterday: null, // yyyy-mm-dd
firstDayOfMonth: null, // yyyy-mm-dd
lastDayOfMonth: null, // yyyy-mm-dd
oneWeekAgo: null, // yyyy-mm-dd
currentDateTime: null, // yyyy-mm-dd hh:mm:ss
currentHour: null, // hh
oneHourAgo: null, // hh
};
},
2) 함수 생성
methods: {
// 한국시간을 가져오는 함수
toKST(date) {
const utc = date.getTime() + (date.getTimezoneOffset() * 60000); // UTC시간대로 변경
const kstOffset = 9 * 60 * 60 * 1000; // 한국 시간대 조정
return new Date(utc + kstOffset);
},
// yyyy-mm-dd 형식으로 포맷팅하는 함수
formatDate(date) {
const pad = (num) => (num < 10 ? '0' + num : num);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
},
// yyyy-mm-dd hh:mm:ss 형식으로 포맷팅하는 함수
formatDateTime(date) {
const pad = (num) => (num < 10 ? '0' + num : num);
return `${this.formatDate(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
},
// hh 형식으로 포맷팅하는 함수
formatHour(num) {
return num < 10 ? '0' + num : num.toString();
},
getToday() {
const today = this.toKST(new Date());
this.today = this.formatDate(today);
console.log('today: ', this.today)
},
getYesterday() {
const today = this.toKST(new Date());
today.setDate(today.getDate() - 1);
this.yesterday = this.formatDate(today);
console.log('yesterday: ', this.yesterday)
},
getFirstDayOfMonth() {
const firstDay = this.toKST(new Date());
firstDay.setDate(1);
this.firstDayOfMonth = this.formatDate(firstDay);
console.log('firstDayOfMonth: ', this.firstDayOfMonth)
},
getLastDayOfMont() {
const lastDay = this.toKST(new Date());
lastDay.setMonth(lastDay.getMonth() + 1, 0); // 현재 날짜의 다음 달 0번째 날짜
this.lastDayOfMonth = this.formatDate(lastDay);
console.log('lastDayOfMonth: ', this.lastDayOfMonth)
},
getOneWeekAgo() {
const oneWeekAgo = this.toKST(new Date());
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
this.oneWeekAgo = this.formatDate(oneWeekAgo);
console.log('oneWeekAgo: ', this.oneWeekAgo)
},
getCurrentDateTime() {
const now = this.toKST(new Date());
this.currentDateTime = this.formatDateTime(now);
console.log('currentDateTime: ', this.currentDateTime)
},
getCurrentHour() {
const now = this.toKST(new Date());
this.currentHour = this.formatHour(now.getHours());
console.log('currentHour: ', this.currentHour)
},
getOneHourAgo() {
const oneHourAgo = this.toKST(new Date());
oneHourAgo.setHours(oneHourAgo.getHours() - 1);
this.oneHourAgo = this.formatHour(oneHourAgo.getHours());
console.log('oneHourAgo: ', this.oneHourAgo)
}
}
3) Mixin 적용하기
❓ Mixin에서 생명주기에 맞춰 필요한 로직을 실행해볼래?
vue의 라이프사이클 훅을 mixin에서 정의하면, 컴포넌트의 생명주기에 맞춰 필요한 로직을 실행할 수 있습니다. 예를 들어, mixin.js의 created 라이프사이클 훅에 '현재시간을 계산하는 기능(getCurrentDay())'을 정의하면, mixin을 호출하는 모든 컴포넌트가 생성될때, 현재시간을 받아올 수 있습니다.
ex) somComponent1.vue클릭: Mixin의 created 훅이 먼저 호출(현재시간 계산) → 그 후에 컴포넌트의 created 훅이 호출
요 장점을 살리고자, 원래는 Mixin의 라이프사이클 훅을 사용해서 시간을 받아오려고 했습니다... 그런데 '1) 각 컴포넌트에서 사용하는 시간 스타일 확인' 결과... 사용하는 시간 종류가 많아서 안되겠더라구요.. 아래처럼 created에 원하는 시간 계산 함수를 다 넣으면, mixin을 호출하는 컴포넌트가 실행될때 마다 created()가 실행되면서 불필요한 함수들을 다 호출하게되더라구요.. 그래서 이 방법은 패쓰..!
ex) somComponent1.vue클릭: Mixin의 created 훅이 먼저 호출(모든 함수 다 계산) → 컴포넌트에선 현재시간만 사용
// mixin.js의 라이프 사이클 훅 사용하기
created() {
this.getCurrentday();
this.getYesterday();
this.getFirstDayOfMonth();
this.getLastDayOfMonth();
this.getOneWeekAgo();
this.getCurrentDateTime();
this.getCurrentHour();
this.getOneHourAgo();
},
❗ Computed속성 활용해서 날짜 관련된 값들은 캐싱해서 성능 높이기
날짜 관련된 값들을 함수를 호출할 때마다 계산하는 것보다 한번만 계산하고 계산된 값을 받아오는 게 성능 면에서 좋을 것 같더라구요. 그래서 today(), yesterday(), firstDayOfMonth(), lastDayOfMonth(), oneWeekAgo()와 같은 날짜 관련 값들은 computed 속성을 통해 캐싱하고, getCurrentDateTime(), getCurrentHour(), getOneHourAgo()와 같은 현재 시간과 관련된 값들은 실시간으로 계산되도록 메소드를 통해 처리하였습니다.
mixin.js
export default {
computed: {
today() {
return this.formatDate(this.toKST(new Date()));
},
yesterday() {
const date = this.toKST(new Date());
date.setDate(date.getDate() - 1);
return this.formatDate(date);
},
firstDayOfMonth() {
const date = this.toKST(new Date());
date.setDate(1);
return this.formatDate(date);
},
lastDayOfMonth() {
const date = this.toKST(new Date());
date.setMonth(date.getMonth() + 1, 0);
return this.formatDate(date);
},
oneWeekAgo() {
const date = this.toKST(new Date());
date.setDate(date.getDate() - 7);
return this.formatDate(date);
},
},
methods: {
toKST(date) {
const utc = date.getTime() + (date.getTimezoneOffset() * 60000);
const kstOffset = 9 * 60 * 60 * 1000;
return new Date(utc + kstOffset);
},
formatDate(date) {
const pad = (num) => (num < 10 ? '0' + num : num);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
},
formatDateTime(date) {
const pad = (num) => (num < 10 ? '0' + num : num);
return `${this.formatDate(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
},
formatHour(num) {
return num < 10 ? '0' + num : num.toString();
},
getCurrentDateTime() {
return this.formatDateTime(this.toKST(new Date()));
},
getCurrentHour() {
return this.formatHour(this.toKST(new Date()).getHours());
},
getOneHourAgo() {
const date = this.toKST(new Date());
date.setHours(date.getHours() - 1);
return this.formatHour(date.getHours());
}
}
};
SomeComponent.vue
<template>
<div class="someComponent">
<h1>{{ today }}</h1>
</div>
</template>
<script>
export default {
name: "someComponent",
mixins: [ mixin ],
data(){
return {
date: ''
}
},
mounted(){
this.date = this.today
}
}
4) Compositon API 적용하기
useDate.js
import { computed } from 'vue';
export function useDate() {
const toKST = (date) => {
const utc = date.getTime() + (date.getTimezoneOffset() * 60000);
const kstOffset = 9 * 60 * 60 * 1000;
return new Date(utc + kstOffset);
};
const formatDate = (date) => {
const pad = (num) => (num < 10 ? '0' + num : num);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
};
const formatDateTime = (date) => {
const pad = (num) => (num < 10 ? '0' + num : num);
return `${formatDate(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const formatHour = (num) => {
return num < 10 ? '0' + num : num.toString();
};
const today = computed(() => formatDate(toKST(new Date())));
const yesterday = computed(() => {
const date = toKST(new Date());
date.setDate(date.getDate() - 1);
return formatDate(date);
});
const firstDayOfMonth = computed(() => {
const date = toKST(new Date());
date.setDate(1);
return formatDate(date);
});
const lastDayOfMonth = computed(() => {
const date = toKST(new Date());
date.setMonth(date.getMonth() + 1, 0); // 현재 날짜의 다음 달 0번째 날짜
return formatDate(date);
});
const oneWeekAgo = computed(() => {
const date = toKST(new Date());
date.setDate(date.getDate() - 7);
return formatDate(date);
});
const getCurrentDateTime = () => formatDateTime(toKST(new Date()));
const getCurrentHour = () => formatHour(toKST(new Date()).getHours());
const getOneHourAgo = () => {
const date = toKST(new Date());
date.setHours(date.getHours() - 1);
return formatHour(date.getHours());
};
return {
today,
yesterday,
firstDayOfMonth,
lastDayOfMonth,
oneWeekAgo,
getCurrentDateTime,
getCurrentHour,
getOneHourAgo
};
}
SomeComponent.vue
<template>
<div class="dialog">
<div class="pickerWrap">
<Datepicker v-model="date" auto-apply modelType="yyyy-MM-dd"/>
<div class="startDate">
<p v-if="date">{{ date }}</p>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { useDate } from '@/composable/useDate.js';
export default {
name: 'Somecomponent',
data(){
return {
axis: 1
// date: '',
},
setup() {
const { today } = useDateUtils();
const date = ref(today.value); // today의 현재 값을 date에 할당
return {
date
};
},
mounted(){
// this.getToday()
},
methods: {
// getToday(){
// const now = new Date();
// const koreanTime = new Date(now.getTime() + now.getTimezoneOffset() * 60000 + 9 * 3600000);
// this.date= koreanTime.toISOString().split('T')[0];
// },
// date가 바뀌었을때 실행되는 함수
bindDialog(axis){
.....
},
watch: {
date(e){
this.bindDialog(this.axis);
},
}
}
❗ Composition API를 이용해서 모듈화를 했더니, 깔 - 끔하게 mixin을 사용하는 것과 같은 효과를 볼 수 있었습니다. 다만 제가 현재하는 프로젝트에서 아직 Composition API가 도입되지 않은 컴포넌트들이 많아서.. 부분적으로 바꿔 놓으니까 약간 코드가 지저분해 보이네요 ㅠ
[결론] Mixin을 쓸까? Compositoin API를 쓸까?
- Vue 2 프로젝트를 사용하는 중이라면?
Mixin을 사용하는 것이 더 자연스럽고, 이전 작업이 필요 없습니다😉 - Vue 3 프로젝트사용하거나 새 프로젝트를 만들 것이라면?
Composition API를 사용하는 것을 더 추천합니다. 더 명확하고 구조화된 코드 관리를 할 수 있습니다! (게다가 타입스크립트도 지원한다구요 ㅎㅎ ) - 프로젝트가 크고 복잡하다면?
크고 복잡한 프로젝트의 경우엔 Composition API를 사용하는 것이 코드의 관리와 유지보수 측면에서 더 낫습니다.
'지금, 개발하기 > Vue' 카테고리의 다른 글
[Vue.js] 반응성(reactivity)를 가진 상태 데이터 만들기 (0) | 2024.04.19 |
---|---|
[Vue.js] Immediate Watch를 이용한 (1) | 2024.02.15 |
암호화 (0) | 2023.05.16 |
Vue3] data-grid _ tabulator 테마 변경 (0) | 2023.01.11 |
Vue] vue.js 데이터 시각화 라이브러리 for 리포트 (0) | 2023.01.05 |