본문 바로가기

개발/프론트엔드

Vue3로 프로젝트를 진행하면서 알게 된 것들

반응형

최근에 Vue3와 Typescript를 학습하기 위한 목적으로 간단한 개인 프로젝트를 진행하였다. 해당 프로젝트를 진행하면서 알게 된 것들을 정리하고자 한다. Vue3에 대한 튜토리얼은 아니고, 시행착오에 대한 기록이라 할 수 있다.

script setup

Vue3의 composition api는 보통 아래와 같은 형태로 사용했다.(Vue.js 공식문서의 예제)

<script>
const isAbsent = Symbol()

export default {
  props: {
    foo: { default: isAbsent }
  },
  setup(props) {
    if (props.foo === isAbsent) {
      // foo was not provided.
    }
  }
}
</script>

하지만, 최근 Vue의 공식문서를 보면 <script setup> syntax를 사용할 것을 권장한다.

<script setup>
import { ref } from 'vue'

const props = defineProps({
  foo: String
})
const emit = defineEmits(['change', 'delete'])
const a = 1
const b = ref(2)

</script>

setup function을 쓰는 것보다 훨씬 단순하고, Props나 Event를 순수 Typescript를 활용해 선언할 수 있다. 그리고 런타임 시에 성능이 더 좋으며, IDE 지원이 잘된다고 한다. 해당 내용에 대해서 더 자세히 알고 싶다면 아래 공식문서를 확인하자.

SFC <script setup>

<script setup>을 활용한 엄청난 규모의 오픈소스 프로젝트 slidev.js도 있으니 참고하면 좋을듯하다. 개인적으로도 해당 프로젝트를 많이 참고하였다.

async component와 <suspense>

async setup() 함수나 <script setup>을 사용하고 가장 상위단에서 async, await를 사용하면 해당 컴포넌트가 비동기 컴포넌트가 된다. 비동기 컴포넌트라는 말은 최상단 await가 모두 완료되고 나서야 mount되는 컴포넌트를 의미한다. 주의할 점이 이런 비동기 컴포넌트를 사용할 때에는 해당 컴포넌트를 사용하는 상위 컴포넌트에서 <suspense>를 사용하여 해당 컴포넌트를 감싸줘야 한다.

<suspense timeout="0">
  <AsnycComponent />
  <template #fallback>
    <div>
        <h1>Loading...</h1>
      </div>
  </template>
</suspense>

위의 코드에서 timeout="0"으로 지정해준 이유는, AsyncComponent가 마운트되기 전에 #fallback 컴포넌트를 로딩으로 보여주기 위함이다. timeout이 0이면 시작하자 마자 해당 컴포넌트 로딩이 실패한 것으로 간주하고 #fallback 컴포넌트를 보여주고, AsyncComponent가 마운트될 때 #fallback 컴포넌트는 AsnycComponent로 대체된다.

*<suspense>는 추후 사용법이 변경될 가능성이 높은 컴포넌트라고 하니 주의하자!

자세한 내용은 공식문서를 확인하자.

router-view + async component page

만약 vue-router를 사용하고, <router-view>에 해당하는 페이지가 async component라면 아래와 같이 사용한다.

<router-view v-slot="{ Component }">
  <suspense timeout="0">
    <component :is="Component" class="w-full h-full"></component>
    <template #fallback>
      <div>
        <h1>Loading...</h1>
      </div>
    </template>
  </suspense>
</router-view>

번외로 router-view는 v-slot을 통해 Component뿐만이 아니라 route도 전달할 수 있다. 해당 부분을 이용하여 특정한 페이지에만 트랜지션 효과 등을 넣을 수 있다. 자세한 부분은 vue-router의 공식문서를 확인하자.

async component 테스트

Vue3에서 Jest를 사용하기 위해서는 vue-test-utils-next를 사용하면 된다. 다만 async component를 사용하는 경우에는 일반적인 방법으로 mount, 또는 shallowMount가 되지 않는다. async component를 mount시키기 위해서는 아래와 같은 utils함수를 만들어서 사용한다.

github 이슈를 참고하여 코드를 조금 변형시켰다.

export function AsyncWrapper(component: Component): DefineComponent {
  return defineComponent({
    components: { AsyncComponent: component },
    template: `<Suspense><AsyncComponent/></Suspense>`,
  });
}

테스트 코드에서 아래와 같이 mount시킨다.

import { mount, flushPromises } from '@vue/test-utils';

// ...

  it('should do something', async () => {
    mount(AsyncWrapper(AsyncComponent));
    await flushPromises();
  });

위에서 flushPromises를 사용하는데, 비동기 컴포넌트의 경우 해당 함수를 사용해주어야 비동기 컴포넌트 내부의 비동기 호출이 모두 완료됨을 보장할 수 있다. 따라서 mount이후에 바로 해당함수를 사용하도록 하자.

