JQuery 프로젝트에 VUE를 점진적으로 도입하기
VUE를 도입하고 싶거나, 레거시 때문에 망설여진다면
천보성 팀장
2018-10-23
편집자 주
각 코드에 대한 설명은 평어체로 서술함
JQuery로 작성된 프로젝트를 VUE로 한 번에 컨버팅하기는 어렵습니다. 신규 프로젝트가 아닌 이상 레거시를 기반으로 VUE 도입을 고민해야 하죠.
VUE와 JQuery는 궁합이 좋지 않습니다. JQuery 이벤트 처리와 VUE 이벤트 처리는 서로 충돌되며, 객체를 관리하는 방식 역시 접근 자체가 다릅니다.1) 그렇기 때문에 JQuery 레거시에 VUE를 도입하면서 고민할 부분이 많아집니다.
저는 브랜디에서 관리자 개발과 운영을 맡고 있습니다. 관리자를 한 번에 VUE로 변경하긴 어렵지만, VUE를 꼭 쓰고 싶었습니다. 처음엔 몇몇 페이지를 VUE로 변경하는 방법을 고민했지만, 관리자 특성상 메뉴에 대한 권한 제어, 메뉴 이동 시 생길 부자연스러움, 소스를 이중으로 관리하는 문제가 계속 생길 것이라 좋지 않다고 판단했습니다. 그래서 페이지 내의 일부분을 VUE로 변경하는 방법을 선택했습니다.
프로젝트에 VUE 구조를 넣는 방법은 강원우 과장님의 PHP Codeigniter 환경에서 VUE 사용해보기로 대체합니다. 글에 등장하는 구조는 강원우 과장님과 같이 설계했기 때문에 컨셉과 방식이 똑같습니다.
webpack을 이용하면 하위 호환성 문제를 피해갈 수 있지만 기존의 레거시 코드는 하위 호환성 문제에 그대로 노출되어 있습니다. 물론 하위 호환성을 고려해 코드가 작성된 레거시지만 VUE를 일부 도입할 때는 하위 호환성을 자동으로 처리해주지 않기 때문에 신경을 써야 합니다.
대표적으로 <script type="module">
과 같은 코드를 사용할 수 없는데요. IE 9이하 버전에선 인식이 안 되기 때문입니다. 같은 이유로 ES6 문법 역시 사용하지 않는 것이 좋습니다. 무엇보다 vue 확장자를 직접 사용할 수 없기 때문에 webpack에서 빌드된 js를 import하는 방식으로 사용합니다. 다만 webpack build되는 소스에서는 고려하지 않아도 됩니다.
웹버전 babel코드가 존재하지만 webpack build를 사용하는 것에 비해 제약이 너무 많아 테스트하다가 결국 사용하지 않기로 선택했습니다. 2)
1.컴포넌트로 구성하기
먼저 컨버팅할 기능을 선별했습니다. 여러 부분에서 자주, 그리고 공통적으로 사용하는 기능을 우선순위로 두었습니다. 그중 하나가 상품을 검색해 추가해주는 모달창입니다.
1-1 컴포넌트를 가져와서 화면과 연결하기
위에서 강원우 과장님의 글에 잘 소개되었다고 말했지만, 어느 정도의 설명이 필요하기 때문에 같은 내용을 한 번 더 소개하겠습니다. 최종 빌드된 스크립트 소스를 script로 연결하면서 해당 컴포넌트를 로딩할 수 있습니다.
/vue/productSearchModal/main.js
import Main from './main.vue'
Vue.component('product-search-modal', Main);
그리고 연결을 원하는 위치에 컴포넌트에 해당되는 태그를 넣고 스크립트를 열어 VUE 객체를 생성합니다.
/exhibition/best.php
<script src="/include/scripts/WEBPACK_VUE/productSearchModal/main.js"></script>
<div id="vue-modal">
<product-search-modal></product-search-modal>
</div>
<script>
// 상품 검색 모달 컴포넌트
var productSearchModalComp = new Vue({
el: '#vue-modal',
data: function() {
return {
}
},
methods: {
}
});
</script>
같은 컴포넌트를 한 화면에 여러 개 넣어야 한다면 여러 개의 컴포넌트 태그를 넣고 스크립트로 연결해주면 됩니다. 컴포넌트 js는 한 번만 로딩되기 때문에 용량적인 이득을 볼 수 있을 겁니다.
1-2.외부에서 메소드 연결하기
상품 검색 모달은 상품추가 버튼을 클릭할 때 나타나야 합니다. 컴포넌트내 상품모달을 나타나게 하는 method를 show() 라고 정의했습니다. 이렇게 하면 버튼을 눌렀을 때 컴포넌트의 methods.show를 실행하면 될 겁니다.
/vue/productSearchModal/main.vue
<script>
import Vue from 'vue'
export default {
methods: {
show: function () {
$(this.$el).modal('show');
this.productList = [];
this.total = 0;
}
}
}
</script>
모달을 보여주는 show함수를 추가했다.
컴포넌트를 접근하기 위해선 상품 검색 모달을 가져올 때 받아둔 productSearchModalComp 변수에 접근하면 됩니다. productSearchModalComp는 VUE 객체이기 때문에 $refs나 $emit등 정의 함수를 모두 사용할 수 있습니다.
상품 검색 모달 컴포넌트는 productSearchModalComp의 child 컴포넌트가 되기 때문에 접근하면 methods.show에 접근이 가능합니다. 좀 더 용이하게 접근하려면 $refs를 사용하는 것을 추천합니다.
/exhibition/best.php
<div id="vue-modal">
<product-search-modal ref="reco"></product-search-modal>
</div>
ref를 추가했다.
<script>
$('#add-product').on('click', function(){
productSearchModalComp.$refs.reco.show();
});
</script>
레거시 방식인 Jquey 이벤트 바인딩에서 VUE 컴포넌트 내 show함수를 호출했다.
1-3.외부로 이벤트 연결하기
상품 검색을 성공했다면 선택된 상품을 추천 목록에 추가할 차례입니다. VUE->Jquery로 데이터를 주는 방향인데요. VUE를 사용해봤다면 벌써 눈치챘겠지만, $emit을 이용해 부모 컴포넌트로 전달하면 됩니다.
/vue/productSearchModal/main.vue
<script>
import Vue from 'vue'
export default {
methods: {
show: function () {
$(this.$el).modal('show');
},
addItem: function(d) {
this.$emit('add-item', d);
}
}
}
</script>
$emit을 통해 상위에 추가될 아이템을 전달하고 있다.
/exhibition/best.php
<div id="vue-modal">
<product-search-modal @add-item="addItem" ref="reco"></product-search-modal>
</div>
$emit된 이벤트를 연결했다.
<script>
// 상품 검색 모달 컴포넌트
var productSearchModalComp = new Vue({
el: '#vue-modal',
data: function() {
return {
}
},
methods: {
addItem:function(row){
// 이곳에서 기존 레거시 코드와 연결합니다.
}
}
});
</script>
상품 검색 모달의 부모인 productSearchModalComp에서 이벤트를 받아 처리했다.
상품 검색 모달의 부모인 productSearchModalComp객체에서 이벤트를 받아서 처리했습니다. 이 부분에서 원래 레거시 소스와 연결해주면 되는데요. 이때 주의할 점은 반드시 이벤트명을 케밥 케이스로 해야 한다는 것입니다.
위의 스크립트 코드는 webpack 빌드되는 부분이 아니기 때문에 HTML표준을 따라야 합니다. 또한 컴포넌트를 넣기 위한 태그명도, 변수를 바인딩하는 속성도 모두 케밥 케이스로 작성해야 정상적으로 작동할 겁니다.
1-4.두 개 이상의 컴포넌트를 연결하기
작업을 진행하다 보니 추천상품을 진열하는 기능도 VUE로 컨버팅하게 되었습니다. 이렇게 되다 보니 두 개의 컴포넌트가 나눠서 작업되고, 서로 연결해야 하는 일도 생겼죠. 원래 JQuery에서 이벤트 핸들링한 상품 추가 버튼이 VUE로 변경되었기 때문입니다.
/exhibition/best.php
<div id="vue-modal">
<product-search-modal @add-item="addItem" ref="reco"></product-search-modal>
</div>
<div id="vue-exhibition">
<exhibition></exhibition>
</div>
추천 상품 컴포넌트가 추가되었다.
<script>
// 베스트 상품 진열관리 컴포넌트
var bestExhibitionComp = new Vue({
el: '#vue-exhibition',
data: function(){
return {
}
},
methods: {
}
});
</script>
추천 상품 진열관리를 화면에 불러왔다.
브릿지라고 해서 거창할 건 없습니다. $emit된 이벤트를 받게 되면 상품 검색 모달의 show를 호출하면 됩니다.
/exhibition/best.php
<div id="vue-modal">
<product-search-modal @add-item="addItem" ref="reco"></product-search-modal>
</div>
<div id="vue-exhibition">
<exhibition @show-product-search-modal="showModal"></exhibition>
</div>
show-product-search-modal이란 이벤트를 추가했다.
<script>
// 베스트 상품 진열관리 컴포넌트
var bestExhibitionComp = new Vue({
el: '#vue-exhibition',
data: function(){
return {
}
},
methods: {
showModal:function() {
productSearchModalComp.$refs.reco.show();
}
}
});
</script>
베스트 상품 진열관리 컴포넌트에서 상품 검색 모달을 띄웠다.
물론 추천 상품 관리 컴포넌트가 상품검색 컴포넌트를 import하는 방법도 있습니다. 하지만 상품 검색 컴포넌트 코드가 추천 상품 관리 컴포넌트에도 추가되는 문제와 상품 검색 모달 컴포넌트가 이런 식으로 여러 군데 중복해 들어가면 컴포넌트화하는 의미가 약해진다는 생각이 들었습니다. 결국 선택하지 않았습니다.
브릿지를 연결하는 방법이 조금 불편해 보일 수 있지만 컴포넌트를 다시 사용하는 측면에선 컴포넌트 사이의 종속성을 최대한 낮추는 것이 좋다고 봅니다.
1-5.컴포넌트 옵션 외부에서 지정하기
컴포넌트의 재활용성을 높이려면 옵션을 외부에서 지정하게 하는 것이 좋습니다. 모달의 타이틀이 위치마다 달라야 하거나, 검색해야 하는 상품의 카테고리가 변경되어야 한다면 이는 옵션이 적합합니다. 방법은 의외로 간단합니다. 바로 v-bind를 이용하면 됩니다.
/exhibition/best.php
<div id="vue-modal">
<product-search-modal @add-item="addItem" ref="reco" :option="{'productCateNo':2, 'title':'추천 상품 추가'}"></product-search-modal>
</div>
설명을 위해 옵션을 직접 기술했지만 스크립트 코드에서 지정해도 무방하다.
/vue/productSearchModal/main.vue
<template>
<div tabindex="-1" data-width="90%" class="modal fade">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"></button>
<h4 class="modal-title"></h4>
</div>
<div class="modal-body">
<!— 생략 —>
</div>
</div>
</template>
<script>
import Vue from 'vue'
export default {
props: {
option:{
defualt:{
title:'상품 검색',
productCateNo:1
}
}
},
created: function() {
if (!option.title) option.title = '상품 검색'
if (!option.productCateNo) option.productCateNo = 1
},
methods: {
show: function () {
$(this.$el).modal('show');
this.productList = [];
this.total = 0;
},
addItem: function(d) {
this.$emit('add-item', d);
}
}
}
</script>
옵션을 받아 타이틀과 필터 조건으로 사용했다.
옵션을 받아서 템플릿에서 출력했습니다. 옵션을 지정하지 않을 때가 있을 수도 있어 방어 코드도 같이 작성했습니다.
2.빌드 용량 최적화
실제 빌드를 하면 컴포넌트 js의 용량이 200kb가 넘게 생성됩니다. 코드를 얼마 작성하지 않았는데도 용량이 커서 의아했는데요. 확인해보니 빌드를 할 때 VUE 코어 및 import한 내용들이 같이 빌드되기 때문이었습니다. 컴포넌트를 개별 빌드하기 때문에 생기는 문제였던 거죠.
이것을 해결하기 위해 고민을 하던 중 의외로 간단한 방법을 찾았습니다. 바로 VUE를 import하지 않으면 되는 것이었습니다. 그리고 VUE를 사용해야 하기 때문에
또 다른 문제였던 컴포넌트 사이에 VUE global이 따로 잡히는 오작동도 해결했습니다. 개별 빌드를 할 때 VUE를 import하면 각기 컴포넌트마다 각기 다른 VUE 객체를 참조한 것입니다.
/exhibition/best.php
<script src="/include/plugins/vue/vue.2.5.16.min.js" type="text/javascript"></script>
/vue/productSearchModal/main.vue
<template>
<!— 생략 —>
</template>
<script>
// import Vue from 'vue' // 빌드에 포함되지 않게 제거한다.
export default {
// 이하 중략
}
</script>
3.JQuery와 코드 혼용
기존 레거시 소스가 JQuery 기반이기 때문에 100% VUE로 컨버팅하긴 어렵습니다. 대표적으로 JQuery 플러그인을 썼다면 VUE에 유사한 컨포넌트로 대체하거나 처음부터 구현해야 하는 일이 생길 수 있습니다.
그래서 불가피하게 JQuery를 혼용하게 되는데요. 상품 검색 모달은 기능은 JQuery UI를 사용했기 때문에 이질감을 줄이려고 그대로 사용했습니다. 그렇지만 JQuery와 VUE의 접근 방식은 다르므로 다른 엘리먼트에 영향을 끼치지 않도록 주의해야 합니다.
만약 아래처럼 사용했다면 당장은 문제가 없지만, 언젠가 datepicker라는 class를 가진 엘리멘트가 생기는 순간 오작동을 일으킬 겁니다.
/vue/sample.vue
<template>
<input type="text" class="datepicker">
</template>
<script>
$('.datepicker').datepicker();
</script>
JQuery와 혼용할 때는 반드시 refs를 이용해 정확한 엘리먼트를 지칭해야 오류가 나지 않습니다. 아래처럼요.
/vue/sample.vue
<template>
<input type="text" class="datepicker" ref="datepicker">
</template>
<script>
$(this.$refs.datepicker).datepicker();
</script>
마치며
JQuery 기반의 프로젝트에 VUE를 도입하면서 즐거웠습니다. VUE 사용으로 재활용이 높아져서 특정 기능에 많은 노력을 기울이는 것이 부담스럽지 않았죠. 만들어진 컴포넌트를 다른 곳에 적용할 때 간편해졌고, 잘 만들어진 기능을 붙이니 퀄리티와 안정성까지 좋아졌습니다.
이 글에서 제시한 방법이 다소 복잡하고, 고려할 것도 많지만 VUE 도입을 고려하고 있거나, 레거시 때문에 망설이고 있다면 속는 셈 치고 한 번 시도해보세요. 분명 좋은 결과를 만날 겁니다.
참고
1) JQuery는 css selector를 기반으로 거시적으로 보면, VUE는 컴포넌트 위주로 작고 견고하게 봅니다.
2) babel browser 버전은 여기를 클릭하세요.