
FE 프로젝트 버전 업그레이드 작업을 대비하기 위한 두번째 글
(첫번째는 Vite)
Vue2 와 Vue3 의 차이점이 뭘까
현재 Vue 2.7 + Composition API를 사용하는 중이라 개발할 때 크게 체감하는 부분은 없을 것 같다.
다만 Vue3의 코어 개념 한 가지를 꼽아보자면 역시 Composable 아닐까
결론부터 말하면
컴포넌트가 커질수록 로직이 찢어져 관리가 불가능해졌기 때문이다.
Vue3 설계 철학과 Composable을 정확하게 이해해보자.
Composable은 Composition API로 만든 로직 재사용 함수이다.
일반적으로 useXxx 형태의 이름을 가진다.
// useCounter.ts
export function useCounter() {
const count = ref(0);
function increase() {
count.value++;
}
return { count, increase }
}
컴포넌트에서는 이렇게 사용한다.
// Count.vue
setup() {
const { count, increase } = useCounter();
return { count, increase }
}
겉보기에는 단순한 함수처럼 보이지만, 일반 유틸 함수와 중요한 차이가 있다.
| 구분 | Composable | 일반 유틸 함수 (Util Function) |
|---|---|---|
| 목적 | Vue 로직 재사용 | 순수 기능 재사용 |
| Vue 의존성 | Vue Composition API 사용 (ref, watch 등) | Vue와 관련 없음 |
| 상태(State) | 반응형 상태 보유 가능 | 상태 보유하지 않음 (보통 stateless) |
| 반응성(Reactivity) | reactive / ref 자동 추적 | 없음 |
| 생명주기 연동 | 컴포넌트 lifecycle과 함께 동작 | lifecycle 개념 없음 |
| 컴포넌트와 관계 | 컴포넌트의 일부처럼 동작 | 완전히 독립적 |
| 실행 시점 | setup() 내부에서 사용 | 어디서든 호출 가능 |
| 상태 공유 | 가능 (파일 상단 ref 사용 시) | 기본적으로 불가능 |
| 주 사용 용도 | UI 로직, 데이터 흐름, 상태 관리 | 계산, 포맷팅, 변환 |
| 예시 | useSearch(), useAuth() | formatDate(), sum() |
| 테스트 방식 | Vue 환경 고려 필요 | 순수 함수 테스트 가능 |
| 구조적 역할 | 아키텍처 구성 요소 | 단순 헬퍼 함수 |
// Composable
export function useCounter() {
const count = ref(0);
function increase() {
count.value++;
}
return { count, increase }
}
// Util function
export function sum(a: number, b: number) {
return a + b;
}
Composable은 Vue 반응형시스템 위에서 동작하는 상태를 가진 로직 모듈이고,
Utility Function은 프레임워크와 무관한 순수 기능 함수이다.
Vue2의 Options API는 구조적으로 이런 형태였다.
export default {
data() {},
methods: {},
computed: {},
watch: {}
}
이러한 구조는 기능 단위가 아니라 옵션 단위로 코드가 나뉜다는 문제점이 있다.
ex) "검색 기능"이라는 단일 기능에 대해 연관된 상태, 동작, 반응형 동작(computed, watch)이 한 곳에 모여있는게 아니라 여러 곳에 흩어져 작성된다.
// 다 같은 검색 기능인데 흩어져 작성된다
├ data
├ watch
├ methods
└ mounted
이런 기능들이 하나의 컴포넌트에 여러개있다면?🥴
컴포넌트가 커질수록 로직은 파편화되고
Vue 팀은 이 문제를 Logic Fragmentation이라고 설명한다.
Composition API는 코드를 기능 기준으로 묶는다.
setup() {
const search = useSearch();
}
// 검색 기능과 관련된 것들은 아래 함수 하나에 묶인다
└ useSearch()
| Options API | Composition API | |
|---|---|---|
| 분리 기준 | 옵션(data, methods 등) 기준 분리 | 기능(로직) 기준 분리 |
| 컴포넌트 규모 | 로직이 모이며 비대해짐 | 로직이 분리되어 가벼움 |
| 재사용 전략 | mixin | composable |
Vue는 로직 파편화 문제를 해결하기 위해 Composition API 를 선택했고 이 과정에서 composable이 등장했다.
Composition API는 함수 기반 구조라서 함수형 프로그래밍처럼 보인다.
하지만 Vue 공식 문서에는
Composition API는 함수형 프로그래밍이 아니다.
라고 명확하게 써져있다.

// Composition API (mutable)
const count = ref(0);
count.value++; // 상태를 직접 변경
// 함수형 프로그래밍 (immutable)
const increment = count => count + 1;
const newCount = increment(count); // 새 값 생성
Vue는 상태를 직접 변경한다.
이는 불변성을 강조하는 함수형 프로그래밍과 다르다.
상태는 기본적으로 독립적이다
const a = useCounter();
const b = useCounter();
a와 b는 서로 다른 상태를 가진다.
상태를 공유하는 방법은 없을까? 있다.
모듈 스코프에 상태를 생성하면 공유 상태가 된다.
// useAuth.ts
const user = ref(null);
export function useAuth() {
return { user }
}
useAuth()를 어디서 호출해도 user는 같은 ref 인스턴스를 가리킨다.
작은 범위의 전역 store처럼 동작한다.
Composable을 이해하고 보면
ue3에서 가장 체감되는 변화 중 하나(Composition API)는 단순히 새로운 문법이 아니라 로직을 분리하고 다시 구성하는 방식의 변화이다.
이전에는 .vue 파일 내부에 로직을 직접 작성했다면,
이제는 여러 composable을 조합하여 화면을 구성하는 조립 레이어(assembly layer)에 가까워졌다.
[정리]
나는 composable 조립 레이어로서 컴포넌트를 분리하고 있었는가🤔
반성하고 하나 또 배우며 마무리해본다.