Vue Composition API

sejin kim·2022년 8월 27일
0
post-thumbnail

왜 Composition API인가?

Composition APIVue 3를 대표하는 핵심 기능 중 하나입니다. 그 이름대로, 컴포넌트를 구성하고 조합하기 위해 사용하는 새로운 API 세트라고 할 수 있습니다. 규모가 있는 프로젝트에서, 로직을 재사용하고 관심사를 분리하기에 보다 용이하며 읽기 쉽고 논리적인 코드를 작성하기 위한 목적으로 도입되었습니다.

다만 그동안 Vue 2 버전에서 사용되던 기존의 Options API도 폐기되는 것은 아니어서, 플러그인을 추가하여 사용할 수 있는 등, 공식적으로 상호 취사선택이 가능하다고 가이드하고 있습니다.

그러나 일찌감치 생태계가 Vue 3 기반으로 재편되어가고 있었고(Vue 3는 20년 9월에 릴리즈되었고, 소프트 런칭을 통해 점진적인 마이그레이션을 지원하다가 22년 2월에 공식적으로 기본 버전으로 지정되었습니다), 일종의 패러다임 - 방법론 측면에서의 변화이기도 하기 때문에, 아무래도 개발자의 입장에서는 '이제 Composition API를 써야 한다'고 생각하게 됩니다.



하지만 무작정 '새로운 것이니까' 사용하는 것은 바람직하지 않을 것입니다. 무언가 기술을 사용할 때에는 먼저 그 기술에 대한 이해, 목적 및 배경에 대한 파악이 선행되어야 합니다. Composition API는 무슨 이점이 있으며, 기존의 어떤 문제를 해결하기 위해 등장했을까요?



코드 가독성/복잡도 개선

Options API의 경우, 역할과 책임 소재에 따라 정해진 위치에 코드가 분리되어 있는 형태였습니다(data, methods, computed, watch ... ). 이는 일견 직관적이고 코드가 잘 조직화되어 있다고 생각될 수 있지만, 프로젝트의 규모가 커질 수록 중대한 결점이 드러나게 됩니다.

필연적으로 컴포넌트는 여러 기능의 조합으로 구성됩니다. 그런데 하나의 기능을 구현하는 코드부터가 파편화되어 있으므로, 갈수록 난잡하게 뒤섞이게 되면서 로직을 추적하거나 이해하기 어렵게 됩니다. 흩어진 코드들은 서로 밀접한 관계를 맺고 유기적으로 동작하고 있을 것이기 때문입니다. 재사용을 위해 코드를 분리하려고 해도, 추출하기가 번거로우니 유지보수성도 낮아집니다.

아래 예제는 단순한 케이스이므로 당장 크게 체감되지 않을 수 있지만, 실제 프로젝트에서 얼마나 효과가 있을지 추측하는 데에는 도움이 될 것입니다.


// Options API
<script>
export default {
    data() {
        return {
            count: 0
        }
    },
    computed: {
        countString() {
            return `Current count: ${this.count}`;
        }
    },
    methods: {
        addCount() {
            this.count++;
        },
        subtractCount() {
            this.count--;
        }
    },
    mounted() {
        console.log('mounted');
    },
}
</script>


// Composition API (script setup syntax)
<script setup>
import { onMounted, ref } from 'vue';

const count = ref(0);
const countString = computed(() => `Current count: ${count.value}`);

const addCount = () => {
    count.value++;
};

const subtractCount = () => {
    count.value--;
};

onMounted(() => {
    console.log('mounted');
});
</script>

위 예제는 조금 더 코드를 간결하고 읽기 쉽게 표현하기 위해 <script setup> 이라는 구문을 활용한 예제입니다만, 여하튼 Composition API의 핵심은 setup() 함수에 모든 기능들이 조합(composition)된다는 점입니다.

파편화된 코드들이 함수 내에서 그룹화되어 정리되었고, 특히 <script setup> 구문으로 작성하면서, 템플릿에 전달하기 위한 return도 명시적으로 하지 않아 조금 더 간결해졌다는 점도 돋보입니다.


