본문으로 바로가기

Reusability & Compostion

category JavaScript/Vue.js 2021. 7. 19. 11:32

Composition API

 - 컴포넌트 내에서 사용하는 특정 기능을 갖는 코드를 유연하게 구성하여 사용할 수 있도록 Vue 3 버전에 추가된 함수 기반의 API이다.

 

 - 그동안 Vue는 '프로젝트 규모가 커질수록 관리하기 힘들다'는 단점이 있었다. data, computed, watch, methods 등 프로젝트 규모가 커질수록, 컴포넌트의 계층 구조가 복잡할수록 코드에 대한 추적 및 관리가 어려웠다.

 - 하지만 컴포지션 API를 이용하면 Setup이라는 메소드 안에서 한 덩어리로 코드를 구현할 수 있어서 코드에 대한 관리가 훨씬 쉬워지게 된다.

 - 즉, 컴포지션 API는 그동안 Vue가 가지고 있던 단점을 보완하기 위해서 추가된 Vue 3 버전의 핵심 기능이다.

 

Setup

 - Setup은 컴포지션 API를 구현하는 곳이다.

 - 예제로 사용자로부터 숫자 2개를 입력받고, 입력받은 숫자를 더한 값을 출력하는 코드를 작성하겠다.

 - 컴포지션 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>

 - 사용자로부터 숫자가 입력되는 이벤트(keyup)이 발생할 때마다 plusNumbers 함수를 호출해서 사용자가 입력한 값을 더하여 result로 반환하도록 코드가 작성되었다.

 

 - 컴포지션 API 기능을 이용해서 동일한 기능을 갖는 코드를 작성하면 다음과 같다.

// src/views/CompositionAPI.vue
<template>
  <div>
    <h2>Calculator</h2>
    <div>
      <input type="text" v-model="state.num1" @keyup="plusNumbers" >
      <span> + </span>
      <input type="text" v-model="state.num1" @keyup="plusNumbers" >
      <span> = </span>
      <span>{{ state.result }}</span>
    </div>
  </div>
</template>
<script>
  import { reactive } from 'vue'	// reactive 추가
  export default {
    name: 'calculator',
    setup() {
      let state = reactive({	// reactive를 이용해서 num1, num2, result를
        num1: 0,		// 실시간 변경사항에 대한 반응형 적용
        num2: 0,
        result: 0
      });
      function plusNumbers() {
        state.result = parseInt(state.num1) + parseInt(state.num2)
      }
      return {		// reactive로 선언된 state와 plusNumbers 함수를 반한함으로써
        state,		// 기존 data, methods 옵션처럼 사용이 가능해짐
        plusNumbers
      }
    }
  }
</script>

 - 컴포지션 API의 reactive를 이용해서 코드를 작성했다. 지금 작성된 코드는 컴포지션 API를 이용하지 않은 코드와 크게 다를 바가 없어보인다. 여기서 코드를 좀 더 수정해보자

// src/views/CompositionAPI2.vue
<template>
  <div>
    <h2>Calculator</h2>
    <div>
      <input type="text" v-model="state.num1" >
      <span> + </span>
      <input type="text" v-model="state.num1" >
      <span> = </span>
      <span>{{ state.result }}</span>
    </div>
  </div>
</template>
<script>
  import { reactive, computed } from 'vue'	// computed 추가
  export default {
    name: 'calculator',
    setup() {
      let state = reactive({
        num1: 0,
        num2: 0,
        result: computed(() => parseInt(state.num1) + parseInt(state.num2))
        	// computed를 이용해서 num1, num2가 변경이 일어나면 즉시 result로 더한 값을 반환
      });
      return {
        state
      }
    }
  }
</script>

 - reactive와 computed를 이용하니까 input type='text'에 바인딩했던 keyup 이벤트를 없앨 수 있고, 코드가 훨씬 간결해졌다.

 - 지금 작성한 코드는 컴포넌트 내에서만 사용 가능하다.

 

 - 현재 컴포넌트 내에서만 사용하는 코드를 작성하는 경우도 있지만, 계산기에서 덧셈 연산을 여러 번 반복해서 사용할 수 있는 것처럼 재사용 가능한 코드를 사용하는 경우가 있다. 이러한 경우, 작성한 코드를 여러 컴포넌트에서 재사용할 수 있도록 함수를 분리해야 한다.

 

 - 일단 Setup에 작성된 코드를 분리해서 별도의 function으로 작성해보자

