Vue Mixin vs. HOC 그리고 Composition API

박희주·2022년 11월 16일
0

Vue에서 활용하는 Mixin, HOC와 Composition API에 대한 간단한 학습

1. Mixin

💡 Mixin은 여러 컴포넌트 간에 공통으로 사용되고 있는 로직, 기능들을 재사용하는 방법 중 하나이다. Mixin에 정의 할 수 있는 재사용 로직은 data, methods, created 등과 같은 컴포넌트의 옵션이다.

위의 말대로 Mixin은 뷰 컴포넌트에서 사용되는 공통되는 로직들을 하나로 캡슐화 시켜 여러 곳에서 사용 할 수 있게 해주는 유용한 방법이다.

Mixin의 문법은 아래와 같다.

let HelloMixins = {
  // 컴포넌트 옵션들 (data, methods, created 등...)
};

new Vue({
  mixins: [HelloMixins],
});

1-1. Mixin 적용해보기

먼저 Mixin을 활용하기 위해 Vue v2.x.x 버젼을 활용했다.

먼저 Mixin을 만든다. (Mixin의 작명 규칙은 "동사+able"의 형용사 형태로 작명한다.)

  • firstMixin.js

    let firstMixin = {
      data() {
        return {
          data: 10,
        }
      },
      created() {
        console.log("firstMixin");
      },
      methods: {
        onClick() {
          console.log("click");
          this.data += 1;
        }
      }
    }
    
    export default firstMixin;
  • secondMixin.js

    let secondMixin = {
      props: {
        message: {
          default: "Bye",
        },
      },
      created() {
        console.log("secondMixin");
      },
      methods: {
        onClick() {
          this.data += 5;
        }
      },
    };
    
    export default secondMixin;
  • thirdMixin.js

    let thirdMixin = {
      props: {
        message: {
          default: "Hello",
        },
      },
      created() {
        console.log("thirdMixin");
      },
      methods: {
        onClick() {
          this.data += 10;
        }
      },
    };
    
    export default thirdMixin;

믹스인들이 완성이 되었다면 vue 컴포넌트에 적용하면 완료

<template>
  <div id="app">
    <div id="sub" @click="onClick" @contextmenu.prevent="onRightclick">
      {{ data }} {{ message }}
    </div>
  </div>
</template>

<script>
// 1. 작성한 믹스인을 import
import firstMixin from "@/components/mixin/firstMixin";
import secondMixin from "@/components/mixin/secondMixin";
import thirdMixin from "@/components/mixin/thirdMixin";
  
export default {
  name: "App",
  mixins: [firstMixin, thirdMixin, secondMixin] // 2. mixin을 적용할 땐 배열에 적용
  data() {
    return {
      data: 5,
    }
  },
  methods: {
    onRightClick() {
      this.data--;
    }
  },
};		
</script>

이렇게 작성하고 로컬 서버를 실행시켜 페이지를 확인한다면 아래와 같은 화면이 출력된다.

화면상으로 봤을 때 출력되는 부분은 {{ data }} {{ message }}이지만 data는 App.vue에서 보유한 데이터를 렌더링하는 중이고 Bye라는 글자는 App.vue에서 보유하지 않고 secondMixin에서 보유한 propsdefault message를 렌더링 하고 있다.

또한, 기존에 Mixin들을 만들어 둘 때 created훅에 각 믹스인의 이름을 콘솔에 출력하도록 해놨는데 순차대로 호출되는 것이 아니고 thirdMixin과 secondMixin이 순서가 바뀌어서 호출되었다.

1-2. 옵션 병합

위 처럼 Mixin과 각 컴포넌트에서 보유한 구성 요소 자체에 겹치는 옵션들이 포함되어 있으면 병합이 이루어지는데 Mixin이 import되는 순서가 기준이 아닌 컴포넌트에서 Mixin배열에 적용되는 순서가 기준으로 가장 마지막에 적용되는 Mixin이 우선권을 갖게 된다.

⛔️ But, 최상위 우선권은 Mixin에서 보유한 옵션들이 아닌 컴포넌트에서 보유한 옵션들이 최우선권을 갖고 적용된다.

