실무에서 Vue3를 이용해 아토믹 디자인 패턴을 적용한 공통 UI 컴포넌트 셋을 개발해고 있다.
원래 나는 React 개발자 였으므로, hooks 아키텍처에 익숙하기에,
대부분의 업무로직과 상태가 결합된 기능들을 composable로 뽑아내서 작업하려 하고 있다.
그런데 Vue3은 Vue2의 하위호환을 위한 Option API로 작성된 코드들도 혼용하는 경우도 꽤 보인다.
나는 Vue2를 안해보았기 때문에 새로운 API를 만나면 헷갈린다... @.@
그렇다면 Composition API와 Composable은 무엇이며 Option API와 무슨 차이가 있을까?
Composition API
Option API는 컴포넌트 인스턴스에 프로퍼티와 메서드를 추가하기 위한 수단이다.
또한 Composition API의 라이프사이클과 약간 다른 명세를 지니고 있다.
이는 UI 중심이 아닌 객체(컴포넌트 인스턴스) 중심의 사상을 반영하고 있다.
(물론 내가 파악하기에 그렇다는 것이다. Option API는 써본적이 없다.)
https://vuejs.org/api/options-lifecycle.html
Vue3 플레이그라운드의 기본 예제를 Option API(예제)로 바꾸어 보았다. Sfc 인스턴스의 this를 통해 데이터에 접근 가능하다.
const __sfc__ = {
data() {
return { msg:"hello world" }
},
created() {
console.log(this.$data.msg) // "hello world"
}
}
Composition API는 객체에 바인딩 된 프로퍼티, 메서드를 벗어나 해당 데이터와 기능을 이용하는 로직들을 재사용하기 위한 수단이다.
기존에 this binding으로 접근하던 데이터와 메서드, 라이프사이클 훅이 setup 메서드 안으로 이동하였다.
const __sfc__ = {
__name: 'App',
setup(__props) {
const msg = ref('Hello World!')
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("h1", null, _toDisplayString(msg.value), 1 /* TEXT */),
_withDirectives(_createElementVNode("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ((msg).value = $event))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, msg.value]
])
], 64 /* STABLE_FRAGMENT */))
}
}
}
이는 composition API를 이용해 setup 함수의 컨텍스트 내에 데이터와 함수를 자유롭게 넣고 뺄 수 있다는 것을 의미한다.
Composable은 Composition API를 이용하여 리액티브하고 재사용 가능한 로직을 구현하는 함수이다.
Composition API를 이용해 컴포넌트 인스턴스 밖으로 뽑아낸 데이터, 메서드 등을 조합하여 새로운 stateful한 로직을 만들면
그게 Composable이다.
- stateful한 데이터를 활용하는 업무로직을 하나로 뽑아낼 때 사용
- lifecycle과 연관된 기능들을 캡슐화하여 사용자가 라이프싸이클에 대해 알 수 없도록 하기
Composable caveat
요새는 보통 <script setup>(https://vuejs.org/api/sfc-script-setup.html)을 이용해 컴포넌트를 개발한다.
이는 setup 메서드에 대한 컴파일 타입 문법 설탕인데, 다음과 같은 장점이 있다.
- 더 적은 상용구로 더 간결한 코드
- TypeScript만 사용하여 prop 및 emit 이벤트를 선언하는 기능
- 런타임 성능 향상(템플릿이 중간 프록시 없이 동일한 범위의 렌더링 함수로 컴파일됨)
- 더 나은 IDE 타입 추론 성능(랭귀지 서버가 코드에서 타입을 추출하는 작업 감소)
2번처럼 보통 타입스크립트를 사용하게 되면 defineProps, defineEmits 함수를 사용하는데,
이 친구들은 composition API가 아니다! (정확하게는 컴파일러 매크로;defineprops-defineemits)
따라서 컴포저블로 뽑아내면 오류가 발생한다.
결론 : Why Composable?
아직까지 왜 Composition API로 전환해야 하는지 이해하지 못하는 기존 개발자들이 많은것 같다.
이는 공식문서 FaQ에서 설명하고 있다.
https://vuejs.org/guide/extras/composition-api-faq.html#better-logic-reuse
예를 들어 파일 탐색기를 Vue로 구현한다고 생각하면, 아래와 같은 관심사가 존재한다.
- 현재 폴더 상태 추적 및 내용 표시
- 폴더 탐색 처리(열기, 닫기, 새로 고침...)
- 새 폴더 생성 처리
- 즐겨찾기 폴더만 표시
- 숨겨진 폴더 표시 전환
- 현재 작업 디렉토리 변경
Vue CLI 내부에서 사용하는 Option API로 개발한 파일 탐색기 소스(original version)를 관심사에 따라 색칠하면 아래와 같이 된다.
이는 컴포넌트 인스턴스라는 너무 큰 단위로 객체의 단위를 잡는데서 오는 문제점이다.
컴포넌트의 데이터와 행위 중심으로 생각하다보니 이와 같은 문제가 발생한다.
- 동일한 논리적 문제를 처리하는 코드가 파일의 다른 부분에 있는 다른 옵션으로 분할되어 있다.
- 수백 줄 길이의 컴포넌트에서 하나의 논리적 문제를 이해하고 탐색하려면 파일을 지속적으로 위아래로 스크롤해야 한다.
- 자바로 개발하다 보면 너무 쉽게 만나는 문제다.
- 논리적 문제를 재사용 가능한 유틸리티로 추출하려는 경우 파일의 다른 부분에서 올바른 코드 조각을 찾아 추출하는 데 오래걸린다.
- 리팩토링의 기회가 줄어든다.
같은 컴포넌트를 Composition API로 리팩토링한 결과다(refactor into Composition API)
컴포넌트의 속박에서 벗어나면 데이터와 기능이 더 작은 관심사를 달성하기 위해 뭉칠 수 있다.
물론 이 또한 객체지향 적으로 생각할 수 있으나, 데이터 중심이 아니라 외부에 노출하는 기능 중심으로 그룹화 해야 좋다
- 이제 동일한 논리적 문제와 관련된 코드를 함께 그룹화할 수 있다.
- 더 이상 특정 논리적 문제에 대해 작업하는 동안 다른 옵션 블록 사이를 이동할 필요가 없다.
- 또한 이제 코드를 추출하기 위해 더 이상 코드를 뒤섞을 필요가 없으므로 최소한의 노력으로 코드 그룹을 외부 파일로 이동할 수 있다
- 높은 리팩토링의 가능성은 코드베이스의 장기적 유지보수를 위한 핵심이다.
Vue를 좋아하는 개발자들은 기본적으로 자바와 같은 객체지향 세계에서 온 사람들이 많고,
클래스나 인터페이스와 같이 이미 정해진 컴포넌트(Service ,Controller)에 기능과 데이터를 할당하는 것을 좋아한다.
만약 Composition API를 사용하게 된다면, 작은 단위로 여러개의 함수를 추출할 수 있게 되지만,
클래스와 같은 정형적인 프레임을 찾기 어려우며, 덜 체계적인 것 같다고 오해할 수 있다.
이에 대해 Vue3 공식문서에서는 다음과 같이 답변한다.
- 위와 같은 정형적인 guard rail을 희생하는 대가로 일반 js 코드를 작성하는 것처럼 stateful한 로직을 작성할 수 있다.
- 이는 당신이 javascript 코딩을 잘하면 체계화 또한 잘할 수 있다는 것이다.
- Option API를 사용하면 컴포넌트 중심으로 기능과 데이터를 다 때려박으면 되니 체계화에 대해 생각을 덜 할 수 있어서 사람들이 좋아한다.
- 이것이 Controller 객체와 Service 객체가 커지는 이유다
- 하지만 규정된 코드 패턴이 리팩터링의 가능성을 손상시킬 수 있다.
- 따라서 기능 중심의 작은 캡술화를 허용하는 Composition API는 장기적으로 반드시 win이다.
- Composition API는 Option API의 사용사례를 100% 커버할 수 있다.
- props, emits, name, inheritAttrs만 제외하고, stateful한 로직은 100% 대체 가능
- script setup을 사용하면 inheritAttrs만 필요함
믹스인은 왜 안되나요?
Vue3 창시자인 Evan You의 말을 인용하면
- 해당 프로퍼티가 어떤 믹스인에서 제공되는 지 암시적임
- 네임스페이스 충돌 문제
- 타입스크립트 적용이 매우 어렵다
- 중복 속성을 가진 타입 두개의 타입 머징을 처리해보셨나요?
mixin은 리액트가 hook 아키텍처로 전환하는 바탕이 된 요소중 하나임.
에반 유에 따르면 리액트는
훅 아키텍처 이전에 mixin 기능을 지원 중단하고 HOC의 사용을 권장했다 함.
나는 리액트 16 이후부터 훅으로 개발해서 잘 모르겠음 @.@
한 마디로 안티패턴, 레거시 패턴이므로 쓰지 말자
HOC(high order component)는요?
이 또한 Evan You의 설명을 첨언하자면
- HOC가 중첩될 경우 믹스인 1번과 유사한 문제가 발생; 어떤 prop이 어떤 HOC에서 제공되는지 알 수 없음
- HOC가 중첩될 경우 믹스인 2번과 유사한 문제가 발생; prop 이름 중복
해당 패턴은 Renderless Component로 대체할 수 있음
Renderless Component 패턴은 추가 인스턴스화 비용을 지불하는 것으로 앞의 두 가지 문제를 해결한다
- Injection Source Problem
- namespace collision
좀 더 자세한 설명은 adam wathan이 작성한 글을 보자
Renderless Component 패턴에서 추가 인스턴스 비용을 제거하면 Composable 형태가 된다.
- 물론 유상태 컴포넌트와 무상태 컴포넌트를 확실하게 분리해서 사용하고 싶다면 컨테이너 컴포넌트를 쓸 것이다
- 하지만 Vue는 탬플릿 영역이 따로 존재하므로 비즈니스가 없는 UI 컴포넌트 또한 무상태로 둘 필요는 없는것 같다.
컴포저블의 장점은 단순 함수이므로 타입스크립트 적용이 매우 쉬움
참고 : useFetch 컴포저블 예시
Evan You가 직접 코딩한 예제임
'FrontEnd' 카테고리의 다른 글
[번역] Vite 플러그인 만들기 (0) | 2022.12.19 |
---|---|
[Typescript] 객체 함수와 타입스크립트 (0) | 2022.12.18 |
[Vue3] watch vs watchEffect 사용사례 비교 (0) | 2022.12.18 |
[React 디자인 패턴] Renderless Component 패턴 (0) | 2022.12.17 |
Svelte 코드 컴파일러는 어떻게 동작할까? (0) | 2022.12.17 |