Vue.js Composition API 깊이 이해하기: Options API와의 비교 및 실제 활용 방식

Vue.js는 그간 Options API를 중심으로 풍부한 생태계를 만들어 왔습니다. 그러나 복잡도가 높아지고 유지보수가 어려워지는 프로젝트 환경 속에서, 더 유연하고 확장 가능한 방식이 요구되면서 Composition API가 등장하게 되었습니다. 이 글에서는 Composition API의 핵심 개념부터 실제 컴포넌트 작성 방법, 그리고 Options API와의 실질적인 비교까지 폭넓게 다뤄보겠습니다.


Vue.js Composition API 깊이 이해하기

📚 목차


1. 서론: 왜 Composition API인가?

JavaScript 프레임워크의 세계는 끊임없이 변화합니다. Vue.js 역시 예외는 아닙니다. Vue 3의 등장과 함께 도입된 Composition API는 단순한 문법 변화 그 이상입니다. 개발자들이 코드의 가독성과 재사용성을 높이면서도 복잡한 상태 관리를 효율적으로 처리할 수 있도록 돕는 철학적 전환점이라 할 수 있습니다.

기존의 Options API는 초기에 배우기 쉽고 직관적이라는 장점이 있었지만, 점점 기능이 많아지고 컴포넌트가 커질수록 로직 간의 분리가 어려워지고 유지보수가 복잡해지는 단점이 부각되기 시작했습니다. 반면 Composition API는 기능 단위로 코드를 나누어 관리할 수 있어 코드의 명확성과 재사용성을 획기적으로 향상시켜 줍니다.

이 글에서는 Composition API가 왜 등장하게 되었는지 그 맥락을 짚어보고, 기존의 Options API와 어떻게 다르며, 어떤 방식으로 실무에 활용될 수 있는지를 깊이 있게 다루어 보겠습니다. 이로써 여러분이 Vue.js 프로젝트를 설계하고 구현하는 데 있어 보다 전략적인 선택을 할 수 있도록 도와드리고자 합니다.


2. Options API와 Composition API 개요

Vue.js를 처음 접한 많은 개발자들이 Options API를 통해 Vue의 매력을 느끼곤 합니다. Options API는 data, methods, computed, watch, props 등의 옵션을 분리된 구조로 명확히 정의할 수 있어 직관적인 구성이 장점이었습니다. 각 섹션에 기능을 배치함으로써, 초보자에게는 마치 사전처럼 코드를 참조할 수 있는 장점이 있었죠.

하지만 프로젝트의 복잡도가 올라가고, 하나의 컴포넌트 안에 여러 로직이 섞이기 시작하면 문제가 발생합니다. 서로 연관된 로직이 datamethods, watch 등 다양한 곳에 흩어져 관리되어야 하기 때문에, 유지보수와 테스트가 어려워지고 협업 시 혼란도 커질 수 있습니다.

이런 한계를 극복하고자 Vue 3에서는 Composition API가 도입되었습니다. Composition API는 setup() 함수 내부에 모든 논리를 구성하며, 필요한 기능을 기능 단위로 정의하고 불러올 수 있습니다. 이는 마치 ‘옵션’ 중심의 조립식 방식에서 ‘기능’ 중심의 모듈화 방식으로 사고의 전환을 요구하는 것입니다.

Composition API의 가장 큰 특징은 로직의 응집도를 높이는 데 있습니다. 즉, 관련된 기능은 하나의 함수나 파일로 구성하고, 이를 재사용 가능하게 만든다는 점에서 매우 유용합니다. 뿐만 아니라 TypeScript와의 호환성이 좋아 정적 타입 환경에서도 개발 효율을 끌어올릴 수 있는 기반이 됩니다.

Options API와 Composition API 개요

즉, 두 API는 단순히 문법적 차이로 나뉘는 것이 아니라, 프로그래밍 패러다임의 차이로 이해해야 합니다. 다음 장에서는 이 두 API의 차이를 실제 코드로 비교해 보면서 그 차이를 직접 체감해 보겠습니다.


3. 코드로 비교해보는 차이점

이제 실제 코드 예제를 통해 두 API 방식이 어떤 차이를 갖는지 비교해 보겠습니다. 가장 기본적인 Todo 리스트 컴포넌트를 기준으로, 동일한 동작을 Options API와 Composition API 방식으로 각각 구현해 보겠습니다.

Options API 방식

