- I. Vue.js 소개
- II. HTTP 웹 서버 (HTTP Web Server) 시작
- III. Vue.js 예제 프로젝트 작성
- IV. Vue 객체를 생성하고 제목 바인딩
- V. 제품 목록 추가
- VI. 장바구니 목록 추가
- VII. 장바구니 합계 추가
- VIII. REST API 호출
Vue.js 프레임워크를 기반으로 간단한 예제 프로그램을 만들고, 스프링부트(Spring Boot)로 만들어 놓은 REST API 를 호출하는 예제를 구현해본다.
I. Vue.js 소개
Vue.js 는 현재 가장 많이 사용되는 자바스트립트 프레임워크 TOP 3 중 하나이다.
“Angular”, “React”, “Vue.js”
Vue.js 는 위의 세 프레임워크 중 가장 늦게 배포되었지만, 활발히 사용자 커뮤니티가 늘어나는중이다.
Vue.js 가 다른 프레임워크와 가장 큰 특징은 점진적으로 채택이 가능하다는것이다.
이미 HTML 과 Javascript 으로 구성된 페이지가 있을때, 이 구성을 그대로 두고 Vue.js 적용이 가능하다.
React의 경우에는 JSX 와 ES6 (ECMAScript 2015) 에 의존을 많이 하고 있는데…
아직 Internet Explorer 11 과 같은 (ES6 이상을 지원하지 못하는) 웹브라우져들이 존재하는 까닭에
NPM + Webpack + Babel 구성으로 빌드하는 방법만을 고수해야 하는 상황이다.
물론 Vue.js 도 고급기능을 사용할때는 NPM + Webpack + Babel 구성으로 빌드하겠지만
아무래도 개발자에게 기존 페이지 구성를 두고 점진적으로 적용이 가능하다는 선택사항이 있는점은 큰 장점이 된다.
II. HTTP 웹 서버 (HTTP Web Server) 시작
이번 글에서는 index.html 1개 파일만을 작성한 뒤 이 파일을 HTTP 웹서버에 올려서 테스트 하려고 한다.
로컬 개발환경에서 HTTP 웹서버를 띄우는 방법은 아래와 같은 방법이 있을것이다.
- Apache WebServer 나 Nginx WebServer 등의 설치패키지를 수동으로 다운받아 설치 후 실행하는 방법
- NPM의 webpack-dev-server 로 실행하는 방법
- 도커(Docker)에서 Apache WebServer 나 Nginx WebServer 등의 컨테이너를 실행하는 방법
- 스프링부트(Spring Boot) 자바 어플리케이션에 HTML 파일을 올려서 실행하는 방법
아래에서는 네번째 방법(스프링부트(Spring Boot) 자바 어플리케이션에 HTML 파일을 올리는 방법)에 대해 간략히 설명한다.
-
이전 글 스프링 부트 (Spring boot) 기반으로 JPA, Lombok, H2 를 이용하여 REST API 구현 예제 을 구현해봤다면 그 스프링부트 프로젝트를 그대로 사용한다.
( sbtest03.zip 소스파일 을 다운로드 받아도 된다.) -
스프링부트 프로젝트의 “/src/main/resource/static” 디렉토리에 빈 index.html 파일을 생성해 넣는다.
-
스프링부트 프로젝트를 빌드하고 실행한 뒤 웹브라우져로 “http://localhost:8080/” 주소로 접속하면 index.html 이 실행된다.
-
(주의) index.html 파일을 변경하면 프로젝트를 다시 빌드해야 변경된 내용이 반영 된다.
III. Vue.js 예제 프로젝트 작성
1. index.html 에 HTML 템플릿 추가
index.html 에 아래와 같은 HTML 템플릿을 추가한다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Vue.js Shop</title>
</head>
<body>
<div id="app">
<h1></h1>
<div>
<h2>Product</h2>
<ul>
<li>
</li>
</ul>
</div>
<div>
<h2>Cart</h2>
<ul>
<li>
</li>
</ul>
<h3>Total Amount : </h3>
</div>
</div>
</body>
</html>
이후에 여기에 쇼핑몰에서 많이 사용중인 프로세스인 “제품 목록을 나열하고 ‘장바구니 담기’ 버튼을 누르면 장바구니에 담는 프로세스” 를 구현할것이다.
2. 웹브라우져에서 확인
웹브라우져로 “http://localhost:8080/” 또는 “http://localhost:8080/index.html” 을 접속하면 아래와 같은 화면이 표시된다.
IV. Vue 객체를 생성하고 제목 바인딩
1. HTML 템플릿에 title 추가
HTML 템플릿의 <h1> 안에 {{ title }} 문장을 넣어준다.
<div id="app">
<h1>{{ title }}</h1>
2. Vue.js 프레임워크 추가
</body> 바로 앞에 아래의 스크립트 태그를 추가한다.
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
</body>
3. Vue 객체 생성
위의 스크립트 태그 아래에 아래와 같은 JavaScript 스크립트 태그를 추가한다.
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script type="application/javascript" language="JavaScript">
new Vue({
el: '#app',
data: {
title: 'Vue.js Store'
}
});
</script>
</body>
이 Vue 객체에서…
el 이 지정한 #app DOM은 Vue 객체와 맵핑이 되고
data의 title 값이 HTML 템플릿의 {{ title }} 으로 바인딩된다.
4. 웹브라우져에서 확인
웹브라우져 화면을 리로드 한다.
<h1> 값이 “Vue.js Store”로 변경되어 있음을 확인 할 수 있다.
V. 제품 목록 추가
1. HTML 템플릿의 첫번째 <li> 에 루프문 추가
HTML 템플릿의 첫번째 <li> 에 아래와 같은 코드를 추가한다.
<li v-for="product in products">
{{ product.title }} ($ {{ product.price}}) - Inventory {{ product.inventory }}
</li>
2. Vue 객체의 data 에 products 배열 추가
Vue 객체의 data 에 products 배열을 추가한다.
<script type="application/javascript" language="JavaScript">
new Vue({
el: '#app',
data: {
title: 'Vue.js Store',
products: [
{id:1, title:'iPad Mini', price:500.5, inventory:7},
{id:2, title:'iPad Pro', price:800.4, inventory:5},
{id:3, title:'MacBook', price:1200.6, inventory:2}
]
}
});
</script>
Vue 객체의 products 배열의 아이템 갯수만큼 <li> 가 생성이되고 자식 텍스트에 재품 이름, 가격, 재고가 맵핑이 된다.
3. 웹브라우져에서 확인
웹브라우져 화면을 리로드 한다.
Product 목록에 제품의 이름, 가격, 재고수량이 잘 표시됨을 확인할 수 있다.
VI. 장바구니 목록 추가
1. HTML 템플릿의 첫번째 <li> 의 <button> 에 이벤트 추가
HTML 템플릿의 첫번째 <li> 의 <button> 에 아래와 같은 코드를 추가한다.
<li v-for="product in products">
{{ product.title }} ($ {{ product.price}}) - Inventory {{ product.inventory }}
<button v-on:click="addToCart($event, product)" v-bind:disabled="product.inventory <= 0">Add Cart</button>
</li>
2. HTML 템플릿의 두번째 <li> 에 루프문 추가
HTML 템플릿의 두번째 <li> 에 아래와 같은 코드를 추가한다.
<li v-for="product in cartItems">
{{ product.title }} ($ {{ product.price}}) x {{ product.quantity}}
</li>
3. cartItems 배열과 addToCart 함수 추가
Vue 객체 정의에서
data 안에 cartItems 배열을 추가하고
methods 안에 addToCart 함수를 추가한다.
<script type="application/javascript" language="JavaScript">
new Vue({
el: '#app',
data: {
title: 'Vue.js Store',
products: [
{id:1, title:'iPad Mini', price:500.5, inventory:7},
{id:2, title:'iPad Pro', price:800.4, inventory:5},
{id:3, title:'MacBook', price:1200.6, inventory:2}
],
cartItems: [
]
},
methods: {
addToCart : function(event, product) {
// event: DOM 이벤트 객체
// product: 클릭 이벤트가 발생된 제품 객체
// <코드블럭 #1>
// 전체 products 배열에서 product.id 값과 일치하는 제품을 찾아 재고수량(inventory)을 -1 감소시킨다.
this.$data.products
.filter(function(p) {return p.id === product.id})
.map(function(p) {return p.inventory--});
// <코드블럭 #2>
// 전체 cartItems 배열에서 product.id 값과 일치하는 제품을 찾아
// 존재하지 하면 수량(quantity)을 +1 증가시킨다.
// 존재하지 하지 않으면 product 파라메터로 넘어온 값을 참고하여 새로운 객체를 만들고 수량(quantity)은 1로 지정한다.
var _cartItem = this.$data.cartItems.filter(function(p) {return p.id === product.id});
if (_cartItem.length > 0) {
_cartItem.map(function(p) {return p.quantity++});
} else {
this.$data.cartItems.push({
"id": product.id,
"title": product.title,
"price": product.price,
"quantity": 1
});
}
}
}
});
</script>
Vue 객체의 products <li> 아래의 <button> 을 클릭하면 addToCart() 함수가 실행된다.
이 함수에서는 products 배열의 아이템에서 inventory를 하나 감소시키고, cartItems 배열의 아이템에서는 quantity를 하나 증가시키게 된다.
<코드블럭 #1>
여기에는 filter()와 map() 함수를 사용했다.
filter(), map(), reduce() 는 대용량데이터 처리 목적으로 나온 개념을 자바스크립트로 구현한 함수로 배열처리에서 그 효용성이 좋으니 숙지하면 좋다.
전통적인 for 루프를 사용한 코드로는 아래와 같이 구현 할 수 있다.
// 전통적인 for 루프
for (var p = 0; p < this.$data.products.length; p++) {
if (this.$data.products[p].id === product.id) {
this.$data.products[p].inventory--;
break;
}
}
그리고 “Arrow Function”을 사용하여 구현할 수도 있다.
ES6(ES2015) 이상에서만 지원
// "Arrow Function" 사용 (ES6 이상 지원)
this.$data.products
.filter(p => p.id === product.id)
.map(p => p.inventory--);
<코드블럭 #2>
여기에도 filter()와 map() 함수를 사용했다.
this.$data.cartItems.push() 의 객체 복사시에 Object.assign() 함수를 사용하면 훨씬 깔끔해진다.
ES6(ES2015) 이상에서만 지원
// Object.assign() 사용 (ES6 이상 지원)
this.$data.cartItems.push(Object.assign({'quantity':1}, product));
4. 웹브라우져에서 확인
웹브라우져 화면을 리로드 한다.
Product 목록 아이템의 “Add Cart” 버튼을 클릭하면 재고(Inventory)가 감소하고 하단의 Cart 목록에 아이템이 추가되는것을 확인할 수 있다.
VII. 장바구니 합계 추가
1. HTML 템플릿에 totalAmount 추가
HTML 템플릿의 <h3> 안에 “Total Amount : $ {{ totalAmount }}” 문장을 넣어준다.
<h3>Total Amount : $ {{ totalAmount }}</h3>
2. totalAmount 함수 추가
Vue 객체 정의에서 computed 안에 totalAmount 함수를 추가한다.
<script type="application/javascript" language="JavaScript">
new Vue({
el: '#app',
data: {
title: 'Vue.js Store',
products: [
{id:1, title:'iPad Mini', price:500.5, inventory:7},
{id:2, title:'iPad Pro', price:800.4, inventory:5},
{id:3, title:'MacBook', price:1200.6, inventory:2}
],
cartItems: [
]
},
methods: {
addToCart : function(event, product) {
// event: DOM 이벤트 객체
// product: 클릭 이벤트가 발생된 제품 객체
// <코드블럭 #1>
// 전체 products 배열에서 product.id 값과 일치하는 제품을 찾아 재고수량(inventory)을 -1 감소시킨다.
this.$data.products
.filter(function(p) {return p.id === product.id})
.map(function(p) {return p.inventory--});
// <코드블럭 #2>
// 전체 cartItems 배열에서 product.id 값과 일치하는 제품을 찾아
// 존재하지 하면 수량(quantity)을 +1 증가시킨다.
// 존재하지 하지 않으면 product 파라메터로 넘어온 값을 참고하여 새로운 객체를 만들고 수량(quantity)은 1로 지정한다.
var _cartItem = this.$data.cartItems.filter(function(p) {return p.id === product.id});
if (_cartItem.length > 0) {
_cartItem.map(function(p) {return p.quantity++});
} else {
this.$data.cartItems.push({
"id": product.id,
"title": product.title,
"price": product.price,
"quantity": 1
});
}
}
},
computed: {
totalAmount : function() {
// <코드블럭 #3>
// 장바구니에 담긴 제품들 전체 합계를 reduce() 함수를 사용해서 구한다.
var totalAmount = this.$data.cartItems.reduce(function (previousSum, currentItem) {
return previousSum + currentItem.price * currentItem.quantity;
}, 0);
return Math.round(totalAmount);
}
}
})
</script>
computed() 는 계산하여 값을 가져와야 할 경우에 구현한다.
(C++ 나 C# 에서의 property getter 와 유사하다)
여기서는 cartItems 배열이 변경될때마다 이 배열의 아이템들 금액을 계산해서 합계 내주는 로직을 구현하였다.
<코드블럭 #3>
여기에는 reduce() 함수를 사용했다.
reduce() 함수는 배열을 순차적으로 엑세스하여 하나값을 도출하는데 유용한 함수이다.
전통적인 for 루프를 사용한 코드로는 아래와 같이 구현 할 수 있다.
var totalAmount = 0.0;
for (var i = 0; i < this.$data.cartItems.length; i++) {
totalAmount += this.$data.cartItems[i].price * this.$data.cartItems[i].quantity;
}
return Math.round(totalAmount);
이 역시 “Arrow Function”을 사용하여 구현할 수도 있다.
ES6(ES2015) 이상에서만 지원
// "Arrow Function" 사용 (ES6 이상 지원)
var totalAmount = this.$data.cartItems.reduce(
(previousSum, currentItem) => previousSum + currentItem.price * currentItem.quantity
, 0);
return Math.round(totalAmount);
3. 웹브라우져에서 확인
웹브라우져 화면을 리로드 한다.
Cart 목록에 아이템이 추가될때마다 총 합계가 계산되는것을 볼 수 있다.
VIII. REST API 호출
1. 스프링부트 REST API 실행
이전 글 스프링 부트 (Spring boot) 기반으로 JPA, Lombok, H2 를 이용하여 REST API 구현 예제 을 구현해봤다면 그 스프링부트 프로젝트를 그대로 사용한다.
그렇지 않으면 sbtest03.zip 소스파일 을 다운로드 받아도 된다.
2. 스크립트 태그 추가
기존의 존재했던 스크립트 태그 아래에 두줄의 스크립트 태그를 추가한다.
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js"></script>
3. products 배열 값 주석처리
Vue 객체 정의에서 products 배열의 값들을 주석처리 하고 mounted 함수를 추가한다.
new Vue({
el: '#app',
data: {
title: 'Vue.js Store',
products: [
// {id:1, title:'iPad Mini', price:500.5, inventory:7},
// {id:2, title:'iPad Pro', price:800.4, inventory:5},
// {id:3, title:'MacBook', price:1200.6, inventory:2}
],
cartItems: [
]
},
methods: {
// ...(중략)...
},
computed: {
// ...(중략)...
},
mounted: function () {
var PRODUCT_API_URL = "/product/";
var _app = this;
// use Axios + Promise
axios.get(PRODUCT_API_URL)
.then(function (response) {
_app.$data.products = response.data;
})
.catch(function (error) {
console.log(error);
})
}
})
mounted() 함수는 new Vue 로 생성한 객체에 #app DOM 엘리먼트에 마운트된 시점에서 실행이 된다. 이 함수에서 REST API를 호출하는 코드를 작성했다.
여기서는 AXIOS 라이브러리를 사용하였다.
AXIOS는 HTTP 요청의 응답을 Promise 구조로 넘겨준다.
현재 Internet Explorer는 Promise를 지원하지 않으므로 Promise 지원을 위해 스크립트 태그에서 es6-promise 라이브러리를 추가하였다.
만약 전통적인 XMLHttpRequest 를 사용한다면 아래와 같이 코드를 작성할 수도 있다.
// XMLHttpRequest 사용
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
_app.$data.products = (typeof this.response === "string") ? JSON.parse(this.response) : this.response;
}
};
xhttp.open("GET", PRODUCT_API_URL, true);
xhttp.send();
REST API 호출시에 async/await 를 사용하면 훨씬 깔끔해진다.
ES8(ES2017) 이상에서만 지원
// async/await 사용
(async () => {
try {
_app.$data.products = (await axios.get(PRODUCT_API_URL)).data;
} catch(error) {
console.log(error);
}
})();
4. 웹브라우져에서 확인
웹브라우져 화면을 리로드 한다.
웹 페이지 로딩시에 REST API 호출 결과로 Products 목록이 채워짐을 확인 할 수 있다.
본 예제 결과물 sbvue.zip 소스파일은 아래 링크에서 다운받을 수 있다.
sbtest04.zip 소스파일
앞에서 언급된 ES6(ES2015) 관련코드는 Vue.js 를 NPM + Webpack + Babel 구성으로 빌드하면
Internet Explorer 등의 낮은스펙의 웹브라우져에서도 문제없이 실행이 가능하다.
이 부분에 대해서는 다음에 글을 남겨보도록 하겠다.