// src/views/CompositionAPI3.vue
<template>
  <div>
    <h2>Calculator</h2>
    <div>
      <input type="text" v-model="num1" >
      <span> + </span>
      <input type="text" v-model="num1" >
      <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가 외부 function에서
    				// 정상적으로 동작하기 위해서는 toRefs를 사용해야 함
  }
  export default {
    name: 'calculator',
    setup() {
      let { num1, num2, result } = plusCalculator()	// 외부 function
      return {
        num1, 
        num2, 
        result
      }
    }
  }
</script>

 - 외부 function에서 반응형 변수를 사용하기 위해서 toRefs가 추가되었다.

 

 - 컴포넌트 안에서는 v-model 디렉티브를 통해 바인딩된 변수가 사용자의 입력값이 바뀔 때마다 반응형으로 처리가 되었지만, 함수를 컴포넌트 밖으로 뺴면 사용자가 입력한 값에 대한 반응형 처리가 불가능해진다. 그래서 toRefs를 사용하여 컴포넌트 밖에서도 반응형 처리가 가능하도록 할 수 있다.

 

 - 컴포넌트 내에서 정의된 코드를 다른 컴포넌트에서도 사용할 수 있도록 컴포넌트 밖으로 분리해보자.

 - Common.js 파일을 생성하고 앞서 구현한 plusCalculator 코드를 다음과 같이 작성한다.

// src/common.js
import {
  reactive,
  computed,
  toRefs
} from 'vue'

const plusCalculator = () => {
  let state = reactive({
    num1: 0,
    num2: 0,
    result: computed(() => parseInt(state.num1) + parseInt(state.num2))
  })
  return toRefs(state)
}

export {
  plusCalculator
}

 - 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()
      return {
        num1,
        num2,
        result
      }
    }
  }
</script>

 - 이렇게 특정 기능을 갖는 함수를 컴포지션 API를 이용하고 개발해서 공통 스크립트로 제공하면 뷰 컴포넌트 내에서 반응형으로 처리를 할 수 있어서 매우 활용도가 높아지게 된다.

 

Lifecycle Hooks

 - 컴포지션 API 내에서 사용할 수 있는 컴포넌트 라이프사이클 훅은 다음 표와 같다.

Options API Hook inside setup()
beforeCreate  
created  
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

 - 컴포지션 API에서 setup()은 컴포넌트 라이프사이클의 beforeCreate와 created 훅 사이에서 실행되기 때문에, onBeforeCreate, onCreated 훅은 필요가 없고, setup() 안에서 코드를 작성하면 된다.

 

 - 다음은 setup() 에서 onMounted 훅을 적용한 코드이다.

export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

 

Provide/Inject

 - Composition API에서 Provide/Inject를 사용하려면 provide와 inject를 별도로 import 해야 사용할 수 있다.

 - 부모 컴포넌트에서는 provide 함수를 통해서 전달할 값에 대한 키(key), 값(value)을 설정한다.

// src/views/CompositionAPIProvide/vue
<template>
  <CompositionAPIIject />
</template>
<script>
  import { provide } from 'vue'	// provide 추가
  import CompositionAPIIject from './CompositionAPIIject'
  
  export default {
    components: {
      CompositionAPIIject
    },
    setup() {
      provide('title', 'Vue.js 공부하기')
      // provide 함수를 통해서 전달할 키(key), 값(value) 설정
    }
  }
</script>

 - 자식 컴포넌트에서는 inject를 이용해서 부모 컴포넌트에서 정의한 provide 키로 데이터를 가져올 수 있다.

// src/views/CompositionAPIIject.vue
<template>
  <h1>{{ title }}</h1>
</template>
<script>
  import { inject } from 'vue'	// inject 추가
  
  export default {
    setup() {
      const title = inject('title')
      // inject를 사용해서 provide에서 정의한 키(key)로 데이터를 전달받음
      return {title}
    }
  }
</script>

 

믹스인 (Mixins)

