09 Reusability & Composition

vencott·2022년 2월 21일
0

9.1 Composition API

9.1.1 Composition API란?

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

Vue는 프로젝트 규모가 커질수록 관리하기 힘들었다

data, computed, watch, methods 등 컴포넌트의 계층구조가 복잡할수록 코드에 대한 추적 및 관리가 힘들었다

컴포지션 APIsetup 메소드 안에서 한 덩어리로 연관성 있는 로직을 코드로 구현할 수 있어 코드 관리가 쉬워진다

  • 일반적인 API의 특성처럼, 특정 기능을 갖는 함수를 정의하고 API 처럼 사용할 수 있게 해 코드의 재활용성과 가독성을 높이는 것이 목적이다
  • Vue 2 에선 mixin을 활용했으나 오버라이딩, 다중 믹스인 등의 문제로 관리가 어려웠다

9.1.2 setup

setup은 컴포지션 API를 구현하는 곳이다

기존 개발 방법과 컴포지션 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>

컴포지션 API > src/views/CompositionAPI.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>
import { reactive } from "vue";
export default {
  name: "calculator",
  setup() {
    let state = reactive({
      num1: 0,
      num2: 0,
      result: 0,
    });
    function plusNumbers() {
      state.result = parseInt(state.num1) + parseInt(state.num2);
    }
    return {
      state,
      plusNumbers,
    };
  },
};
</script>
  • setup() 메소드에서 반환된 값들은 data 옵션과 동일하게 사용이 가능하다
  • 컴포지션 API의 reactive를 이용해서 코드를 작성했으나, 이전과 크게 다를 바가 없다

src/views/CompositionAPI2.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 { reactive, computed } from "vue"; // computed 추가
export default {
  name: "calculator",
  setup() {
    let state = reactive({
      num1: 0,
      num2: 0,
      // computed를 이용해서 num1, num2가 변경이 일어나면 즉시 result로 더한 값을 반환
      result: computed(() => parseInt(state.num1) + parseInt(state.num2)),
    });
    return {
      state,
    };
  },
};
</script>
  • computed를 reactive와 함께 사용하면 코드가 더 간결해진다
  • 하지만 위 코드는 현재 컴포넌트에서만 사용 가능하다

작성한 코드를 여러 컴포넌트에서 재사용할 수 있도록 함수를 분리해본다

src/views/CompositionAPI3.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 { 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가 외부에서 정상적으로 동작하기 위해선 toRefs를 사용해야 함
}

export default {
  name: "calculator",
  setup() {
    let { num1, num2, result } = plusCalculator(); // 외부 function
    return {
      num1,
      num2,
      result,
    };
  },
};
</script>
  • 외부 함수에서 반응형 변수를 사용하기 위해 toRefs를 사용했다
  • 컴포넌트 내부에선 v-model 디렉티브를 통해 바인딩된 변수가 사용자의 입력값에 따라 반응형으로 처리가 되었지만, plusCalculator 함수를 밖으로 빼면 반응형 처리가 불가능해 toRefs를 사용하여 컴포넌트 밖에서도 반응형 처리가 가능하도록 할 수 있다

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(); // 외부 function
    return {
      num1,
      num2,
      result,
    };
  },
};
</script>
  • 이렇듯 특정 기능을 갖는 함수를 컴포지션 API를 이용해서 개발하여 공통 스크립트로 제공하면 뷰 컴포넌트 내에서 반응형으로 처리를 할 수 있어 매우 활용도가 높아진다

9.1.3 Lifecycle Hooks

컴포지션 API의 setup()beforeCreatecreate 훅 사이에서 실행되기 때문에 onBeforeCreate, onCreate는 필요가 없고, 나머지 라이프사이클 훅 메소드의 앞에 on을 붙여 사용한다

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

9.1.4 Provide/Inject

컴포지션 API에서 Provide/Inject를 사용하려면 각각 별도로 import를 해야한다

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

