공부하는 블로그

Vue.js Composition API 리뷰 본문

자바스크립트

Vue.js Composition API 리뷰

devtimothy 2019. 12. 21. 12:28

Vue composition API 리뷰

Vue 3가 출시를 앞두고 있다. Vue-next에서 공개한 주요 변경점으로는 Typescript 지원 강화, Composition API, 컴파일러 개선 등이 있겠다. 기존 Vue 2 버젼에서 업그레이드 시에 어떤 대격변이 일어날까 걱정을 많이 했는데, Vue composition API의 경우에는 2 버젼에서 사용할 수 있도록 배포가 되었다.

설치

npm install @vue/composition-api --save 로 설치를 한다.

그리고 main.js에 아래 코드를 추가한다.

import Vue from 'vue';
import App from './App.vue';
import VueCompositionApi from '@vue/composition-api';

Vue.use(VueCompositionApi);
Vue.config.productionTip = false;

new Vue({
  render: h => h(App)
}).$mount('#app');

기존 프로젝트에서 리팩토링

필자는 기존에 쓰던 토이 프로젝트에 composition api를 적용해보았다.

아래는 data를 선언해놓은 코드이다. 이를 변경해보자.

data() {
    return {
      time: moment().locale('ko').format('k:mm'),
      showModal: false,
      selected: null,
      today: moment().locale('ko').format('LL'),
      input: { content: "", time: "", me: false },
      user: {
        imageSrc:
          "http://www.pororopark.com/images/sub/circle_pororo.png",
        name: "뽀로로"
      },
      messages: [
        { content: "친구야 안녕!", time: moment().locale('ko').format('A h:mm'), me: false }
      ]
    };
  }

데이터를 옮기는 건 간단하다. 코드 내에서 setup을 선언 후 아래와 같이 변경해준다.

import { ref } from "@vue/composition-api";

export default {
    ...
  setup() {
        const time = ref(
      moment()
        .locale("ko")
        .format("k:mm")
    );
    let showModal = ref(false);
    let selected = ref(null);
    const today = ref(
      moment()
        .locale("ko")
        .format("LL")
    );
    let input = ref({ content: "", time: "", me: false });
    const user = ref({
      imageSrc: "http://www.pororopark.com/images/sub/circle_pororo.png",
      name: "뽀로로"
    });
    const messages = ref([
      {
        content: "친구야 안녕!",
        time: moment()
          .locale("ko")
          .format("A h:mm"),
        me: false
      }
    ]);
  }
}

여기서 ref는 변수를 반응형으로 감싸는 역할을 하는 것 같다.

그렇게 어렵지 않다.

다음은 methods 부분이다.

methods: {
    add(event) {
      if (!this.input.content) return;
      if (!this.input.time) this.input.time = moment().locale('ko').format('A h:mm');
      this.messages.push(this.input);
      this.initInput();
      this.showModal = false;
    },
    addOtherTalk() {
      this.input.me = false;
      this.add();
    },
    addMyTalk() {
      this.input.me = true;
      this.add();
    },
    remove(event) {
      if (window.confirm("정말 제거하시겠습니까?")) {
        this.messages.pop();
        this.initInput();
      }
    },
    initInput() {
      this.input = { content: "", time: "", me: false };
    },
    select(index) {
      console.log("select");
      this.selected = index;
    },
    unselect() {
      console.log("unselect");
      this.selected = null;
    },
    editable(index) {
      return this.selected === index;
    },
    async download() {
      this.showModal = false
      // const el = this.$refs.printMe;
      const el = document.body;
      // add option type to get the image version
      // if not provided the promise will return
      // the canvas.
      const options = {
        type: "dataURL",
        useCORS: true
      };
      const MIME_TYPE = "image/png";
      // const imgURL = canvasElement.toDataURL(MIME_TYPE);
      const imgURL = await this.$html2canvas(el, options);
      const dlLink = document.createElement("a");

      dlLink.download = "capture";
      dlLink.href = imgURL;
      dlLink.dataset.downloadurl = [
        MIME_TYPE,
        dlLink.download,
        dlLink.href
      ].join(":");

      document.body.appendChild(dlLink);
      dlLink.click();
      document.body.removeChild(dlLink);
    }
  }
setup() {
  // ...데이터 선언부

  const add = () => {
      if (!input.value.content) return;
      if (!input.value.time)
        input.value.time = moment()
          .locale("ko")
          .format("A h:mm");
      messages.value.push({ ...input.value });
      initInput();
      showModal.value = false;
    };
  const addOtherTalk = () => {
    input.value.me = false;
    add();
  };
  const addMyTalk = () => {
    input.value.me = true;
    add();
  };
  const remove = () => {
    if (window.confirm("정말 제거하시겠습니까?")) {
      messages.value.pop();
      initInput();
    }
  };
  const initInput = () => {
    input.value = { content: "", time: "", me: false };
  };
  const select = index => {
    console.log("select");
    selected.value = index;
  };
  const unselect = () => {
    console.log("unselect");
    selected.value = null;
  };
  const editable = index => {
    return selected.value === index;
  };
  const download = async () => {
    showModal.vaue = false;
    // const el = $refs.printMe;
    const el = document.body;
    // add option type to get the image version
    // if not provided the promise will return
    // the canvas.
    const options = {
      type: "dataURL",
      useCORS: true
    };
    const MIME_TYPE = "image/png";
    // const imgURL = canvasElement.toDataURL(MIME_TYPE);
    const imgURL = await this.$html2canvas(el, options);
    const dlLink = document.createElement("a");

    dlLink.download = "capture";
    dlLink.href = imgURL;
    dlLink.dataset.downloadurl = [
      MIME_TYPE,
      dlLink.download,
      dlLink.href
    ].join(":");

    document.body.appendChild(dlLink);
    dlLink.click();
    document.body.removeChild(dlLink);
  };

  return {
    time,
    showModal,
    selected,
    today,
    input,
    user,
    messages,
    add,
    addOtherTalk,
    addMyTalk,
    remove,
    initInput,
    select,
    unselect,
    editable,
    download
  };
}