<script setup>Vue의 싱글 파일 컴포넌트(SFC) 구조에서 Composition API를 더 간결한 형태로 작성하기 위한 문법적 설탕 구문입니다. setup() 함수를 직접 정의하는 기본 형태에 비해 가독성이 조금 더 개선되고, 성능적 이득도 있어 보편적으로 권장됩니다.


아래는 흔히 Composition API를 설명할 때 인용되는 예제로, Vue의 개발자인 Evan You가 직접 동일한 컴포넌트를 Composition API리팩토링한 코드입니다. 복잡하게 혼재되어 있던 코드들이 비교적 깔끔하게 정리되는 모습을 볼 수 있습니다.



Composition API는 각각의 기능을 함수로 묶어서 독립적으로 정의, 처리할 수 있기 때문에, 어떤 기능을 수정하고자 하면 그 함수만 보면 됩니다.



Composables을 활용한 재사용성 개선

import { computed, ref } from 'vue';

export const useUserList = () => {
    const users = ref(['Jane', 'Mark', 'Bob']);
    const usersList = computed(() => users.value.join(', '));
    const addUser = (user) => {
        users.value.push(user);
    };
    const removeUser = (username) => {
        users.value = users.value.filter((user) => user !== username);
    };

    return {
        users,
        usersList,
        addUser,
        removeUser,
    };
};
<script setup>
import { useUserList } from './userList.js';

const { users, usersList, addUser, removeUser } = useUserList();
</script>

위와 같이 일련의 코드, 논리를 캡슐화할 수 있는 Composables 공통 함수를 사용하여 깔끔하게 코드를 재사용할 수 있습니다. 기존에도 Mixins을 활용하는 방법이 있었지만, 아래와 같은 문제가 있어 현재는 더 이상 사용이 권장되지 않고 유지만 되고 있습니다.


  • 네임스페이스가 충돌할 가능성이 높습니다. 예를 들어 두 믹스인을 함께 사용한다면, 서로 같은 이름의 메소드가 존재할 수 있습니다. 또한, 믹스인을 사용하는 해당 컴포넌트에서도 충돌한 이름으로 메소드를 정의할 수 없습니다. 이를 회피하려면 사전에 엄격하게 컨벤션을 합의해야 하는데, 번거롭고 쉽지 않은 일입니다. 컴포저블이었다면 다른 컴포저블과 충돌하는 경우 이름을 변경할 수 있으므로 이러한 문제로부터 자유롭습니다.
  • 인수(argument)를 전달할 수 없습니다. 만약 여러 믹스인이 서로 상호작용하는 관계라면, 어떤 공유되는 값에 의존해야 하므로 결합도가 높아집니다. 컴포저블은 일반 함수처럼 다른 컴포저블에 인수를 전달할 수 있습니다.
  • 여러 믹스인을 동시에 사용할 때, 제각기 어떤 믹스인에 의해 주입된 것인지가 명확하지 않아 레퍼런스를 추적하거나 동작을 이해하기가 난해합니다.

Mixins이 어째서 바람직하지 않은지는 React 진영에서도 설명하고 있는데, 해당 글을 읽어보면 위와 비슷한 내용임을 알 수 있습니다.



타입스크립트 최적화/타입 추론 성능 개선

TypeScript와의 밀착도가 높아져 성능적으로 이득을 볼 수 있습니다. Vue 2에서는 타입스크립트를 제한적으로 지원하기 때문에, Options API의 경우에도 타입스크립트 특유의 타입 추론을 구현하기 위해 매우 비효율적인 방법을 사용하고 있다고 공식 문서에서는 설명하고 있습니다.

Compostion API는 통상적인 자바스크립트/타입스크립트의 코드의 형태로 구현되고 있는 만큼, 타입 추론이 효율적으로 수행될 수 있고, IDE에서도 구문 분석이 용이해 더 잘 지원되므로 개발자 경험이 크게 향상됩니다.



더 짧은 코드, 더 적은 오버헤드

