Post

[Vue3] 로직의 재사용이 유용한 Composition API와 hook을 통한 관심사의 분리

Vue2와 Vue3의 차이점

🐀 Vue2와 Vue3의 차이점
  • Composition API
    Composition APIVue2에서는 플러그인 형태로 사용가능했지만,
    Vue3부터 라이브러리 공식 API로 채택되었다.

  • root element
    Vue2에서는 root element가 하나여야 되는 한계점이 있지만,
    Vue3에서는 root element가 여러개여도 상관없다.

Vue3로 넘어가면서 라이브러리의 전반적인 로직을 타입스크립트로 재작성했다.


🍪 Vue와 React의 가장 큰 차이점
Vue는 Reactivity(반응성) 기반이고 React는 Immutability(불변성) 기반이다.
반응성은 객체의 내용이 변화함에 따라 화면의 내용도 바껴져 나가는 시스템을 말하고
Vue3는 이를 Proxy를 통해 구현한다.
반대로, 불변성이란 메모리 영역에서 값이 변하지 않도록 하는 것을 의미한다.
state가 변했을 때, 객체의 참조 주소만을 비교하여 렌더링을 결정한다.
만약 react가 불변성을 유지하지 않으면, 즉 객체나 배열의 값이 변할 수 있으면 새로운 상태 역시,
🐖 동일한 참조를 가리키므로 react가 상태변화를 감지하지 못할 수 있다.

Composition API

🐁 함수기반의 Composition API

Option API에서 나누어 작성했던 속성을 Composition API는 setup 속성에 한번에 작성한다. Option API와 섞어쓸 수 있다..
즉, setup 함수 안에서 변수와 함수를 선언해서 사용할 수 있게 만들었는데, 이러한 구조적인 이점은 타입추론을 용이하게 한다.
※ 함수나 변수를 정의할 때 타입을 추론하기 쉽다.

🪄 defineComponent와 타입추론
defineComponent는 vue 컴포넌트의 더 나은 타입추론 및 실시간 오류검사를 제공한다.
물론, vue3가 기본적으로 타입스크립트를 지원하기 때문에 없이 사용해도 문제가 발생하지 않을 수 있다.
하지만 🐖 컴포넌트 관계가 복잡해질수록 일부 타입추론이 누락되거나 수동으로 추가해야 하는 경우가 발생할 수 있다.
즉, defineComponent를 사용하면 모든 컴포넌트가 동일한 패턴을 따르게 되어 코드의 일관성 🏆이 향상된다.
1
2
3
4
5
6
7
<script lant="ts">
export default defineComponent({
  setup() {
    // ...
  }
});
</script>

setup 함수는 Vue 인스턴스가 생성되기 전에 실행된다.
즉, Vue 컴포넌트의 반응성 시스템이 완전히 설정되지 않은 상태에서 실행되기 때문에
setup 함수 내부에서 thisVue 인스턴스를 참조하지 않는다.

대신, setup 함수 내부에서 reactiveref 같은 api가 반응성 상태를 직접 관리한다.
setup 함수를 실행하고 반환할 때 Vue 인스턴스가 생성된다.

📕 객체 기반의 Options API와 this
Vue 컴포넌트가 마운트되기 전에 Vue는 컴포넌트 상태를 초기화하고 Vue 인스턴스를 생성한다.
created 라이프사이클 훅을 호출한 후에 DOM에 마운트된다.
즉, created 이후부터 this를 사용하여 Vue 인스턴스를 참조하여 속성이나 메서드를 사용할 수 있다.
🪄 Composition API, 동적 ref
Composition API에서는 this가 사용불가하기 때문에, DOM 요소에 직접 접근하기 위해 this.$refs 역시 사용할 수 없다.
때문에, 다른 방법으로 DOM요소에 접근하는데, 이는 반응성을 관리하는 변수명특정 태그의 ref속성값이 같으면
해당 컴포넌트가 마운트될 때 해당 변수에 DOM 요소에 대한 직접적인 참조를 얻을 수 있다.
또한, v-for문을 이용하여 동적으로 생성한 태그에도 같은 방식으로 동적태그에 대한 직접적인 참조를 얻을 수 있는데
이 때, 다른게 있다면 반응성을 관리하는 변수를 객체나 배열로 초기화하는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
  <div v-for="n in 5" :key="n">
    <img src="https://picsum.photos/200" ref="imgRefs"
  </div>