그리고, script setup을 사용할 경우에는 현재로서는 내부에서 정의된 props나 computed, function 등을 가져올 수 없다. 따라서, 나처럼 삽질하지 말고, template을 통해 잘 테스트해주도록 하자. 테스트가 반드시 필요한 로직이라면 외부에서 테스트할 수 있게 프로젝트를 구성해야 한다. 사견으로 유닛 테스트 관점에서는 굳이 public interface가 아닌 내부 props, computed, function을 테스트하도록 작성할 필요가 없기도 한 것 같다.

vue-router 테스트 모킹(Mocking)

vue-router를 사용하는 경우 아래와 같이 pushreplace 메서드를 mocking 할 수 있다.

const mockRouterReplace = jest.fn();
const mockRouterPush = jest.fn();
jest.mock('vue-router', () => ({
  useRouter: () => ({
    push: mockRouterPush,
    replace: mockRouterReplace,
  }),
}));

// ...

  it('should do something', async () => {
    const wrapper = mount(AsyncWrapper(AsyncComponent));
    await flushPromises();
    await wrapper.find('header').trigger('click');
    expect(mockRouterPush).toHaveBeenCalledWith('/user');
  })

만약 해당 컴포넌트 내에서 <router-view>또는 <router-link>를 사용했다면 아래와 같이 stubs을 사용할 수 있다.

shallowMount(Component, {
  stubs: ['router-link', 'router-view']
})

이외에도 다양한 방법이 있는데, 더 자세한 내용은 vue-test-utils-next의 Testing Vue Router를 살펴보면 된다.

vueuse/core

vueuse/core는 다양한 헬퍼 composition api 함수들을 모아둔 라이브러리이다. event listener부터, debounce function, storage 등을 composition api로 구성하고 있다. Vue.js의 창시자인 Evan You도 후원하고 있는 프로젝트이다. 어떤 Vue 프로젝트를 하든 해당 라이브러리를 설치해서 사용하면 쓰임새가 괜찮겠다고 느꼈다. (이런 곳에서 바퀴를 재발명하지 말자.) 위에서 언급한 Slidev.js 역시 해당 라이브러리를 사용하고 있다. Treeshaking 기능을 제공하고 있으니, 모든 함수들을 다 사용하지 않아도 라이브러리 용량 걱정을 하지 않아도 된다.

한 가지 예를 보여주면, 아래와 같이 사용할 수 있다.

import { useEventListener } from '@vueuse/core'

const element = ref<HTMLDivElement>()
useEventListener(element, 'keydown', (e) => { console.log(e.key) })

일반적으로 addEventListener를 통해 이벤트를 추가했다면 beforeMount에서 해당 이벤트를 해제해주어야 한다. 그렇지 않으면 해당 이벤트가 컴포넌트가 사라졌는데도 계속 발생한다. 해당 composition api를 사용하면 useEventListener가 이 과정을 자동으로 해준다.

일단, Vuex를 사용하지 말고, Pinia를 사용하자.

현재 Vue3를 지원하고 있는 Vuex는 버전 4이다. 하지만, Vue2에서 사용하던 Syntax와 거의 같은 Syntax를 사용하고 있고, Typescript Support가 부족하고, Vue3의 개선점을 반영하지 않고 있다. (만족스럽지 않다는 뜻) 아무래도 Vuex5를 기다리는 게 좋을 듯하다.

Vuex5의 RFC를 보면 Vue3의 개선점을 훨씬 잘 반영하고 있다.

Pinia는 Vuex가 아닌 다른 상태 관리 라이브러리인데, Github Star 수도 2.8k로 꽤나 많은편이고 npm 다운로드수도 계속 증가하고 있다. 문서화가 잘되어있고, 무엇보다 Vue 3의 이점을 많이 반영한 상태관리 라이브러리이다. Vuex5가 나오기 전까지, 혹은 앞으로 계속해서도 사용하기 좋은 라이브러리로 보인다. (물론 개인적인 판단이다.)

내가 작업하면서 참고한 오픈 소스와 자료들 공유

작업하면서 많은 블로그 포스트, 오픈소스들을 봤고 도움을 많이 받았다.

  1. vue3-realworld-example-app
    • 꽤나 큰 예제 프로젝트이다. 해당 프로젝트를 통해 프로젝트 구성, 테스트 코드, Vue3 활용법 등 다양하게 도움을 받았다.
  2. slidev.js
    • 위에서 몇 번 소개했는데, Vue3와 <script setup>을 활용한 프로젝트이다. 프로젝트 자체도 흥미롭고, 많은 도움을 받았다.
  3. 프로젝트 구성에 도움을 받은 블로그들
반응형