[Vue] Vue 3 main concept (composition API)

박기범·2022년 3월 16일
0

Vue 2에서 Vue 3로 메인 버전이 업데이트되면서 몇가지 중요한 변화가 있었다.

그 중 가장 중요한 변화는 누가 뭐라해도 composition api를 들 수 있다. 실제로 Vue 3 migration guide를 살펴보면 주목할만한 새로운 기능으로 가장 먼저 composition api를 언급한다.

따라서 본 게시글에서는 Vue 3의 composition api가 가지는 main concept과 Vue 2 소스코드를 Vue 3로 간단하게 포팅해본 결과를 서술해보려 한다.

이 컨셉의 서술에 대한 레퍼런스는 모두 공식 문서를 참조하였음을 미리 밝힌다.

또한 아래에 서술할 특정 코드가 공식문서의 코드일경우, 코드 상단 주석에 표기하도록 한다.

Vue 3 공식문서

Composition API (setup)


//official docs code
export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { type: String }
  },
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // `this.user`를 사용해서 유저 레포지토리 가져오기
    }, // 2
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}

이는 Vue 공식 문서에 존재하는 예시 코드이다. 해당 컴포넌트는 data, computed 등의 option API를 이용하여 컴포넌트를 구성하였다. 비즈니스 로직은 모두 생략처리 되었기에 코드의 양이 상당히 적어 보이지만, 만약 각 option 내부에 존재하는 요소들의 길이가 상당하고, 그 종류도 많다고 생각해보자. 어떤 일이 일어날까?

data 옵션 내부에 존재하는 repositories 데이터를 구성하기 위한 매우 긴 코드가 computed, watch, methods, mounted에 각각 산재되어 서술되는 상황을 생각해보면, 규모가 큰 프로젝트에서는 그 논리적 흐름을 파악하는데 상당한 어려움을 겪는것이 예상된다.

그렇다. 실제로 대규모 프로젝트에서 Vue 2를 도입할경우 컴포넌트의 데이터 및 논리의 흐름을 파악하기 어렵다는것은 여러차례 지적되었던 Vue의 맹점이다. (그 규모가 크면 당연히 컴포넌트를 분할하여 최대한 작게 만들어야 하지 않느냐고? 맞는 말이다. 그런데 그게 말처럼 쉽지만은 않은것이 문제다..)

이러한 부분을 보완한것이 바로 compostion API이다. 그리고 이를 대표하는 api가 바로 setup 이다.

Vue 3에서는 setup을 통해 동일한 논리적 흐름을 한 군데에 묶어서 서술할 수 있도록 하였다. 즉, 동일한 논리의 흐름이라면 computed, method 등의 다양한 option으로 분리하는것이 아닌 setup에 묶어서 서술하는것을 지원한다는 뜻이다.

아래의 예시를 보자.

//official docs code
// src/components/UserRepositories.vue `setup` 펑션
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs } from 'vue'

// 컴포넌트 내부
setup (props) {
  // `props.user`에 참조 .value속성에 접근하여 `user.value`로 변경
  const { user } = toRefs(props)

  const repositories = ref([])
  const getUserRepositories = async () => {
    // `props.user`의 참조 value에 접근하기 위해서 `user.value`로 변경
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)

  // props로 받고 반응성참조가 된 user에 감시자를 세팅
  watch(user, getUserRepositories)

  return {
    repositories,
    getUserRepositories
  }
}

이 예시 또한 공식문서에 존재하는 코드이며, 위의 vue 2 option api를 사용한 코드에서 1번 논리적 흐름을 setup api에 묶어둔 모습이다.

여기서 onMounted와 watch는 composition api에서 지원하는 라이프사이클 훅으로, 각각 option api의 mounted와 watch를 대체한다. 즉, mount되고나서 getUserRepositories를 실행하고 user의 변화에 대해 getUserRepositories를 실행하는 watcher를 등록한것이다.

그리고 setup에서 return된 요소들은 template에서 사용할 수 있다. 다시 다음의 예시를 보자.


<template>
  <transition v-on:enter="enter">
    <div v-if="show" ref="introduceWrapper" class="introduceWrapper">
      <div class="photoWrapper">
        <img src="../assets/myphoto.png" alt="my photo" />
      </div>
      <IntroduceObserver v-on:triggerFadeIn="fadeIn"></IntroduceObserver>
      <IntroduceTemplate></IntroduceTemplate>
    </div>
  </transition>
</template>

// 위 template의 구현부

setup() {
    const show = ref(false);
    const introduceWrapper = ref(null);
    const makeShowTrue = () => {
      show.value = true;
    };
    onMounted(makeShowTrue);
    const fadeIn = function () {
      introduceWrapper.value.style = "transition: opacity 1s";
    };

    const enter = function (el) {
      el.style.opacity = 0;
    };
    return {
      show,
      introduceWrapper,
      fadeIn,
      enter,
    };
  },