</template>

<script lant="ts">
import { Ref, ref } from 'vue';

export default {
  setup() {
    const imgRefs = Ref<HTMLImageElement[]> = ref([]);
  }
}
</script>
 
위와 같이 동적 태그의 ref속성값반응성을 관리하는 변수명을 맞춰주면
해당 컴포넌트가 마운트될 때, 배열로 초기화한 변수의 각 요소에 아래와 같은 DOM에 대한 직접적이 참조를 얻을 수 있다.
1
2
3
4
5
6
imgRefs: Array[5]
> 0: <img>
> 1: <img>
> 2: <img>
> 3: <img>
> 4: <img>

또한, 🎯 Composition API를 이용하면 로직을 재사용을 극대화할 수 있다.

🐝 useMessage.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ref } from "vue";

function useMessage() {
  const message = ref("hello");

  function changeMessage() {
    message.value = "hi";
  }

  return { message, changeMessage }; // setup 바깥에서 사용하기 위해 return
}

export { useMessage };

🦇 App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
  <div>
    <p></p>
    <button @click="changeMessage">change</button>
  </div>
</template>

<script>
import { useMessage } from "./hooks/useMessage";

export default {
  setup() {
    const { message, changeMessage } = useMessage();
    return { message, changeMessage }; // setup 바깥에서 사용하기 위해 return
  }
};
</script>
☑️ setup의 인자
setup의 첫번째 인자는 props이고, 두번째 인자는 context이다.
단, props의 속성에 접근할 때, Destructuring을 이용하면 안된다.
Destructuring을 이용하는 순간 Reactivity가 사라진다. 이러면, 데이터 싱크를 맞추기 어려워지고 버그로 이어질 수 있다.

Destructuring the `props` will cause the value to lose reactivity

두번째 인자인 context에는 emit (이벤트 발생)과 같은 함수가 들어있는데, 이는 호출하는 용도이므로, Destructuring 해도 상관없다.
⚠️ Composition API에서는 this를 쓰지 않는다.

ref API와 .value

🐙 ref API와 .value
  • ref vue3
    🗣️ ‘Reactivity를 주입하는 변수를 선언하겠다.’
  • .value
    해당 속성을 통해, 변수의 실질적인 값을 제어한다.
🍪 왜 .value를 통해 실질적인 값에 접근하게 만든걸까?
vue는 자동으로 데이터의 변경사항을 감지하고, 그에 따라 DOM을 업데이트한다.
즉, vue는 렌더링 중에 사용된 모든 참조를 추적한다.
참조가 변경되면, 참조를 추적하는 구성요소에 대한 리렌더링을 트리거한다.
 
표준 js에서는 일반 변수의 접근이나 변경을 감지할 방법이 없다.
하지만, getter/setter 메서드를 이용하면 접근과 변경을 감지할 수 있는데,
여기서 .value 속성은 vue가 참조에 접근하거나 변경한 시점을 감지할 수 있는 기회를 제공한다.
ref로 변수를 초기화 했을 때, 즉 Reactivity가 주입된 값에 대해서
.value를 통해 값에 접근하거나, 값이 변경됨에 따라 특정 동작을 수행할 수 있는 구조를 만들어 놓았다.
1
2
3
4
5
6
7
8
9
10
11
12
const myRef = {
  _value: 0,
  get value() {
      track()
      return this._value
  }

  set value(newValue) {
      this._value = newValue
      trigger()
  }
}

하지만, 기존 vue2에서의 개발경험을 해치지 않기 위해 setup 바깥에서는 .value없이 접근할 수 있게 고안되었다.

Composition Style Based API, vue3