<script setup> 구문으로 작성된 Composition API 코드는 Options API에 비해 상대적으로 더욱 잘 축소(minify)될 수 있습니다. 공식 문서의 설명에 따르면, 컴포넌트의 템플릿이 Composition API의 코드와 동일한 범위에 인라인 형태로 컴파일되고, 변수명도 보다 안전하게 단축할 수 있기 때문이라고 설명하고 있습니다.






React Hook과의 유사성

개인적으로 React 보다 Vue를 먼저 접해보았기 때문에, 'Composition APIReactHook으로부터 영향을 받았으며 그렇기 때문에 서로 닮았다'는 사실을 구체적으로 체감하지는 못했었습니다.

그러다 이후 React를 접하고, VueReact의 공식 문서를 각각 읽어보고 비교하면서, 구현 내용이나 구조는 다소 차이가 있으나 결국에는 서로 비슷한 문제의식을 공유하고 있음을 알 수 있었습니다.


  • 로직을 모듈화하고 관심사에 따라 분리하며, 유지보수성과 재사용성을 높임
  • 상용구(boilerplate)를 최소화해 코드량을 줄이고자 함
  • 구조 개선 및 효율화를 통한 성능 최적화
  • 불필요하게 어렵거나 난해한 요소를 줄여 진입 장벽, 러닝 커브를 낮추고 개발자 경험을 향상시키고자 함

여기서 발견할 수 있는 핵심은 '복잡하고 거대한 컴포넌트'의 문제를 해결하려 한다는 점입니다.

개발자가 의도/예상하지 못한 동작은 최소화하면서, 누가 보아도 이해하기 좋게끔 코드를 작성할 수 있는 가장 대표적인 방법이자 화두는 바로 '커다란 문제를 작은 문제로 분리하는 것'입니다. UI 개발에서 '컴포넌트'라는 개념/패턴 역시 이러한 목적에서 채택된 것으로, 커다란 UI를 작고 독립적이며 재사용 가능한 조각으로 쪼개어 구성하고자 한 결과물이라고 할 수 있습니다.

그런데 점점 상태 관리 로직들이 많아지고, 중복 코드가 증가하며, 서로 연관 없는 코드가 같은 메소드 내에서 결합한다거나 분해되지 않는 등 컴포넌트가 비대해지는 문제가 발생했고, '본래의 목적'이 다시금 흐려지는 상황이 발생하면서 이를 해소하고자 Composition API, Hook 같은 것이 등장한 것이라고 이해할 수 있습니다.

기술은 대체로 수렴 진화하는 경향성을 나타냅니다. 프론트엔드의 경우 변화의 주기가 상대적으로 짧고, 기존 패러다임과 방법론에 대한 의심과 회의도 빈번하며 지속적입니다. 그래서 제각기 파편화된 기술들이 가진 장점은 취하면서, 어떠한 문제와 결점은 개선하는 형태로 새로운 기술들이 빠르게 등장합니다.

실제로 Vue공식 문서를 보면, Composition APIReactHook에 대비해 약간의 시간차를 둔 후속적인 개념으로서 직접적인 영감의 원천이 되었다고 말하고 있으며, Vue 자체의 구조적인 특징 덕분에 몇 가지 차이점이나 개선점을 가지고 있다고 설명하고 있습니다. 일부 예를 들면 아래와 같습니다.


  • ReactHook은 컴포넌트가 업데이트될 때마다 호출되며, 호출 순서에도 민감하고 조건부일 수도 없습니다. 반면 Composition APIsetup() 함수는 초기 한 번만 호출되며 호출 순서와 무관하고 조건부일 수 있어 더 직관적입니다.
  • React에서 불필요한 컴포넌트 리렌더링 문제를 회피하기 위해 useMemo를 사용하는 것처럼, 직접 콜백 함수를 캐시(메모이제이션)할 필요가 없습니다. Vue의 반응형 시스템이 필요한 때에만 컴포넌트를 리렌더링하도록 제어하기 때문입니다.





참고 문서

profile
퇴고를 좋아하는 주니어 웹 개발자입니다.

0개의 댓글