Vue3 #4 [새로운 기능 1 - Composition API]

컴포넌트를 구성하는 새로운 방법 Composition API.


Vue3는 현재 총 7편의 시리즈로 구성되어 있습니다.

  1. Vue3 #0 [Vue3를 만나다]
  2. Vue3 #1 [변경된 기능 1 - 전역 API]
  3. Vue3 #2 [변경된 기능 2 - v-model]
  4. Vue3 #3 [변경된 기능 3 - v-for와 v-if]
  5. Vue3 #4 [새로운 기능 1 - Composition API]-(현재글)
  6. Vue3 #5 [새로운 기능 2 - Teleport와 Fragments]
  7. Vue3 #6 [제거된 기능들]


들어가며

안녕하세요. 이번에는 Composition API를 다뤄보려고 합니다. 사실 변경된 기능은 아니고 3버전에서 새롭게 추가된 API죠. 이전 회차에서 간단히 언급되었던 새로운 API에 대해 조금 더 파악해보는 시간을 가져보도록 하겠습니다.


왜 새로운 API가 필요했나?

2.x 버전에서 Options API를 이용하여 컴포넌트를 구성하는 것은 개인적으로 봤을 때 매우 직관적이고 편리하다고 느꼈습니다. 러닝 커브가 크지 않기 때문에 vue의 진입 장벽을 낮추는 데도 큰 역할을 했을 거라고 생각합니다. 그런데도 불구하고 컴포넌트를 구성하는 뼈대의 방식을 변경하는 새로운 API를 왜 제안하는 것일까요?

제 개인적인 생각이지만 Options API의 컴포넌트 구성 방식은 기능적 분리의 성격이 강합니다. 자, 인스턴스가 생성되면 판매 목록을 불러와서 화면에 표시하고 싶다고 가정해봅시다. 또한, 간단한 카테고리별 필터링 기능도 구현한다고 해보죠. 이 기능을 구현하기 위해서 내가 해야 할 작업의 흐름은 어떻게 될까요? 머릿속에서 대충 흐름을 떠올려 볼게요.


  1. 판매 목록 호출을 위한 메서드
  2. 인스턴스가 생성되면 메서드를 호출
  3. 호출 결과를 화면에 바인딩
  4. 사용자가 카테고리를 변경하면 목록을 재 호출

디테일이 더 필요하지만 대략 위와 같은 흐름을 가진다고 가정해봅시다. 실무에서는 신경 써야 할 부분이 많고 단순한 코드 부분만으로는 구동에 어려움이 있기 때문에 이러 저러한 검증은 건너뛰고 설명을 위한 용도로 단순하게 정리해보겠습니다.

export default {
  data() {
    return {
      sellList: [], // 목록이 담길 배열 선언
      filter: 'all', // 보고싶은것만 보고싶어
    };
  },
  watch: {
    filter() {
      // v-model 변화 감지
      this.sellList = [];
      this.fetchSellList(this.filter);
    },
  },
  created() {
    this.fetchSellList(); // 컴포넌트 생성시 목록 가져오기
  },
  methods: {
    async fetchSellList() {
      // 목록을 호출호출
    },
  },
};

기능을 구현하는 데에는 여러 가지 방법이 있겠지만 일단 저는 이렇게 대략적인 코드를 만들어보았습니다. 지금은 깔끔한 코드를 유지하고 있지만 몇가지 기능을 더 추가한다면 어떻게 될까요? 이해를 돕기 위한 목적으로 임의의 메서드와 더미 데이터들을 몇 가지 더 추가해보겠습니다.

export default {
  data() {
    return {
      sellList: [], // 목록이 담길 배열 선언
      userList: [],
      alarmList: [],
      filter: 'all', // 보고싶은것만 보고싶어
    };
  },
  computed: {
    sortedUserList() {},
  },
  watch: {
    filter() {
      // v-model 변화 감지
      this.sellList = [];
      this.fetchSellList(this.filter);
    },
  },
  created() {
    this.fetchUserList();
    this.fetchSellList(); // 컴포넌트 생성시 목록 가져오기
  },
  methods: {
    async fetchSellList() {
      // 목록을 호출호출
    },
    async fetchUserList() {},
    submitDatas() {},
  },
};

