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” 을 접속하면 아래와 같은 화면이 표시된다.

index.html 웹브라우져에서 확인 #1

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”로 변경되어 있음을 확인 할 수 있다.

index.html 웹브라우져에서 확인 #2

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 목록에 제품의 이름, 가격, 재고수량이 잘 표시됨을 확인할 수 있다.

index.html 웹브라우져에서 확인 #3

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 목록에 아이템이 추가되는것을 확인할 수 있다.

index.html 웹브라우져에서 확인 #4

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 목록에 아이템이 추가될때마다 총 합계가 계산되는것을 볼 수 있다.

index.html 웹브라우져에서 확인 #5

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 목록이 채워짐을 확인 할 수 있다.

index.html 웹브라우져에서 확인 #6


본 예제 결과물 sbvue.zip 소스파일은 아래 링크에서 다운받을 수 있다.
sbtest04.zip 소스파일


앞에서 언급된 ES6(ES2015) 관련코드는 Vue.js 를 NPM + Webpack + Babel 구성으로 빌드하면
Internet Explorer 등의 낮은스펙의 웹브라우져에서도 문제없이 실행이 가능하다.
이 부분에 대해서는 다음에 글을 남겨보도록 하겠다.

Front-end Vue.js Spring SpringBoot Back-end REST API

Blog Logo

HeeHun Kang


Published