- Vue에서 공통모듈에 해당하는 파일을 만들어서 사용할 수 있는 방법 중 하나

 - 믹스인은 이름에서도 알 수 있듯이 믹스(mix)-인(in), 믹스인 파일을 컴포넌트 안에(in) 삽입해서, 합쳐서(mix) 사용하는 것이다.

 - 일반적인 언어의 공통모듈처럼 메소드를 정의해서 사용할 수도 있고, 이외에도 Vue의 라이프사이클 훅까지 사용할 수 있다.

 - 이벤트 훅까지 사용할 수 있다는 것은 굉장히 큰 장점으로 작용한다.

 

 - 믹스인은 기능을 따로 구현하고, 필요할 때마다 믹스인 파일을 컴포넌트에 결합해서 사용하는 방법을 말한다.

 

 - 믹스인은 여러 컴포넌트에 동일한 로직을 사용할 필요가 있을 때 매우 유용하다.

 - 특정 기능을 캡슐화하여 단순히 코드의 수가 줄어들고 재사용성이 늘어나는 것 뿐만 아니라 애플리케이션 운영 시에도 큰 이점을 가지게 된다. 로직 변경이 일어났을 때 믹스인 파일만 수정하면 참조하고 있는 모든 컴포넌트에 반영되기 때문이다.

 

믹스인 파일 생성

 - axios 패키지를 이용해서 서버와의 데이터 통신을 위한 공통함수를 작성했다

// src/api.js
import axios from 'axios'

export default {
  methods: {
    async $callAPI(url, method, data) {
      return (swait axios({
        method: method,
        url,
        data
      }).catch(e => {
        console.log(e)
      })).data
    }
  }
}

 - 함수 이름은 $callAPI 라고 작성이 되었다. 함수 이름에 $라는 prefix를 사용하는 이유는 믹스인 파일을 사용하는 컴포넌트 내에 동일한 메소드명이 있어서 오버라이딩 되는 것을 방지하기 위해서이다.

 - 일반적으로 컴포넌트에 정의되는 메소드명에는 $와 같은 prefix를 사용하지 않기 때문에 믹스인 파일의 메소드명을 이렇게 작성하면 컴포넌트의 메소드명과 구분할 수 있다.

 

컴포넌트에서 믹스인(Mixins) 사용

 - 다음과 같이 mixins 프로퍼티에 사용할 믹스인 파일을 정의해서 사용하면 된다.