🍝 vue 라이브러리에서 제공하는 API를 가져와서 호출하여 Option API의 속성을 구현한다
  • computed API
    특정 연산을 담아두는 속성으로, 값을 원하는 형태로 변환하는 익명함수를 작성한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    <template>
      <h1></h1>
      <h4></h4>
    </template>
    
    <script>
    import { computed } from "vue";
    
    export default {
      props: ["appTitle"],
      setup(props) {
        const newTitle = computed(() => {
          return props.appTitle + "!!";
        });
    
        return { newTitle };
      }
    };
    </script>
    

  • Lifecycle API
    라이프사이클 훅 제공
    vue 인스턴스 라이프사이클이란 인스턴스가 생성되어 소멸되기까지 거치는 과정을 의미한다.
    인스턴스가 생성되면 라이브러리 내부적으로 다음 과정이 진행된다.
    1
    2
    3
    
    - data속성의 초기화 및 관찰(Reactivity 주입)
    - vue 템플릿 코드(템플릿 표현식) 컴파일 (virtual DOM -> DOM 변환)
    - 인스턴스를 DOM에 부착
    

    Alt text

    ※ 컴포넌트 생성 직후 바로 setup이 호출된다, 이 때 데이터가 동기화된다.

    🌩️ 네이밍 컨벤션 변경

    vue2vue3
    beforeCreatex, 대신 setup 사용
    createdx, 대신 setup 사용
    beforeMountonBeforeMount
    mountedonMounted
    beforeUpdateonBeforeUpdate
    updatedonUpdated
    beforeDestroyonBeforeUnmount
    destroyedonUnmounted
    errorCapturedonErrorCaptured

  • watch API
    특정 데이터가 변했을 때, 해당 변화를 감지해서 부가적인 동작을 호출한다.
    ※ watch는 최대한 지양, 개발자가 데이터 추적이 어려워진다.
    1
    2
    3
    4
    5
    
    import { ref, watch } from "vue";
    
    watch(message, (newValue, oldValue) => {
      console.log({ newValue });
    });
    

Custom Composition Function (hook)과 관심사의 분리

🍝 Composition API의 강점은 컴포넌트에 남아있어야 할 로직과 그러지 않은 로직을 별도의 파일로 나눌 수 있다는 점이다

컴포넌트의 동작을 알아차릴 수 있는 로직만 남겨 동작을 명시적으로 추상화시키는 것이 좋다.
별도의 파일로 분리된 세부적인 로직은, 컴포넌트에 남아있는 로직에서 변수 및 함수명으로 유추할 수 있게 네이밍한다.
단, 별도의 파일로 분리되었을 때 장점이 명확한 경우에만 분리한다.

🎯 setup 당위성
많은 컴포넌트에서 동일한 코드가 반복되는 경우
하나의 파일에서 해당 컴포넌트들에 뿌려질 수 있는 형태로 모듈화, 관심사의 분리

🐝 useTodo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { ref } from "vue";

function useTodo() {
  const todoItems = ref([]);

  function fetchTodos() {
    // 의존성이 생기지 않게 해당 함수 내에서만 사용하는 변수 선언 (의존성 제거)
    // 함수 내의 동작이 외부 코드와 연결될 때 '의존성이 생긴다' 라고 표현한다.
    const result = [];
    for (let i = 0; i < localStorage.length; i++) {
      const todoItem = localStorage.key(i);
      result.push(todoItem);
    }
    return result;
  }

  function addTodoItem(todo) {
    todoItems.value.push(todo);
    localStorage.setItem(todo, todo);
  }

  return { todoItems, fetchTodos, addTodoItem };
}

export { useTodo };

🦇 App.vue

1
2
3
4
5
6
7
8
9
10
11
<script>
  import { useTodo } from './hooks/useTodo';

  export default {
    setup() {
      const { todoItems, fetchTodos, addTodoItem } = useTodo();

      /*...생략 */
    }
  }
</script>
This post is licensed under CC BY 4.0 by the author.