setup api를 살펴보자. transition의 v-on을 통해 등록된 custom js hook과 스크롤 탐지시 하위 컴포넌트 이벤트 트리거에 의해 실행되는 method인 fadeIn, 라이프사이클인 mounted 옵션에 해당되는 메소드 까지 모두 setup에 묶여있는것을 볼 수 있다. 그리고 v-if 디렉티브에 의해 감시되는 data인 showsetup에서 return된다. 이렇게 setup에서 return된 데이터는 template에서 사용할 수 있다. 단, 이렇게 return된 데이터를 option api에서 접근하는것은 불가능하다. 이는 추후에 라이프사이클과 관련하여 다시 서술하겠다.

위의 예시코드가 Vue 2에서 작성된것을 보면 차이점을 더 한눈에 느낄 수 있다. 다음은 Vue 2 버전 코드이다.

<template>
  <transition v-on:enter="enter">
    <div v-if="show" ref="introduceWrapper" class="introduceWrapper">
      <div class="photoWrapper">
        <img src="../assets/myphoto.png" alt="my photo" />
      </div>
      <IntroduceObserver v-on:triggerFadeIn="fadeIn"></IntroduceObserver>
      ... (중략) ...
    </div>
  </transition>
</template>
// 위 template의 구현부
import TriggerObserver from "./observers/TriggerObserver.vue";
export default {
  name: "MyIntoduce",
  components: {
    IntroduceObserver: TriggerObserver,
  },
  data() {
    return {
      show: false,
    };
  },
  methods: {
    enter: function (el) {
      el.style.opacity = 0;
    },
    fadeIn: function () {
      this.$refs.introduceWrapper.style = "transition: opacity 1s";
    },
  },
  mounted() {
    this.show = true;
  },
};

"뭐야. 그냥 setup으로 뭉치는게 핵심이야? 그러면 오히려 setup이 엄청 비대해지는거 아니야? 이게 맞아?"

맞다. 필자도 이러한 의문을 가진적이 있다. 결론부터 말하자면, 반만 맞는 의문이다.

setup으로 뭉치는게 핵심이지만, setup이 무조건적으로 비대해지는 것은 아니다.

바로 react hook과 상당히 유사한 compsable의 등장 때문이다.

composable (독립적인 composition function)

이는 말 그대로 composition api에서 호출할 수 있는 함수를 독립적으로 작성하여 사용하는것을 말한다. 리액트의 hook을 기억하는가? 단적으로 useState 를 예로 들어보자.

const [value, setValue] = useState(null)

위와 같이 useState 훅을 호출하면 이 훅에서 관리하는 데이터와, 해당 데이터에 접근하고 수정할 수 있는 closure를 제공해준다.

composable은 결론적으로 위와 같이 특정 데이터와 데이터에 접근할 수 있는 closure를 제공해주는 독립적인 객체이다.

다음의 예시를 보자.


import { ref } from "vue";

export default function TodayComposable(value) {
  const todayString = ref(value);
  const generateToday = function () {
    const today = new Date();
    const year = today.getFullYear();
    const month = today.getMonth() + 1;
    const date = today.getDate();
    const result =
      todayString.value + " today is " + year + "/" + month + "/" + date;
    todayString.value = result;
    return result;
  };
  return {
    todayString,
    generateToday,
  };
}

위는 특정 스트링을 파라미터로 받아 그 스트링에 오늘 날짜를 붙여서 새로운 스트링을 만들어내는 composable이다. 이 composable은 새로운 스트링을 생성하고 todayString에 집어넣어주는 closure인 generateToday 함수와 그 결과물인 todayString을 반환한다.

이러한 composable은 간단하게 component에서 import하여 사용할 수 있다.

<template>
  <header class="header">
    <div class="hello">{{ todayString }}</div>
    <nav class="buttonSet">
      ...(중략)...
    </nav>
  </header>
</template>
//위의 template 구현부
import { ref } from "vue";
import TodayComposable from "./composables/TodayComposable";
export default {
  name: "MyHeader",
  setup() {
    const hello = ref("HELLO");
    const { todayString, generateToday } = TodayComposable(hello.value);
    generateToday();
    // ...(중략)...
    return {
      //...
      todayString,
    };
  },
};

위와 같이 composable을 import하고 구조분해 할당으로 데이터와 closure를 직접 제공받을 수 있다. 그리고 closure를 호출한뒤 결과 데이터를 setup에서 return해주면 template에서 해당 데이터를 사용할 수 있다. 렌더링된 결과를 보자.

오늘 날짜가 정상적으로 header에 포함된 것을 알 수 있다.

이렇게 composable을 이용하여 setup의 크기를 줄이고, 특정 데이터와 데이터의 가공 closure들을 재사용에 용이하도록 분리해낼 수 있다.

마무리

이것으로써 Vue 3가 지향한 가장 중요한 concept인 composition API와 사용 사례, 그리고 이러한 변화가 가져다준 Vue 2와의 차이점을 알아보았다.

이 후에는 composable의 더 다양한 용례와 vue 3에서의 breaking changes 등을 바탕으로 더 상세한 문법적 특성에 대해 알아보도록 하겠다.

profile
원리를 좋아하는 개발자

0개의 댓글