// src/views/Mixins.vue
<script>
  import ApiMixin from '../api.js'
  export default {
    mixins: [ApiMixin],	// 사용할 믹스인 파일을 배열로 등록
    data() {
      return {
        productList: []
      }
    },
    async mounted() {
      this.productList = await this.$callAPI("https://ada1e106-f1b6-4ff2-be04-
      e311ecba599d.mock.pstmn.io/list", "get")
      console.log(this.productList)
    }
  }
</script>

 - 믹스인은 이렇게 메소드를 정의해서 컴포넌트에서 사용할 수 있게 해준다. 이외에도 믹스인은 컴포넌트에서 일어나는 이벤트 훅을 그대로 이용할 수 있다는 큰 이점을 가지고 있다.

 

믹스인(mixins)에서 라이프사이클 훅 이용하기

 - 애플리케이션을 이용하는 사용자가 방문한 페이지 및 페이지에 머문 시간을 기록하는 코드를 작성한다고 가정해보자. 믹스인에 사용자가 특정 페이지에 방문하고 빠져나갈 때 데이터베이스에 시간을 저장하는 메소드를 만들었다.

 

 - 각 컴포넌트에서는 mounted 훅이 발생할 때 믹스인의 방문 시작 메소드를 호출하고, unmounted 훅이 발생할 때 믹스인의 방문 종료 메소드를 호출해서 데이터베이스에 방문 시작 시간과 방문 종료 시간을 기록하여 페이지에 머문 시간을 계산할 수 있다.

 

 - mounted, unmounted마다 모든 컴포넌트에서 믹스인에 메소드를 호출하는 것은 간편하긴 하지만, 어찌 보면 굉장히 반복적이고 불편한 작업이 될 것이다. 만약 개발자의 실수로 특정 컴포넌트에 해당 코드를 작성하지 않으면 코드가 작성되지 않은 컴포넌트의 페이지 방문 이력을 기록할 수 없게 된다.

 

 - 믹스인에서는 단순히 메소드만 정의해서 사용하는 것이 아니라, 컴포넌트의 라이프사이클 훅을 그대로 이용할 수 있다. 즉, 믹스인 파일에 mounted, unmounted마다 데이터베이스에 방문 시작 시간과 방문 종료 시간을 기록하는 코드를 작성하면, 해당 믹스인 파일을 사용하는 모든 컴포넌트에서는 자동으로 컴포넌트가 mounted, unmounted 될 때 데이터베이스에 방문 기록을 저장할 수 있게 된다.

 

 - 실제 믹스인 파일의 mounted, unmounted 훅에 작성된 코드가 컴포넌트 안에서 어느 시점에 실행되는지 다음 코드를 통해 확인해보자

// mixin.js
mounted() {
  console.log('믹스인 mounted')
},
unmounted() {
  console.log('믹스인 unmounted')
}
// component.vue
mixins: [mixin],
mounted() {
  console.log('컴포넌트 mounted')
  // 믹스인 mounted
  // 컴포넌트 mounted
},
unmounted() {
  console.log('컴포넌트 unmounted')
  // 믹스인 unmounted
  // 컴포넌트 unmounted
}

 - 이 코드를 실행하면 컴포넌트가 mounted 되는 시점에 믹스인에 있는 mounted 코드가 먼저 실행되고, 그 다음 컴포넌트의 mounted 코드가 실행된다. 즉, 컴포넌트의 라이프사이클 훅 시점에 동일한 믹스인 라이프사이클 훅 코드가 먼저 실행된다. 2개의 파일이 같은 프로퍼티, 같은 라이프사이클 훅끼리 코드가 합쳐지는데, 믹스인 코드가 먼저 실행된다.

 

믹스인 파일 전역으로 등록하기: main.js에 등록

 - api를 호출하는 기능은 애플리케이션 내에 거의 모든 컴포넌트에서 사용하는 기능이므로 전역으로 등록해서 각 컴포넌트에서 별도의 mixins 추가 없이 사용할 수 있게 해보자

 

 - 다음과 같이 mixins.js 파일을 생성한다.

// 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
    }
  }
}

 - mixins.js 파일을 전역으로 등록하기 위해서 main.js에 다음과 같이 추가한다.

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import mixins from './mixins'	// mixins import

const app = createApp(App);
app.use(router);
app.mixin(mixins);	// app에 mixin 추가
app.mount('#app');

 

Custom Directives

 - Vue에서는 v-mode, v-show 디렉티브같은 기본 디렉티브 외에도 사용자가 직접 디렉티브를 정의해서 사용할 수 있다.

 - 웹 사이트 방문 시 로그인 페이지에 접속하면 페이지가 열림과 동시에 사용자 ID를 입력하는 필등 마우스 포커스가 위치해 있는 것을 빈번하게 보았을 것이다. 사용자가 컴포넌트에 접속했을 때 지정된 입력 필드로 포커스를 위치시킬 수 있는 커스텀 디렉티브를 만들어 보자

 - 참고로 커스텀 디렉티브를 전역에서 사용할 수 있도록 등록이 가능하고, 특정 컴포넌트 안에서만 사용하도록 등록도 가능하다.

 

 - main.js에 커스텀 디렉티브를 다음과 같이 추가한다.

const app = createApp(App);
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

 - 코드를 보면 컴포넌트가 mounted되면 v-focus 디렉티브를 적용한 HTML 객체로 포커스(el.focus())를 위치시키도록 작성되었다.

 - 컴포넌트에서는 다음과 같이 v-focus 디렉티브를 사용하면 v-focus 디렉티브가 정의된 HTML 객체에 마우스 포커스가 위치하게 된다.

<input type="text" v-focus >

 - 실제로 지금 사용한 커스텀 디렉티브는 Vue 애플리케이션 개발 시 main.js에 전역으로 등록해서 많이 사용한다.

 

 - 다음은 전역에 등록하는 방법이 아닌, 컴포넌트 내에 등록해서 사용하는 방법을 알아보자

 - 다음과 같이 directives 옵션에 정의하면 된다.