위의 상황을 예로 들자면

  1. data가 적용되는 부분
    • data라는 옵션은 현재 firstMixin과 App.vue컴포넌트에만 보유하는 중
      • firstMixin은 10이라는 값을 가지고 있고 App.vue에는 5라는 값을 가지고 있다.
    • 결론적으로 컴포넌트가 최우선권을 가지고 있기 때문에 병합이 이루어지지 않고 컴포넌트의 data를 활용하게 되며 숫자 5를 출력하게 된다.
  2. message가 적용되는 부분
    • message는 App.vue컴포넌트가 보유하지 않고 secondMixin과 thirdMixin이 각각 보유하고 있다.
      • secondMixin이 mixins배열의 마지막에 적용되었기에 thirdMixin의 props가 아닌 secondMixin의 props가 우선권을 갖는다.
    • 결론적으로 secondMixin이 최종 우선권을 가지기 때문에 Hello가 아닌 Bye가 출력된다.
  3. methods가 적용되는 부분
    • methods는 현재 App.vue에는 onRightClick함수로 우클릭 했을 때 data의 value를 1씩 감소하게 하는 함수만 보유하고 있다.
    • firstMixin에는 1씩 증가, secondMixin에는 5씩 증가, thirdMixin에는 10씩 증가하는 methods 함수를 보유하고 있다.
    • 현재의 상태에서 클릭을 한다면 아래와 같은 결과가 도출된다.
      • 콘솔창에서 보이다시피 모든 Mixin들은 methods에 onClick함수를 가지고 있지만 최종적으로는 secondMixin의 onClick함수가 실행되어 5씩 증가하게 되었다.
        • 이는 위에서 보다시피 mixin배열에 최종적으로 secondMixin이 우선권을 가지기 때문에 secondMixin의 onClick함수로 병합된 것을 알 수 있다.

1-3. Mixin 결론

Mixin기능은 컴포넌트 내의 반복되는 로직을 줄여주는데 매우 효율적인 방법 중 하나이다.
하지만 프로젝트 규모가 작을 땐 Mixin을 활용하는데 효과적으로 다가오지만 그 규모가 점점 더 커지게 된다면 "병합"이라는 개념으로 인해 추후 사용중인 Mixin의 옵션들이 어디서 받아오는 Mixin 로직들인지 추적하기가 어렵게 되고 옵션들의 위주로 재사용이 되는 방법이기 때문에 여전히 컴포넌트의 재사용을 줄이는데에는 한계가 있으며 컴포넌트가 옵션 최우선권을 갖는다는 점에 있어 Mixin을 잘못 활용하게 된다면 오히려 유지보수하는데 있어 더 큰 어려움을 겪을 것이다.

이를 활용하기 위해서는 데이터의 구조 단계에 있어 설계가 매우 중요하게 받아들여진다.

2. High-Order Component(HOC)

💡 고차 컴포넌트(HOC, high-order component)는 컴포넌트 로직을 재사용하기 위한 React의 고급 기술입니다. 고차 컴포넌트는 그 자체로는 React API의 일부분이 아닙니다. 고차 컴포넌트는 React의 컴포넌트적 성격에서 나타나는 패턴입니다. (React.js docs)

고차 컴포넌트는 고차 함수의 컴포넌트 버젼으로 느껴졌다.

고차 함수의 함수가 함수를 리턴해주는 개념을 바탕으로 고차 컴포넌트에서는 컴포넌트가 컴포넌트를 새롭게 리턴해주는 개념으로 생각하면 될 것 같다.

2-1. HOC in Vue

예시로 댓글들을 fetch하여 렌더링 해주는 Comment라는 컴포넌트와 게시글들을 fetch하여 렌더링 하는 Post컴포넌트가 존재한다.

<!-- Comment.vue -->
<template>
  <div>{{ data }}</div>
</template>

<script>
import axios from 'axios';
  