src/views/CompositionAPIProvide.vue

<template>
  <CompositionAPIInject />
</template>

<script>
import { provide } from "vue";
import CompositionAPIInject from "./CompositionAPIInject";

export default {
  components: {
    CompositionAPIInject,
  },
  setup() {
    provide("title", "Vue.js 프로젝트");
  },
};
</script>

src/views/CompositionAPIInject.vue

<template>
  <h1>{{ title }}</h1>
</template>

<script>
import { inject } from "vue";

export default {
  setup() {
    const title = inject("title");
    return { title };
  },
};
</script>

9.2 믹스인(mixins)

💡 Vue에서의 공통 모듈

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

믹스인(mixin)은 기능을 따로 구현하고, 필요시 믹스인 파일을 컴포넌트에 결합해서 사용한다

예를 들어, 애플리케이션의 모든 컴포넌트에서 사용자가 접근할 때 마다 접근 권한이 있는지를 체크한다고 가정해 보면,

  • 컴포넌트 생성되는 시점에(beforeCreate) 사용자의 권한을 체크하는 로직을 다 넣는다고 생각하면 중복된 코드가 양산될 것이다
  • 믹스인을 이용해서 사용자 권한을 체크하는 로직을 구현하고 각각의 컴포넌트에서는 해당 믹스인 파일을 추가만 하면 된다
  • 이렇듯 특정 기능을 캡슐화해 단순히 코드의 수가 줄어들고 재사용성이 늘어나는 것 뿐 아니라 애플리케이션 운영 관점에서도 큰 이점을 가지게 된다
  • 사용자 권한 체크 로직이 변경되었을 때 믹스인 파일만 수정하면 참조하고 있는 모든 컴포넌트에 반영되기 때문이다

axios를 이용해서 서버 데이터를 호출했던 메소드를 믹스인으로 만들어 보자

9.2.1 믹스인(mixins) 파일 생성

src 폴더에 api.js 파일을 생성하고 axios 패키지를 이용해서 서버와의 데이터 통신을 위한 공통 함수를 작성한다

src/api.js

import axios from "axios";

export default {
  methods: {
    async $callAPI(url, method, data) {
      return await axios({
        method: method,
        url,
        data,
      }).catch((e) => {
        console.log(e);
      }).data;
    },
  },
};
  • $callAPI() : $ prefix는 믹스인 파일을 사용하는 컴포넌트 내에 동일한 메소드명이 있어서 오버라이딩 되는 것을 방지하기 위함이다

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

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

src/views/Mixins.vue

<template>
  <div>
    <div>{{ htmlString }}</div>
    <div v-html="htmlString"></div>
  </div>
</template>

<script>
import ApiMixin from "../api.js";

export default {
  mixins: [ApiMixin],
  data() {
    return {
      productList: [],
    };
  },
  async mounted() {
    this.productList = await this.$callAPI(
      "https://3d5348c0-4158-4724-ad6f-352e7ef79ceb.mock.pstmn.io/list",
      "get"
    );
    console.log(this.productList);
  },
};
</script>

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

애플리케이션 사용자의 페이지 방문 시간을 기록하는 코드를 작성한다고 가정하자

  • 믹스인에 해당 메소드를 만들고, 각 컴포넌트에서 해당 믹스인 파일을 정의해 메소드를 호출할 수 있지만, 번거롭고 반복적인 작업이 될 것이다
  • 또한, 개발자의 실수로 특정 컴포넌트에 이를 추가하지 않으면, 해당 페이지에선 기록이 되지 않을 것이다

믹스인에서는 단순히 메소드만 정의해서 사용하는 것이 아닌, 컴포넌트의 라이프사이클 훅을 그대로 이용할 수 있다