export default {
  data() {
    return {
      todos: [],
      newTodo: ''
    }
  },
  methods: {
    addTodo() {
      if (this.newTodo.trim()) {
        this.todos.push(this.newTodo.trim());
        this.newTodo = '';
      }
    }
  }
}

Options API는 datamethods를 명확히 분리하여 정의합니다. 각 기능이 어느 위치에 있는지 명확해서 초보자에게는 익숙하고 배우기 쉽지만, 로직이 복잡해질 경우 기능 간 응집도가 떨어지고 유지보수가 어려워집니다.

Composition API 방식

import { ref } from 'vue';

export default {
  setup() {
    const todos = ref([]);
    const newTodo = ref('');

    const addTodo = () => {
      if (newTodo.value.trim()) {
        todos.value.push(newTodo.value.trim());
        newTodo.value = '';
      }
    }

    return {
      todos,
      newTodo,
      addTodo
    }
  }
}

Composition API에서는 setup() 함수 내에서 모든 상태와 함수를 선언합니다. ref를 사용하여 반응형 상태를 생성하고, addTodo 함수도 해당 스코프 내에서 정의하여 반환합니다. 관련된 로직이 하나의 함수 내부에 응집되어 있어 훨씬 모듈화된 형태로 관리할 수 있으며, 함수 단위로 분리하거나 테스트하기도 쉬워집니다.

또한, Composition API는 재사용성을 극대화할 수 있다는 큰 장점을 지닙니다. 예를 들어, 여러 컴포넌트에서 사용할 수 있는 로직을 Composition Function으로 분리하여 필요할 때마다 가져와 쓸 수 있습니다. 이는 Vue 컴포넌트를 마치 함수형 프로그래밍처럼 구성할 수 있도록 해줍니다.

다음 섹션에서는 Composition API의 핵심 요소들인 ref, reactive, computed, watch 등 각각이 어떤 역할을 하며 어떻게 활용되는지를 자세히 다뤄보겠습니다.


4. Composition API의 핵심 개념

Composition API의 진가는 그 핵심 개념들을 정확히 이해할 때 드러납니다. 여기서는 Vue 3에서 도입된 주요 기능들이 어떤 목적과 철학을 가지고 있으며, 실제 컴포넌트 구성에서 어떤 방식으로 활용되는지를 구체적으로 살펴보겠습니다.

1) setup() 함수

Composition API의 시작점이자 모든 논리의 중심은 setup() 함수입니다. 이 함수는 컴포넌트가 생성되기 전에 호출되며, Composition API 방식의 상태 선언과 로직 정의가 이루어지는 곳입니다.

export default {
  setup() {
    // Composition API의 진입점
  }
}

setup()에서 반환된 값은 템플릿에서 바로 사용할 수 있습니다. 이는 Options API에서의 data(), methods 등을 하나로 통합하는 구조라 볼 수 있습니다.

2) ref와 reactive

Vue 3에서는 반응형 상태를 만들기 위해 ref()reactive()라는 두 가지 유틸리티를 사용합니다. 각각은 다소 목적이 다릅니다.

  • ref: 단일 값(문자열, 숫자 등)을 반응형으로 만들 때 사용
  • reactive: 객체 전체를 반응형으로 만들 때 사용
const count = ref(0);
const user = reactive({ name: 'Amy', age: 30 });

ref는 내부 값에 접근할 때 항상 .value를 사용해야 하지만, reactive는 일반 객체처럼 사용할 수 있습니다. 하지만 reactive는 객체의 구조가 복잡할수록 toRef, toRefs 등을 함께 사용하는 것이 일반적입니다.

3) computed와 watch

Composition API에서도 계산된 속성과 감시자는 그대로 존재하며, 훨씬 명확하고 선언적인 방식으로 활용됩니다.

const fullName = computed(() => `${firstName.value} ${lastName.value}`);

watch(count, (newVal, oldVal) => {
  console.log(`count가 ${oldVal}에서 ${newVal}로 변경되었습니다`);
});

computed는 선언적이고 의존성이 명확하여 캐싱까지 자동으로 처리되는 반면, watch는 보다 imperative하게 동작하여 외부 API 호출, 로깅, 조건 분기 등에 더 적합합니다.

4) provide와 inject

Vue에서는 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하기 위해 props를 사용하지만, 계층이 깊어질 경우 provide/inject를 통해 전역적으로 공유할 수 있습니다.

provide('theme', ref('dark'));

// 자식 컴포넌트에서
const theme = inject('theme');