앞서 구현하려고 했던 코드의 묶음들이 다른 기능을 위한 코드들과 뒤섞이면서 마치 이곳저곳에 산재하게 된 듯한 모양새가 되어버렸습니다.

개발한 기능을 흐름대로 살펴보고 싶다면 컴포넌트를 전반적으로 둘러 살펴봐야 합니다. 공식 홈페이지에서는 이러한 탐색 행위를 ‘코드 블록을 점프’한다고 표현하고 있습니다. 바로 논리적인 흐름을 한눈에 파악하기 어려워지는 것인데요. 이러한 점은 애플리케이션이 거대화되고 컴포넌트의 내용이 복잡해질수록 더 심해지겠죠. 또한, 실무에서는 내가 개발한 코드를 끝까지 책임지는 상황은 많지 않습니다. 타인의 작업물에 대해서 분석에 시간을 많이 필요로 할수록 유지보수 비용은 증가합니다.

이런 니즈가 Composition API의 도입 배경이라고 볼 수 있습니다.

이유도 알았겠다, 이제 Options API로 구현해보았던 내용을 새 API로 바꿔봅시다.


setup()

옵션 선언 단계에서 새 API인 setup()을 선언해줍니다. 이곳에 논리적인 흐름에 따라서 구현하고 싶은 기능을 넣어줄 거에요. setup()은 컴포넌트가 생성되기 이전에, props가 반환(resolved)될 때 실행됩니다. 즉, 이 시점에는 컴포넌트 인스턴스가 생성되지 않았기 때문에 props를 제외하고 컴포넌트 내부 데이터에 접근할 수 없다는 특이사항은 꼭 기억해야 합니다. (this에 접근 불가능)


data 선언


export default {
  setup() {
    let sellList = []; // 목록이 담길 배열 선언

    return {
      sellList,
    };
  },
};

변수가 선언되었지만, 아직 반응성은 없습니다. 선언한 변수에 반응성을 가지게 하기 위해서는 vue 내장 ref 펑션을 사용하면 됩니다. 그럼 value 속성을 가지고 있는 객체를 반환하게 되고 템플릿 등에서 접근 가능하게 됩니다.

import { ref } from 'vue';

export default {
  setup() {
    const sellList = ref([]); // 목록이 담길 배열 선언

    sellList.value.push({ name: '맥주', count: 5, category: 'alcohol' });

    return {
      sellList,
    };
  },
};

props의 값에 접근하는 경우라면 다음과 같이 작업 될 수 있겠습니다. 데이터에 접근하고 가공하는 데는 다양한 방법이 있으니 코드의 내용보다는 접근 방법에 대해서만 이해하고 넘어가면 될 것입니다.

import { ref } from 'vue';

export default {
  props: {
    list: {
      type: Array,
      default() {
        return [{ name: '맥주', count: 5, category: 'alcohol' }];
      },
    },
  },
  setup(props) {
    const sellList = ref([]); // 목록이 담길 배열 선언

    sellList.value = props.list;

    return {
      sellList,
    };
  },
};

props의 list 데이터에 접근한다는 가정하에 나머지 코드들도 옮겨보겠습니다.

import { ref } from 'vue';