주의할 것은 ref로 감싼 데이터를 변경해 줄 때, this가 빠지는 대신 뒤에 value가 붙는다는 점이다.

setup() 에서 return 해주는 값은 해당 컴포넌트에서 사용되는 변수와 method를 리턴해준다.

그런데 아쉬운 점은... 너무 코드 가독성이 엉망이라는 생각이 든다. 그래서 Composition API에서는 변수와 변수를 사용하는 메소드들을 묶어서 쓰기를 권장하는 듯 하다.

아래를 보자.

// compositions/selected.js
import { ref } from '@vue/composition-api';

export const useSelected = () => {
  let selected = ref(null);
  const select = index => {
    console.log('select');
    selected.value = index;
  };
  const unselect = () => {
    console.log('unselect');
    selected.value = null;
  };
  const editable = index => {
    return selected.value === index;
  };

  return {
    selected,
    select,
    unselect,
    editable
  };
};
// App.vue
...
setup() {
    const { selected, select, unselect, editable } = useSelected();

    return {
        ...
      selected,
      select,
      unselect,
      editable,
      // 혹은 destructuring을 사용할 수도 있다.
      ...useSelected()
    };
}

React의 hook 냄새가 많이 나기는 하지만, 위와 같이 코드 관리를 할 수 있고, 읽기에는 한결 편해졌다. 메소드를 확인하려면 한 depth를 들어가야 하지만, 가독성이 나쁜 것보다는 훨씬 나은 듯 하다.

다른 변수 & 메소드들도 한 곳에 같이 묶어주는 작업을 했다.

// compositions/input.js
import { ref } from '@vue/composition-api';
import moment from 'moment';

export const useInput = () => {
  let input = ref({ content: '', time: '', me: false });
  const messages = ref([
    {
      content: '친구야 안녕!',
      time: moment()
        .locale('ko')
        .format('A h:mm'),
      me: false
    }
  ]);

  const add = () => {
    if (!input.value.content) return;
    if (!input.value.time)
      input.value.time = moment()
        .locale('ko')
        .format('A h:mm');
    messages.value.push({ ...input.value });
    initInput();
    // showModal.value = false;
  };

  const addOtherTalk = () => {
    input.value.me = false;
    add();
  };

  const addMyTalk = () => {
    input.value.me = true;
    add();
  };

  const initInput = () => {
    input.value = { content: '', time: '', me: false };
  };

  const remove = () => {
    if (window.confirm('정말 제거하시겠습니까?')) {
      messages.value.pop();
      initInput();
    }
  };

  return {
    input,
    messages,
    add,
    addOtherTalk,
    addMyTalk,
    initInput,
    remove
  };
};

add() 메소드를 보면 showModal.value = false; 를 주석처리를 해놓았다. 이는 기존에 작업했던 코드에 의존하고 있던 내용을 풀어 결합도를 낮춰주게 되었다. composition api를 구성하면서 관심사의 분리를 자연스럽게 이뤄낸 (?) 것 같다.

한편으로는 토이 프로젝트이기에 간단하게 해결을 했지만, 프로젝트가 복잡한 로직들로 꼬여있다면 이것을 적용하는 데도 많이 애먹게 되지 않을까? 라는 생각이 들기도 한다.

아무튼 아래와 같은 방법으로 showModal 변수와 message 변수 중간에 메서드를 하나 둠으로서 문제를 해결하였다.

const {
    ... (생략)
    add
} = useInput();

const addMessageFromModal = () => {
  add();
  showModal.value = false;
};

코드도 잘 돈다.

이렇게 composition api를 한번 맛보았는데, 코드를 역할에 맞게 관리한다는 점은 좋은 것 같다. 객체지향에서도 SRP 원칙을 지키듯이 use... 로 씀으로서 역할을 분명하게 할 수 있다는 점은 좋은 것 같다.

너무 간단한 프로젝트를 리팩토링하게 되었는데, 과연 실무에서는 어떤 변수가 있을지는 아직 잘 모르겠다. 이상으로 포스팅을 마친다.

'자바스크립트' 카테고리의 다른 글

First look of Svelte App  (0) 2020.02.03
FP 비교  (2) 2020.01.03
Vue.js의 새롭게 개선된 prop.sync  (0) 2019.05.10
console.log에 대해 알아보자  (2) 2018.10.23
JavaScript essentials: 엔진 동작을 알아야 하는 이유  (11) 2018.10.04
Comments