이 방식은 글로벌 상태를 간단히 전달하거나 테마, 언어 설정과 같은 맥락 정보를 공유할 때 유용합니다.

5) Composition Function

Composition API의 진정한 강점은 로직을 재사용 가능한 형태로 추출할 수 있다는 점입니다. 이때 사용되는 것이 바로 Composition Function입니다. 이는 특정 기능을 독립적인 함수로 만들어, 여러 컴포넌트에서 불러다 쓸 수 있게 해 줍니다.

// useCounter.js
import { ref } from 'vue';

export function useCounter() {
  const count = ref(0);
  const increment = () => count.value++;
  return { count, increment };
}

이처럼 Composition Function은 Vue 컴포넌트를 함수형으로 관리할 수 있는 기반이 되어주며, 로직의 관심사 분리(Separation of Concerns)를 자연스럽게 구현할 수 있습니다.

다음 단락에서는 Composition API의 강점과 실제 사용 시 마주할 수 있는 단점들을 구체적으로 비교 분석하며, 이를 고려한 실전 적용 전략에 대해 이야기해보겠습니다.


5. Composition API의 장점과 단점

Composition API는 Vue 3의 중심 철학이 반영된 기능으로, 단순한 문법적 변화에 머무르지 않고 애플리케이션 구조 전체에 영향을 줍니다. 그렇기 때문에 장점과 단점을 명확히 이해하고 상황에 따라 적절히 선택하는 것이 중요합니다.

Composition API의 주요 장점

  • 1. 관심사의 분리(Separation of Concerns)
    여러 기능이 하나의 컴포넌트에 뒤섞여 있던 기존 Options API와 달리, Composition API는 로직을 기능 단위로 모듈화하여 응집도를 높입니다. 예를 들어, 폼 처리, 데이터 페칭, 상태 관리 등을 각각의 Composition Function으로 나누면 유지보수성과 확장성이 높아집니다.
  • 2. 코드 재사용성과 테스트 용이성
    공통 로직을 Composition Function으로 추출해 다양한 컴포넌트에서 재사용할 수 있으며, 각 함수는 독립적으로 테스트가 가능합니다. 이는 특히 대규모 프로젝트나 팀 개발 환경에서 높은 생산성을 보장합니다.
  • 3. TypeScript와의 자연스러운 통합
    Composition API는 TypeScript 기반 프로젝트와의 통합이 자연스럽고 명확합니다. refcomputed 같은 API는 명시적인 타입 선언이 가능하여 정적 분석과 자동완성 기능을 극대화할 수 있습니다.
  • 4. 복잡한 상태 관리의 명료화
    복잡한 상태를 다룰 때 각 상태를 명시적으로 선언하고, 이를 관리하는 로직을 가까운 위치에 배치할 수 있습니다. 이는 디버깅과 로직 추적을 훨씬 용이하게 해 줍니다.

Composition API의 단점과 주의사항

  • 1. 러닝 커브와 진입 장벽
    Vue를 처음 접하는 사용자에게는 setup(), ref, reactive 등 새로운 개념들이 다소 부담스럽게 느껴질 수 있습니다. 초보자에게는 Options API가 여전히 더 직관적입니다.
  • 2. 코드의 구조가 복잡해질 수 있음
    모든 로직을 함수 내부에 작성하다 보면, 적절한 Composition Function으로 분리하지 않을 경우 오히려 코드가 장황하고 추적하기 어려워질 수 있습니다. 이는 잘못된 설계의 위험을 내포합니다.
  • 3. 템플릿과의 연결 지점이 불명확해질 수 있음
    setup() 내부에서 선언한 수십 개의 변수와 함수가 return으로 한 번에 템플릿에 전달되다 보면, 어떤 것이 실제 템플릿에서 사용되는지 명확하게 파악하기 어려울 수 있습니다.

실제 현업 적용 시 유의할 점

Composition API는 뛰어난 유연성과 강력한 기능을 제공하지만, 무분별하게 사용하기보다는 프로젝트의 규모, 팀의 역량, 그리고 유지보수 관점에서 신중히 접근해야 합니다. 특히 Vue에 익숙한 기존 개발자라도 학습곡선을 고려한 리팩토링 전략이 필요합니다.

Vue 팀 또한 Composition API를 Options API의 ‘대체’가 아닌 ‘보완재’로 보고 있습니다. 필요에 따라 두 방식을 혼용하는 것이 가능하며, 실제 많은 오픈소스 프로젝트나 기업 프로젝트에서 이런 방식이 활용되고 있습니다.