즉, 믹스인 파일에 mounted, unmounted 마다 방문 시간과 종료 시간을 기록하는 코드를 작성하면, 해당 믹스인 파일을 사용하는 모든 컴포넌트에서는 자동으로 컴포넌트가 mounted, unmounted 될 때 방문 기록을 저장할 수 있다

mixins.js

mounted() {
  console.log("믹스인 mounted"); 
},
unmounted() {
  console.log("믹스인 unmounted"); 
}

Component.vue

mounted() {
  console.log("컴포넌트 mounted"); 
  // 믹스인 mounted
  // 컴포넌트 mounted
},
unmounted() {
  console.log("컴포넌트 unmounted"); 
  // 믹스인 unmounted
  // 컴포넌트 unmounted
},
  • 컴포넌트가 mounted 되는 시점에 믹스인에 있는 mounted 코드가 먼저 실행되고, 그 다음 컴포넌트의 mounted 코드가 실행된다
  • 즉, 컴포넌트 라이프사이클 훅 시점에 동일한 믹스인 라이프사이클 훅이 먼저 실행
  • 2개의 파일이 같은 프로퍼티, 같은 라이프사이클 훅끼리 코드가 합쳐지는데, 믹스인 코드가 먼저 실행이 된다

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

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

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;
    },
  },
};

src/main.js

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import mixins from "./mixins";

const app = createApp(App);
app.use(router);
app.mixin(mixins);
app.mount("#app");

9.3 Custom Directives

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

커스텀 디렉티브는 전역에서 사용하도록 등록할 수도, 특정 컴포넌트 안에서만 사용하도록 등록할 수도 있다

사용자가 컴포넌트에 접속했을 때 지정된 입력 필드로 포커스를 위치시킬 수 있는 커스템 디렉티브를 만들어 보자

src/main.js

app.directive("focus", {
  mounted(el) {
    el.focus();
  },
});
  • main.js에 다음과 같은 커스텀 디렉티브를 추가했다
  • 컴포넌트가 mounted 되면 v-focus 디렉티브를 적용한 HTML 객체로 포커스(el.focus())를 위치시키도록 작성하였다
<input type="text" v-focus />

전역이 아닌, 컴포넌트 내에 등록해서 사용할 땐, 해당 컴포넌트의 directives 속성을 이용한다

directives {
    focus: {
        mounted(el) {
            el.focus()
        }
    }
}

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

<template>
  <div>
    <div style="height: 1000px">
      <p v-pin="position">페이지 고정 영역</p>
    </div>
  </div>
</template>

<script>
export default {
  directives: {
    pin: {
      mounted(el, binding) {
        el.style.position = "fixed";
        el.style.top = binding.value.top + "px";
        el.style.left = binding.value.left + "px";
      },
    },
  },
  data: function () {
    return {
      position: { top: 50, left: 100 },
    };
  },
};
</script>
  • v-pin 디렉티브에 data 옵션의 position을 바인딩한다
  • 컴포넌트가 mounted 되면 v-pin 디렉티브가 지정된 HTML 객체의 position을 고정한다

9.4 Plugins

Vue에서는 직접 플러그인을 제작해서 전역으로 사용할 수 있게 해준다

다국어(i18n)을 처리해 주는 플러그인을 만들어 보자

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를 이용해 사용 가능하다

src/main.js

import i18nPlugin from "./plugins/i18n";

const i18nStrings = {
  en: {
    hi: "Hello!",
  },
  ko: {
    hi: "안녕하세요!",
  },
};

app.use(i18nPlugin, i18nStrings);

src/views/Plugins.vue

<template>
  <!-- $translate 으로 사용 -->
  <h2>{{ $translate("ko.hi") }}</h2>
  <!-- inject로 사용 -->
  <h2>{{ i18n.ko.hi }}</h2>
</template>

<script>
export default {
  inject: ["i18n"],
};
</script>

출처: 고승원 저, 『Vue.js 프로젝트 투입 일주일 전』, 비제이퍼블릭(2021)


profile
Backend Developer

0개의 댓글