본문 바로가기
지금, 개발하기/Vue

[Vue.js] Vue의 코드 재사용 방법 - mixin vs Compositon API

by Seaco :) 2024. 1. 28.

🚨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)  시간 관련 데이터, 함수 재정의

  1. 오늘: today(yyyy-mm-dd)
  2. 어제: yesterday (yyyy-mm-dd)
  3. 당월의 첫날: firstDayOfMonth(yyyy-mm-dd)
  4. 당월의 마지막날: lastDayOfMonth(yyyy-mm-dd)
  5. 오늘로부터 일주일전 날짜: oneWeekAgo(yyyy-mm-dd)
  6. 현재날짜와시간: currentDateTime (yyyy-mm-dd hh:mm:ss)
  7. 현재시간: currentHour (hh)
  8. 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를 사용하는 것이 코드의 관리와 유지보수 측면에서 더 낫습니다.