// src/views/CustomDirective.vue
directives: {
  focus: {
    mounted(el) {
      el.focus()
    }
  }
}

 - 커스텀 디렉티브 사용 시에도 데이터 바인딩 처리가 가능하다.

 - 다음 코드는 v-pin 디렉티브에 데이터 옵션의 position을 바인딩 했다. 컴포넌트가 mounted되면 v-pin 디렉티브가 지징된 HTML 객체의 position을 top:50px, left:100px;로 고정시킨다.

<div style="height: 1000px;">
  <p v-pin="position">페이지 고정 영역(position:fixed;top:50x,left:100px;)</p>
</div>
directives: {
  pin: {
    mounted(el, binding) {
      el.style.position = 'fixed';
      el.style.top = binding.value.top + 'px'
      el.style.left = binding.value.left + 'px'
    }
  }
},
data() {
  return {
    position: {
      top: 50,
      left: 100
    }
  }
}

 - 애플리케이션에서 필요한 커스텀 디렉티브를 잘 정의해서 사용한다면 애플리케이션 개발 생산성을 향상시킬 수 있다.

 

Plugins

 - 플러그인은 특정 기능을 제공하는 코드이다.

 - 이미 Vue 프로젝트를 진행할 때 유용한 플러그인들을 설치하고 사용하고 있다. NPM을 통해 설치되는 패키지 역시 플러그인이다.

 

 - 플러그인은 때로는 모듈로, 때로는 패키지로 사용될 수 있다. 플러그인은 특정 기능을 제공하고 쉽게 설치해서 사용할 수 있다.

 - 프로젝트를 진행하면서 필요한 대부분의 플러그인은 이미 전세계 개발자들 중 누군가가 개발해서 NPM에 등록했을 것이고, 우리들은 NPM을 통해 쉽게 설치해서 사용할 수 있다. 하지만 대규모 프로젝트를 진행하다보면 해당 프로젝트에 맞게 특화된 플러그인을 제작해야하는 상황이 생길 수 있다. Vue에서는 직접 플러그인을 제작해서 전역으로 사용할 수 있게 해준다.

 

 - 다국어(i18n)를 처리해주는 플러그인을 제작해보자. scr폴더 밑에 plugins 폴더를 만들고 다음과 같이 i18n.js 파일을 생성하자

// 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 키로 다국어 데이터 전달
  }
}

 - 플러그인은 install 옵션에서 정의해서 사용할 수 있다. app.config.globalProperties를 선언하여 컴포넌트에서 $translate로 바로 접근해서 사용할 수 있다.

 - 또한 provide로 다국어 데이터를 전달해서 컴포넌트에서는 inject를 이용해서도 사용 가능하다. 다국어 플러그인은 전역에서 사용해야 하므로 main.js 파일을 열어서 다국어 플러그인을 사용할 수 있도록 추가해야 한다.

// src/main.js
import i18nPlugin from './plugins/i18n'	// i18n 플러그인 추가
const i18nStrings = {
  en: {
    hi: 'Hello!'
  },
  ko: {
    hi: '안녕하세요!'
  }
}

const app = createApp(App)
app.use(i18nPlugin, i18nStrings)	// i18n 플러그인에 다국어 번역 데이터를 파라미터로 전달
app.mount('#app')

 - i18nStrings 변수를 선언해서 다국어 번역이 필요한 내용을 정의한 후 i18nPlugin으로 전달한다.

 - 이제 모든 컴포넌트에서 다국어 플러그인을 사용할 수 있다. 컴포넌트에서 사용하는 방법은 다음과 같다.

// src/views/Plugins.vue
<template>
  <div>
    <h2>{{ $translate("ko.hi") }}</h2>	<!-- $translate로 사용 -->
    <h2>{{ i18n.ko.hi }}</h2>	<!-- inject로 사용 -->
  </div>
</template>
<script>
  export default {
    inject: ['i18n'],	// provide로 전달된 i18n을 inject로 사용할 수 있음
    mounted() {
      console.log(this.i18n)
    }
  }
</script>

 

반응형

'JavaScript > Vue.js' 카테고리의 다른 글

믹스인(Mixins)과 스토어(Store) 생각 정리  (0) 2021.08.02
Vuex (v4.x)  (0) 2021.07.19
Vue Router  (0) 2021.07.16
컴포넌트 심화 학습 (Provide/Inject, Template refs)  (0) 2021.07.16
컴포넌트 심화 학습 (Slot)  (0) 2021.07.16