다음 단락에서는 어떤 상황에서 Composition API를 선택하는 것이 효과적인지에 대한 전략과 기준을 구체적으로 살펴보겠습니다.


6. 언제 Composition API를 선택해야 할까?

Composition API와 Options API는 서로 대립하는 개념이 아닙니다. Vue 3는 두 방식 모두를 지원하며, 개발자는 프로젝트의 성격과 팀의 역량에 따라 유연하게 선택하거나 혼용할 수 있습니다. 이 단락에서는 어떤 조건에서 Composition API가 특히 효과적인지를 중심으로 전략적 선택 기준을 정리해 보겠습니다.

1) 프로젝트 규모에 따른 판단

  • 소규모 프로젝트: 빠르게 구현하고 유지보수가 크지 않은 경우라면 Options API가 더 직관적이며 빠른 생산성을 가져옵니다. 특히 초보자나 디자이너 중심의 협업 프로젝트라면 간결한 구조가 유리할 수 있습니다.
  • 중대형 프로젝트: 컴포넌트 간 공유 로직이 많고 상태 관리가 복잡한 경우에는 Composition API가 확연한 장점을 발휘합니다. 기능 단위로 로직을 모듈화하여 유지보수를 용이하게 하며, 팀 협업 시 역할 분담도 수월합니다.

2) 팀의 역량과 경험

팀 내 Vue 사용 경험이 적고 입문자 중심이라면 Options API의 명확한 구조가 학습에 더 적합할 수 있습니다. 그러나 함수형 프로그래밍, React Hook 패턴, TypeScript 등에 익숙한 팀이라면 Composition API의 유연성과 구조화된 코드 작성 방식이 훨씬 효과적입니다.

3) 테스트 및 확장성 요구 여부

TDD(Test Driven Development), 모듈 기반 아키텍처, 고급 상태 관리 전략 등을 염두에 두고 있다면 Composition API가 큰 이점을 제공합니다. Composition Function으로 로직을 나누면 테스트 코드 작성이 용이하며, 재사용성 또한 뛰어납니다.

4) Options API와 Composition API의 혼용 전략

Vue 3에서는 하나의 컴포넌트 내에서 두 API를 혼용할 수 있습니다. 예를 들어, 기존 Options API로 작성된 컴포넌트에 특정 고급 기능만 Composition API 방식으로 추가하거나, 외부 Composition Function을 불러오는 방식도 가능합니다.

export default {
  data() {
    return {
      title: 'Hello'
    };
  },
  setup() {
    const user = useUser(); // 외부 Composition Function 활용
    return { user };
  }
}

이처럼 상황에 따라 두 방식을 병행하여 점진적으로 Composition API로 전환할 수도 있으며, 팀 전체의 안정적인 적응을 유도할 수 있습니다.

결론적으로, Composition API는 ‘언제나 써야 하는 새로운 방식’이 아니라, ‘적절히 활용할 수 있는 도구’로 접근하는 것이 중요합니다. API의 선택은 기술 자체보다는 프로젝트의 전략, 팀의 현실, 장기적인 유지보수 계획에 따라 결정되어야 합니다.

다음 단락에서는 Composition API를 활용한 실제 컴포넌트 설계 사례를 통해 이론이 어떻게 실무에 구현되는지를 살펴보겠습니다.


7. 실전 예시: 실용적인 컴포넌트 구조 설계

Composition API의 진정한 가치는 실제 프로젝트에서 그것을 어떻게 구조화하고 활용하는지에 달려 있습니다. 이번 섹션에서는 실무에서 자주 마주치는 패턴들을 중심으로, Composition API를 통해 어떻게 명확하고 재사용 가능한 컴포넌트를 구성할 수 있는지 사례를 통해 설명합니다.

1) 폼 처리 로직 분리

폼 검증이나 데이터 처리와 같은 반복적인 로직은 Composition Function으로 분리함으로써 재사용성과 유지보수를 높일 수 있습니다.

// useForm.js
import { ref } from 'vue';

export function useForm() {
  const form = ref({ name: '', email: '' });
  const resetForm = () => {
    form.value = { name: '', email: '' };
  };
  return { form, resetForm };
}

이러한 방식으로 구성된 useForm은 여러 폼 컴포넌트에서 손쉽게 가져와 사용할 수 있습니다.

2) API 통신과 비동기 처리