export default {
  props: {
    list: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  setup(props) {
    const sellList = ref([]); // 목록이 담길 배열 선언
    const getSellList = async () => {
      // 목록을 호출호출
      sellList.value = await fetchSellList(props.list);
    };

    return {
      sellList,
      getSellList,
    };
  },
};

라이프사이클 훅

setup()내부에서 라이프사이클 훅을 호출하려면 on접두사를 붙여주면 됩니다.

import { ref, onMounted } from 'vue';

export default {
  props: {
    list: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  setup(props) {
    const sellList = ref([]); // 목록이 담길 배열 선언
    const getSellList = async () => {
      // 목록을 호출호출
      sellList.value = await fetchSellList(props.list);
    };

    onMounted(getSellList); // 컴포넌트 생성시 목록 가져오기

    return {
      sellList,
      getSellList,
    };
  },
};

라이프사이클 훅 역시 vue 내장 펑션을 import 하여 사용합니다. 여기에서 Composition API의 라이프사이클 훅은 Options API와는 다르다는 점을 알 수 있는데 created 및 beforeCreate의 경우 setup() API가 컴포넌트 진입 시작 역할을 하므로 자체적으로 기능이 대체되어 사용할 수 없습니다.

참고로, 라이프사이클 훅의 변화에 대해서도 알고 넘어가면 좋겠죠?

3버전에서는 일부 명칭이 조금 더 직관적으로 변경되었습니다.

  • destroyed → unmounted
  • beforeDestroy → beforeUnmount

사용자 정의 디렉티브의 라이프사이클 훅은 다음과 같이 변경되었으니 작업할 때 잘 기억해야겠습니다..

  • bind → beforeMount
  • inserted → mounted
  • beforeUpdate: 새로 생김 ( 디렉티브가 사용된 엘리먼트 자체가 업데이트 되기 전에 호출됩니다 )
  • update: 제거됨 (updated와 기능 중복으로 제거되었습니다)
  • componentUpdated → updated
  • beforeUnmount: 새로 생김 ( 엘리먼트가 제거되기 이전에 호출됩니다 )
  • unbind → unmounted

computed와 watch

사용 빈도가 잦은 옵션들인 computed와 watch를 사용하기 위해서 ref처럼 내장 함수를 import 해옵니다. 사용 방법은 크게 다르지 않으니 어려운 점은 없습니다.

import { ref, onMounted, watch } from 'vue';

export default {
  props: {
    list: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  setup(props) {
    const sellList = ref([]); // 목록이 담길 배열 선언
    const getSellList = async () => {
      // 목록을 호출호출
      sellList.value = await fetchSellList(props.list);
    };
    onMounted(getSellList); // 컴포넌트 생성시 목록 가져오기

    const filter = ref('all'); // 보고싶은것만 보고싶어
    watch(filter, (val, oldVal) => {
      // v-model 변화 감지
      sellList.value = [];
      getSellList();
    });

    return {
      sellList,
      getSellList,
    };
  },
};

computed도 비슷하게 사용하면 됩니다. computed로 계산된 속성 역시 .value로 접근해야 한다는 점은 꼭 기억해야겠습니다.

import { ref, computed } from 'vue';

setup() {
	const userName = ref('홍길동');
	const welcomeMessage = computed(() => {
		return `${userName.value}님. 방문을 환영합니다!`
	});

	console.log(welcomeMessage.value); // .value로 접근

	return {
		userName, welcomeMessage
	}
}

전달인자 등 더 다양한 정보는 Docs를 확인하는 것이 좋겠습니다.


마치며

정말 간단하게 Composition API를 찍먹해보았습니다. (맵다)

저는 익숙했던 개발 흐름이 아니라서 처음에는 매우 어색한 느낌이었는데 어떤 느낌이셨을지 모르겠습니다. 하지만 역시 친숙한 프레임워크라서 그런지 약간의 시간만 투자한다면 누구나 쉽게 습득 가능한 내용이라고 생각이 듭니다. 한가지 짚고 가야 할 점은 시리즈 1회차에서도 언급했듯이 기존 문법도 사용할 수 있다는 점입니다. 3버전에서 Composition API를 제시하고 있지만, 여전히 Options API로도 개발이 가능하다는 점, 그렇기에 프로젝트 구성원들과 충분한 협의를 거쳐 개발 방향에 대해 논의해볼 수 있다는 점은 새로운 변화에 적응이 쉽지 않은 개발자(나)들에게 매우 고무적이라는 생각이 드네요.

이왕 새 기능에 대해서 보따리를 풀었으니 다음 글도 Teleport 등 반가운 새 기능에 대해서 마저 다루고 나머지 이야기들을 진행할까 합니다. 그럼, 다음 글에서 뵐게요.


참고자료


추천 글