export default {
  name: "Comment",
  data() {
    return {
      data: null,
    }
  },
  async created() {
    const res = await axios.get("https://jsonplaceholder.typicode.com/comments?postId=1");
    this.data = res.data;
  },
</script>

<!-- Post.vue -->
<template>
  <div>{{ data }}</div>
</template>

<script>
import axios from 'axios';
  
export default {
  name: "Post",
  data() {
    return {
      data: null,
    }
  },
  async created() {
    const res = await axios.get("https://jsonplaceholder.typicode.com/posts/1");
    this.data = res.data;
  },
};
</script>

해당 컴포넌트에서 보면 데이터를 fetch하는 로직(axios함수)이 중복되고 있다. 이를 HOC로 변환하려면

  • 먼저 js파일을 제작해 함수를 만든다.

    • HOC를 제작할 때 작명은 통상적으로 "With"라는 접두어를 붙여 제작한다.

      /* WithRequest.js */
      import Vue from 'vue';
      import axios from 'axios';
      
      const WithRequest = (url) => (component) => {
        return Vue.component("WithRequest", {
          data() {
            return {
              fetchedData: null,
            };
          },
          async created() {
            const res = await.get(url);
            this.fetchedData = res.data;
          },
          render(createElement) {
            return createElement(component, {
              props: {
                data: this.fetchedData,
              },
            });
          },
        });
      };
      
      export { WithRequest };
  • 활용하고자 하는 컴포넌트에서 해당 HOC를 import해서 활용한다.

    <!-- App.vue -->
    <template>
      <post-page />
      <comment-page />
    </template>
    
    <script>
    import Post from '@/components/Post.vue';
    import Comment from '@/components/Comment.vue';
    import { WithRequest } from '@/WithRequest';
      
    const postUrl = "https://jsonplaceholder.typicode.com/posts/1";
    const commentUrl = "https://jsonplaceholder.typicode.com/comments?postId=1";
    
    export default {
      name: "App",
      components: {
        // WithRequest에서 url을 받고 component를 받아 새로운 component를 리턴하는 구조이다.
        "post-page": WithRequest(postUrl)(Post),
        "comment-page": WithRequest(commentUrl)(Comment),
      },
    };
    </script>
    • 컴포넌트를 import할 때 WithRequest함수를 같이 import하고 해당 컴포넌트를 주입하여 준다.

    • 상위 컴포넌트에서 셋팅이 완료되었다면 해당 컴포넌트를 수정해준다.

      <!-- Comment.vue -->
      <template>
        <div>{{ data }}</div>
      </template>
      
      <script>
      export default {
        name: "Comment",
        props: {
          data: Array,
        },
      };
      </script>
      
      <!-- Post.vue -->
      <template>
        <div>{{ data }}</div>
      </template>
      
      <script>
      export default {
        name: "Post",
        props: {
          data: Object,
        },
      };
      </script>
      • 공통된 로직을 제외하고 하나로 캡슐화시키고 나니 확실히 사용하는 컴포넌트 단에서는 코드가 매우 줄어들고 간결해졌다.

2-2. HOC 결론

Mixin은 컴포넌트 내의 옵션들의 공통부분을 간결하게 작성할 수 있다면 HOC는 컴포넌트 자체의 로직을 간결하게 만들 수 있다는게 큰 장점이라고 생각한다.

Mixin은 Vue의 템플릿을 다루지 않고 옵션만을 재사용 가능하게 만들어 주지만 HOC는 컴포넌트 그 자체를 재사용 함으로써 템플릿까지 포함해 캡슐화가 가능한 부분이다.

Mixin과의 용법에 있어 확연한 차이는 있지만 궁극적으로는 반복되는 코드의 양을 줄이기 위한 방법 중 하나로 둘 의 장단점은 존재한다.

하지만 Vue.js에서의 공식 레퍼런스에선 HOC보단 Mixin과 scoped-slot을 통한 구성을 선호한다고 한다. 이는 선호의 방식일 뿐이지 이렇게 해라 라는 정답은 아니기 때문에 상관없이 스타일에 맞춰 활용하면 될 것 같다.

  • Mixin
    • 단점: Mixin의 갯수가 늘어나면 특정 코드가 어느 Mixin에서부터 왔는지 출처를 알기 힘들어져 유지보수가 불편해진다.
      • 대신 HOC보단 컴포넌트 레벨은 낮다.
  • HOC
    • 단점: HOC가 많아질 수록 컴포넌트 레벨이 깊어지면서 컴포넌트간 통신에 있어 불안정해진다.
      • emit, props전달 등

장점은 두 가지 기술 모두 컴포넌트의 코드가 간결해지면서 코드의 재활용성이 높아진다는 점이다.

3. Composition API

Vue v3.0부터는 기존의 HOC와 Mixin의 단점을 극복하고자 Composition API를 도입하게 되었다. 하지만 Vue 2에서도 Composition API를 활용할 수 있도록 plugin을 제공한다. → vuejs/composition-api

💡 Composition API는 인스턴스의 옵션 단위가 아니라 특정 기능이나 논리의 단위로 코드를 그룹화하고 그 그룹화된 로직을 여러 컴포넌트에서 재사용하는 것이 가고자 하는 방향이다.

3-1. Mixin & HOC와의 차이점

Mixin과 HOC모두 반복되는 코드를 줄여준다는 점에 있어 매우 유용한 방법이었으나 위에서 말한듯이 프로젝트의 규모가 커질 수록 활용하는데 어려움을 겪을 수 있으며 유지보수가 까다로워 진다는 점이 아쉬운 점 중 하나이다.

Composition API는 데이터 그룹핑에 있어 매우 용이하고 데이터 흐름을 파악하고 유지보수가 매우 편해진다는 큰 장점이 있다.

또한 함수를 재사용 하는데 있어서도 아주 유용하다는 점에 있어 반복되는 코드를 import해서 api내부에서 사용함으로써 유틸함수 재사용에 매우 좋다.

  • 기존 Vue2 에서는 재사용을 위한 함수들을 Mixin에 포함시켜서 사용하였었다. 하지만 위에서 설명했다 시피 Mixin에 함수가 추가되거나 데이터가 추가 될 경우 또한 규모가 커질 경우 Mixin이 많아질 경우에 더더욱 데이터와 함수에 대한 추적이 어려웠기 때문에 확장성에 있어서 매우 불리하였다.
  • Composition API를 활용함으로써 재사용하는 유틸 함수를 import export가 가능해졌고 좀 더 데이터 추적 및 사용에 있어서 쉽게 변경되었다. 이를 Vue 3에서는 Composition API라고 한다.

3-2. Why Composition API?

Composition API는 꼭 Mixin과 HOC를 대체하기 위해 등장한 API는 아니다 공식문서에서도 Composition API가 등장했으니 기존에 활용하던 Option API는 없어지지 않을 것이라고 명시했고 Option API에서도 마찬가지로 Composition API를 활용할 수 있다. (setup함수 활용)

또한 Composition API가 함수형 프로그래밍은 아니라고 확실히 정의를 해놨고 Option API와 Composition API 둘 중 꼭 무엇을 사용해라! 라는 것도 아니고 개인의 코딩 스타일 취향에 맞춰서 사용하면 된다고 설명하고 있다.

하지만 Composition API를 활용하는데 있어 강력한 장점은 아래와 같다.

  • 코드 가독성 및 복잡도 개선
    • Option API의 경우에는 역할과 책임 소재에 따라 정해진 위치에 코드가 분리되어 있는 형태이다.
      • SFC의 특성을 가장 잘 보여주는 형태 중 하나로 이해하기 쉽고 조직화 되어 있는 코드라고 볼 수 있지만 프로젝트 규모가 커질수록 중대한 결점이 드러난다.
      • 한 스크립트 내에 역할에 따른 기능과 함수코드들이 나뉘어져있는데 이는 가면 갈 수록 난잡하게 뒤섞이게 되면서 로직을 추적하거나 이해하기가 어렵게 된다. 또한, 이 흩어진 코드들은 서로 밀접한 관계를 맺고 유기적으로 동작하고 있기 때문에 재사용을 위해 코드를 분리하려고 해도 추출하기가 번거로워짐으로 유지보수성이 낮아지게 될 것이다.
    • 위의 단점들을 방지하기 위해 setup()함수 내에서 그룹화 되면서 정리가 될 수 있고 여기에 더해 <script setup>을 명시해줌으로써 setup함수를 사용했을 때 return까지 명시하지 않아도 되는 편리함이 있다.
    • Composition API는 각각의 기능을 함수로 묶어서 독립적으로 정의하고 처리가 가능하기 때문에 어떤 기능을 수정하고자 한다면 그 함수만 찾으면 되는 점으로 모듈화가 가능하다.
  • Composable을 통한 재사용성 개선
    • 동일한 로직을 Vue 2에서는 Mixin을 활용해서 캡슐화 하는 방법이 있었으나 아래와 같은 문제로 사용이 권장되지 않고 유지만 되고 있다.
      1. 네임스페이스가 충돌할 가능성이 높습니다. 예를 들어 두 믹스인을 함께 사용한다면, 서로 같은 이름의 메소드가 존재할 수 있습니다. 또한, 믹스인을 사용하는 해당 컴포넌트에서도 충돌한 이름으로 메소드를 정의할 수 없습니다. 이를 회피하려면 사전에 엄격하게 컨벤션을 합의해야 하는데, 번거롭고 쉽지 않은 일입니다. 컴포저블이었다면 다른 컴포저블과 충돌하는 경우 이름을 변경할 수 있으므로 이러한 문제로부터 자유롭습니다.
      2. 파라미터를 전달할 수 없습니다. 만약 여러 믹스인이 서로 상호작용하는 관계라면, 어떤 공유되는 값에 의존해야 하므로 결합도가 높아집니다. 컴포저블은 일반 함수처럼 다른 컴포저블에 파라미터를 전달할 수 있습니다.
      3. 여러 믹스인을 동시에 사용할 때, 제각기 어떤 믹스인에 의해 주입된 것인지가 명확하지 않아 레퍼런스를 추적하거나 동작을 이해하기가 난해합니다.
    • 이를 피하기 위해 일련의 코드와 논리를 캡슐화 할 수 있는 Composable함수를 활용해 깔끔하게 코드를 재사용 할 수 있다.
  • TypeScript 최적화 및 타입 추론 성능 개선
    • Vue 3는 TypeScript로 제작되어 밀착도가 높아져서 성능적으로 이득을 볼 수 있으며 디버깅에도 아주 효율적이다. 하지만 Vue 2에서는 TypeScript를 제한적으로 지원하기 때문에 Option API에서도 TypeScript 특유의 타입 추론을 구현하기 위해 매우 비효율적인 방법을 사용하고 있다고 공식문서에서 설명한다.
    • Composition API는 통상적인 JS/TS의 코드의 형태로 구현되고 있는 만큼 타입 추론이 효율적으로 수행 될 수 있고 IDE에서도 더 잘 지원됨에 있어 개발 경험이 상승하게 된다.
profile
하나부터 열까지, 머리부터 발 끝까지

0개의 댓글