API 요청은 상태 변화와 오류 처리가 필수적인 복합 로직입니다. 이를 Composition Function으로 분리하면 다양한 컴포넌트에서 반복 구현을 피할 수 있습니다.

// useFetchUser.js
import { ref } from 'vue';
import axios from 'axios';

export function useFetchUser(userId) {
  const user = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const fetchUser = async () => {
    loading.value = true;
    try {
      const response = await axios.get(`/api/users/${userId}`);
      user.value = response.data;
    } catch (err) {
      error.value = err;
    } finally {
      loading.value = false;
    }
  };

  return { user, loading, error, fetchUser };
}

이처럼 API 로직이 독립적으로 분리되어 있을 경우, 다양한 페이지나 컴포넌트에서 재사용할 수 있을 뿐 아니라, 에러 처리 및 로딩 상태도 일관되게 관리할 수 있습니다.

3) 전역 상태 관리 (간단한 예제)

Composition API는 Vuex 없이도 전역 상태를 관리할 수 있도록 해줍니다. 다음은 간단한 글로벌 카운터 상태를 Composition Function으로 구현한 예입니다.

// useCounterStore.js
import { ref } from 'vue';

const count = ref(0);
const increment = () => count.value++;
const decrement = () => count.value--;

export function useCounterStore() {
  return { count, increment, decrement };
}

useCounterStore는 전역에서 import 하여 사용할 수 있으며, React의 Context와 유사한 패턴으로 Vuex 없이도 간단한 글로벌 상태 처리가 가능합니다. 물론 규모가 크고 복잡한 상태 관리가 필요한 경우에는 Pinia와 같은 상태 관리 라이브러리를 Composition API와 함께 사용하는 것이 좋습니다.

구조 설계 시 주의할 점

  • 관심사 분리의 기준을 명확히 하라: API 호출, 상태 관리, 유효성 검증 등 역할에 따라 Composition Function을 분리해야 합니다.
  • 지나친 추상화를 경계하라: 모든 로직을 외부 함수로 분리하면 오히려 코드의 가독성이 떨어질 수 있습니다. “적절한 추상화”가 핵심입니다.
  • 명명 규칙을 일관되게 유지하라: useXxx 패턴은 Composition Function임을 직관적으로 알 수 있게 해 줍니다.

다음 단락에서는 이 모든 내용을 종합하여, Composition API가 Vue 생태계에서 어떤 의미를 가지며, 개발자에게 어떤 방향성을 제시하는지를 마무리하며 살펴보겠습니다.


8. 결론: Vue.js 생태계에서 Composition API의 의미

Vue.js는 오랫동안 Options API를 중심으로 쉬운 진입성과 강력한 양방향 바인딩이라는 장점을 바탕으로 성장해왔습니다. 하지만 웹 애플리케이션이 점점 더 복잡해지고 규모가 커짐에 따라, 보다 구조화된 개발 방식이 요구되었고, 그 해답이 바로 Composition API였습니다.

Composition API는 단지 “새로운 방식”이 아니라, 변화하는 개발 환경에 맞춘 “필요한 진화”입니다. 관심사 분리, 코드 재사용성, 테스트 용이성, TypeScript 친화성 등은 단순한 문법상의 개선을 넘어, 애플리케이션 아키텍처에까지 영향을 미치는 중요한 요소입니다.

하지만 Vue는 여전히 Options API도 공식적으로 지원하고 있으며, 실제 현업에서도 두 방식을 혼용하는 전략이 유효하게 사용되고 있습니다. 중요한 것은 어떤 API가 더 ‘좋다’는 이분법적 사고가 아니라, 프로젝트의 성격과 팀의 상황에 따라 ‘더 적절하다’는 유연한 판단입니다.

앞으로의 Vue 개발은 아마도 점진적으로 Composition API로 중심축이 이동할 가능성이 높습니다. 하지만 이 전환은 강요가 아닌 선택이며, 그 선택은 곧 설계의 깊이와 품질로 이어집니다.

이제 Vue.js로 무엇을 어떻게 만들 것인가를 고민할 때, 단순히 기능 구현을 넘어서 구조와 방향을 설계하는 개발자가 되어 보세요. Composition API는 그 여정을 보다 체계적이고, 우아하게 만들어 줄 것입니다.

기술은 선택이고, 그 선택은 곧 설계입니다. 당신의 Vue는 어떤 구조를 갖고 있나요?

댓글 남기기

Table of Contents