ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [To-Do 앱]Vue.js/Node.js 앱 에서 API Call 하기 (Axios)
    웹 어플리케이션 2019. 2. 26. 17:16

    이전 포스트에서 Vue.js와 Node.js를 이용해 프론트엔드 웹 서버의 뼈대를 구현 해 보았다. 이번 포스트에서는 진짜로 To Do 리스트를 만들기 위해 Vue.js/Node.js 프론트엔드 서버에서 Spring Boot RESTful API서버로 API콜을 해 보도록 하겠다. 들어가기에 앞서 vue.js는 우리가 추가한 eslint패키지 때문에 들여쓰기(indention)에 매우 민감하다. 따라서 들여쓰기에 주의를 하거나 package.json에서  "lint": "eslint --ext .js,.vue src" 이 부분을 지우고 다시 실행시켜야 할 것이다. 일단 실습을 위해 프로젝트를 실행시켜라. npm run dev

    예상독자

    목표

    • 프론트엔드 프로젝트구조
    • Vue.js 컴포넌트 (Component)
    • Vue.js 라우터 (Router)
    • Vue.js App.vue
    • Axios을 이용해 API 콜 하기
    • Cross Origin Resource Sharing

    프론트엔드 프로젝트 구조

    to-do-frontend 프로젝트 내부의 src 디렉토리의 구조를 살펴보자.

    중요한 몇 개의 파일과 디렉토리를 확인 해 보자. src의 디렉토리 아래에는 3개의 디렉토리 assets, components, router가 있다.

    • assets: .css나 .png같은 UI관련 리소스들이 들어간다.
    • components: 여기에 바로 화면 내부에 들어갈 html 템플릿과 vue.js 함수들이 들어간다.
    • router: vue.js의 라우팅 정보가 들어간다.
    • main.js: 이 Vue앱을 초기화 시키는 자바스크립트이다.
    • App.vue: 이 앱의 기본(디폴트) 템플릿이라고 생각하면 된다.
    • index.js: vue-router를 이용해 경로를 설정하는 자바스크립트이다.
    • Hello.vue: 우리가 ToDoItem관련 로직을 넣을 컴포넌트이다.

    Vue.js 컴포넌트 (Component)

    컴포넌트는 Vue.js에서 재사용 할 수 있는 단위이다. 이 튜토리얼에서는 HTML 페이지 하나를 컴포넌트 하나라고 생각하면 쉽다.  Hello.vue를 다음과 같이 수정 해 보자.
    <template>
    <div class="hello">
    오늘 해야 할 일
    {{toDoItems}}
    </div>
    </template>

    <script>
    export default {
    name: 'hello',
    data: () => {
    return {
    toDoItems: ['1. 밥 먹기.', '2. 잠 자기.']
    }
    }
    }
    </script>

    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style>
    h1, h2 {
    font-weight: normal;
    }

    ul {
    list-style-type: none;
    padding: 0;
    }

    li {
    display: inline-block;
    margin: 0 10px;
    }

    a {
    color: #35495E;
    }
    </style>
    이렇게 하면, http://localhost:8080/이 아래처럼 변경 될 것이다.

    헤더의 To Do List는 아래서 변경 할 테니 신경쓰지 마라. 아무튼 위의 캡쳐 화면을 보면 오늘 해야 할 일 그리고 옆에 밥먹기, 잠자기 리스트가 나왔다. 이 부분의 코드를 다시 한번 살펴보자.

    <template>
    <div class="hello">
    오늘 해야 할 일
    {{toDoItems}}
    </div>
    </template>

    <script>
    export default {
    name: 'hello',
    data: () => {
    return {
    toDoItems: ['1. 밥 먹기.', '2. 잠 자기.']
    }
    }
    }
    </script>

    <template></template> 이 사이에는 이 컴포넌트가 사용할 HTML의 템플릿이 들어간다. 코드와 UI사이의 맵핑을 확인 해 보자.

    1과 2를 자세히 보자. 우리가 script안에서 export default하는 부분에 data를 정의했다. 이 데이터가 바로 template내부에서 쓰일 데이터이다. 처음 data를 정의 할 때 함수의 형태로 초기값을 오브젝트의 형태({..})로 리턴 해 줘야 한다. 그래서 함수 () => {}를 써 주고 그 안에 return값을 toDoItems가 있는 오브젝트를 리턴했다. 이렇게 리턴하면 template안에서 {{toDoItems}}처럼 두개의 꺽쇠괄호 안에 변수를 집어넣어 사용 가능하다. 지금 이 예제에서는 투두 리스트를 만들 것이므로 리스트를 넣어보았다.

    Vue.js 라우터 (Router)

    이제 라우터에 대해 알아보자. 라우터는 어떤 역할을 할까? 바로 경로를 설정 해 주는 역할을 한다. 브라우저에서 보면 /products, /contacts같이 슬래시를 사용해 경로를 지정 해 줄 수 있다. 우리의 앱에서는 vue-router라는 패키지를 이용해 손 쉽게 라우팅을 할 수 있다. 이 작업은 스프링부트와 비교하자면 @ReqestMapping(value ="/todo")를 해 줬던 작업과 비슷하다. 스프링에서는 해당 경로로 리퀘스트가 들어오면 어떤 함수를 실행시켰다. 마찬가지이다. 이 라우터도 해당 경로로 누군가 접근하면 그 경로에 필요한 컴포넌트를 이어 줄 것이다. 아래를 보자.
    router/index.js

    import Vue from 'vue' # 1. Vue를 사용 할 수 있도록 import한다.
    import Router from 'vue-router' # 2. Router를 사용 할 수 있도록 import한다.
    import Hello from '@/components/Hello' # 3. 경로 설정을 원하는 컴포넌트를 import 한다.

    Vue.use(Router) # 4. Vue에게 Router를 사용하라고 알려준다.

    # 5. 실제 경로 설정
    export default new Router({
    routes: [
    {
    path: '/', # http://localhost:8080/으로 누가 들어오면
    name: 'Hello', # 이 경로에 Hello라는 이름을 붙이고
    component: Hello # Hello.vue 컴포넌트를 이어줘라... 어디에? <router-view></router-view>에
    }
    ]
    })

    그렇자면 <router-view></router-view>는 과연 어디 있는가? 다음 섹션으로 넘어가 App.vue를 확인 해 보자.

    Vue.js App.vue

    App.vue
    <template>
    <div id="app">
    <header>
    <span>To Do List</span>
    </header>
    <main>
    <router-view></router-view>
    </main>
    </div>
    </template>

    <script>
    export default {
    name: 'app'
    }
    </script>
    그림을 통해 App.vue의 구조를 보자. 

    App.vue의 헤더에 To Do List라고 쓰여있으므로 이를 통해 이 부분이 브라우저상의 To Do List와 맵핑된다는 것을 이해 할 수 있다. 그리고 아래 부분인 하얀 부분이 <router-view></router-view>로 매핑 된 것을 확인 할 수 있다. 바로 이 부분이 우리가 route/index.js에 명시한 경로에 맞는 컴포넌트가 출력되는 부분이다.

    다시말해 vue.js가 <router-view></router-view>부분에 어떤 컴포넌트를 보여줄 지 결정하기 위해, 

    1. 현재 요청받은 브라우저의 경로를 본다.

    2. 그 경로를 router/index.js의 라우터에서 찾는다 (path : '/' 부분)

    3. 해당 경로의 컴포넌트를 (Hello)를 리턴한다.

    4. 해당 컴포넌트의 스크립트를 실행하고 템플릿을 <router-view></router-view>에 넣는다.

    이런 과정을 거쳐 Hello.vue가 <router-view></router-view>부분에 보여지는 것이다.

    이를 확인하기 위해 새 경로인 /about을 만들고 컴포넌트인 About.vue를 components 디렉토리 아래에 만들어 이 둘을 이어보자.

    About.vue

    <!-- to-do-frontend/src/components/About.vue -->
    <template>
    <div class="about">
    {{msg}}
    </div>
    </template>

    <script>
    export default {
    name: 'about',
    data: () => {
    return {
    msg: '내가 개발한 To Do 어플입니다.'
    }
    }
    }
    </script>

    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style>
    h1, h2 {
    font-weight: normal;
    }

    ul {
    list-style-type: none;
    padding: 0;
    }

    li {
    display: inline-block;
    margin: 0 10px;
    }

    a {
    color: #35495E;
    }
    </style>

    데이터에는 간단하게 msg로 '내가 개발한 To Do 어플입니다.'를 할당 해 주었다. 이제 router/index.js에 가서 경로를 추가 해 주자.
    router/index.js

    import Vue from 'vue'
    import Router from 'vue-router'
    import Hello from '@/components/Hello'
    import About from '@/components/About' # component를 import한다 .vue는 안 써줘도 됨.

    Vue.use(Router)

    export default new Router({
    routes: [
    {
    path: '/',
    name: 'Hello',
    component: Hello
    },
    {
    path: '/about', # http://localhost:8080/about 경로에 맵핑 해 준다.
    name: 'About',
    component: About # About.vue를 맵핑 해 준다.
    }
    ]
    })

    이렇게 하고 저장 후 http://localhost:8080/#/about로 들어가 보자

    위처럼 화면이 바뀌고 About.vue에서 추가 했던 메시지가 보이는 것을 확인 할 수 있다.

    Axios을 이용해 API 콜 하기

    라우터의 기능까지 확인 했으니 이제 Axios를 이용해 스프링 부트에 API콜을 해 보자.

    Axios 설치

    이를 위해 일단 Axios 패키지를 설치해야 한다. 실행중인 to-do-frontend앱을 종료하고 터미널을 이용해 to-do-frontend앱의 프로젝트 디렉토리로 들어간다.
    ➜ npm install axios
    
    + axios@0.18.0
    added 1 package from 1 contributor and audited 9253 packages in 14.731s
    found 1 low severity vulnerability
    위처럼 npm install axios를 실행하면 npm이 알아서 axios를 to-do-frontend앱에 추가 할 것이다. 이제 다시 to-do-frontend앱을 실행 해 보자.
    ➜ npm run dev
    그리고 스프링 부트 앱도 실행 해야 한다. 잠깐, 현재 to-do-frontend앱은 8080에서 실행되고 있다. 만약 스프링 부트 앱을 그냥 실행시킨다면 8080 포트에 실행 시킬 것 이므로 port address in use라는 바인드 에러가 난다. 즉 8080포트는 이미 다른 어플리케이션이 사용중이라는 뜻이다. 따라서 우리는 스프링 부트 앱의 포트를 변경 해야 한다.
    src/main/resources/application.properties 에 다음과 같은 엔트리를 추가 해 주자.
    server.port = 5000
    이제 스프링 부트 앱을 실행 시켜보자(gradle bootRun). 5000포트에서 실행 될 것이다. Docker에서 실행하는 사람들은 5000:8080으로 포트 맵핑을 해 주면 된다.

    Hello.vue에서 GET API Call하기

    이제 다시 to-do-frontend앱으로 돌아와 Hello.vue를 다음과 같이 수정 해 보자.

    <template>
    <div class="hello">
    오늘 해야 할 일
    <ul v-if="toDoItems && toDoItems.length">
    <li v-for="toDoItem of toDoItems">
    {{toDoItem.title}}
    </li>
    </ul>
    </div>
    </template>

    <script>
    import axios from 'axios' // 아까 받은 axios 패키지를 사용하기 위해 import한다

    export default {
    name: 'hello',
    data: () => {
    return {
    toDoItems: [] // toDoItems를 빈 리스트로 초기화한다.
    }
    },
    created () { // 초기화 함수를 정의 한다.
    axios.get('http://127.0.0.1:5000/todo/') // http://localhost:5000/todo/에 get call을 한다.
    .then(response => {
    this.toDoItems = response.data.map(r => r.data) // 반환되는 값을 toDoItems에 저장한다.

    })
    .catch(e => {
    console.log('error : ', e) // 에러가 나는 경우 콘솔에 에러를 출력한다
    })
    }
    }
    </script>

    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style>
    h1, h2 {
    font-weight: normal;
    }

    ul {
    list-style-type: none;
    padding: 0;
    }

    li {
    display: inline-block;
    margin: 0 10px;
    }

    a {
    color: #35495E;
    }
    </style>

    이제 template 내부를 잠깐 설명하겠다.

    <ul v-if="toDoItems && toDoItems.length">
    <li v-for="toDoItem of toDoItems">
    {{toDoItem.title}}
    </li>
    </ul>

    v-if는 vue.js에서 제공하는 html attribute이다. 이렇게 v-if="조건" 을 적어주면 조건에 따라 해당 블록을 실행하거나 실행하지 않는다. 여기서는 toDoItems && toDoItems.length 즉 toDoItems 오브젝트가 존재하거나, toDoItems.length가 존재하는 경우 아래의 <li ..> </li>를 출력한다.
    v-for은 뭘까? 감이 오지 않는가? 바로 for 문이다. 많은 자바스크립트 프레임워크의 장점 중 하나가 정적 문서인 HTML내에서 마치 HTML이 프로그래밍 언어 인 것처럼 사용 할 수 있게 해준다는 것이다. 위의 블락은 자바에서 아래랑 비슷한 느낌이다. 

    for(ToDoItem todoItem : toDoItems) {
    System.out.println(toDoItem.getTitle());
    }

    아무튼 이렇게 하면 우리의 데이터가 출력되어야 한다. 한번 새로고침을 해 보자. 실행이 되었는가? 아니다. 에러가 났을 것이다. 에러가 났는지 어떻게 확인하는가? F12를 눌러 Developer Tools에 들어가 보자.

    아래와 같은 요상한 에러가 난 것을 확인 할 수 있다.

    Cross Origin Resource Sharing

    Access to XMLHttpRequest at 'http://127.0.0.1:5000/todo/' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

    이는 CORS(Cross Origin Resource Sharing)이라고 보안을 위해 만들어진 정책이다. 무슨 뜻이냐 하면 스프링 부트 어플리케이션이 도메인이 다른 어플리케이션으로부터의 요청을 거부했다는 뜻이다. 즉 localhost:8080 -> 127.0.0.1:5000/todo/는 도메인(포트)가 다르기 때문에 보안상 이를 허락하지 않는다는 뜻이다. 그러면 어떻게 해야 할까? 다 방법이 있다.

    스프링 부트로 돌아가 Cross Origin을 위한 Configuration을 추가 해 줘야 한다. Config패키지 안에 WebConfig.java를 만들어보자. 그리고 아래처럼 설정 해 보자.

    package com.fsoftwareengineer.MySpringApp.Config;

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.CorsRegistry;
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
    .allowedOrigins("http://127.0.0.1:8080")
    .allowedOrigins("http://localhost:8080");
    }
    }

    간단히 말해 127.0.0.1:8080이나 localhost:8080에서 들어오는 리퀘스트는 허락하라는 뜻이다.
    이렇게 저장 하고 스프링 부트를 다시 실행 해 보자.
    gradle clean build; gradle bootRun;
    이제는 아래처럼 에러가 나지 않아야 한다.

    이제 포스트맨이나 cURL을 이용해 To Do Item을 몇 개 추가 해 보도록 하자. 포스트맨을 이용하는 법은 이 포스트(포스트맨(Postman)을 이용한 API 테스트)에서 확인 하도록 한다. 또는 아래의 화면을 참고하되 포트를 5000으로 바꾸는 것을 잊지 말자.


    CURL 코드는 다음과 같다.
    curl -X POST \
    http://localhost:5000/todo \
    -H 'Content-Type: application/json' \
    -H 'Postman-Token: f0b5caf1-d362-480c-adb2-9f3cadc9cc12' \
    -H 'cache-control: no-cache' \
    -d '{
    "title" : "To Do 리스트에 CSS추가하기"
    }'

    이렇게 하고 다시 http://localhost:8080에서 새로고침을 해 보자. 아래와 같은 화면이 나오면 성공 한 것이다.

     여기까지 해서 Axios를 이용해 이전에 만들었던 RESTful API에 GET메서드 콜을 하는 방법까지 살펴보았다. 지금까지 따라왔으면 느끼겠지만 만만치 않다. 웹 앱 하나를 만드는 것은 이렇게 많은 시간과, 디버깅, 노력이 필요 한 것이다. 그래도 여기까지 했으면 거의 다 한 것이다! 이 포스트에서 포트 바인딩이나 CORS에 대해 이야기 했던 것을 기억하면 좋겠다. 웹 앱을 개발하다보면 한번씩 건너야 하는 산이다. 다음 포스트에서는 boostrap-vue를 이용해 UI를 구현하고, UI상에서 POST를 이용해 To Do 아이템을 생성하는 실습을 해 보도록 하겠다.


    다음 포스트 : [To-Do 앱]Vue.js/Node.js Bootstrap-vue를 이용한 UI 구현

    댓글

f.software engineer @ All Right Reserved