본문 바로가기

Server Programming/Spring Boot 2 Full-Stack Programming

[작업 관리 애플리케이션 개발] 3-2. 스프링 부트와 Vue.js 조합 (연동)

반응형

조합하기

  1. 코드 합치기
    1. UI 렌더링 요청
    2. API 요청
  2. HTTP 클라이언트로 통신 (axios 이용)
  3. app.sample.messages 패키지

 

통합 작업

  • 같은 프로젝트에 프런트엔드 코드와 백엔드 코드 추가
  • 메시지를 가져와 새로운 메시지를 저장하는 API를 백엔드 코드에 추가
  • 백은데 코드와 통신하는 기능을 프런트엔드 코드에 추가

코드 합치기

Vue.js를 통해 httlp-server로 만든 index.html 파일과 정적.js 에셋 파일

-> 통합완료시 임베디드 톰캣 서버가 파일들을 서비스한다.

 

작성한 프런트엔드 코드를 백엔드 코드에 옮기는 방식으로 통합한다.

 

처리 과정

#요청이 들어오면 Filter와 DispatcherServlet 객체를 거쳐 Controller 객체에 도달한다.

 

설계한 코드의 요청 유형

  1. UI 렌더링
  2. JSON 메시지를 가져와 새로운 메시지 저장하는 API

UI 렌더링

1. MessageController에 index() 핸들러 메서드 추가

2. 프런트 엔드 코드인 index.html를 resources/templates/index.html에 위치

3. 정적 .js 에셋들은 resources/static/ 에 위치시키면, vue.js 페이지 요청시 스프링이 해당 파일을 제공한다.

 

API 추가하기

messages/ 페이지에서 UI 제공하므로, API는 경로를 분리한다.

->/api/아래에 API 요청 위치시킨다.

 

  1. MessageController 변경 : /api/ 경로 변경, getMessages 추가
  2. MessageService 변경 : getMessages 추가
  3. MessageRepository 변경 : Message 객체 리스트를 가져오는 HQL 실행

 

 

1. MessageController 변경

  • 새 메시지 저장하는 post 메서드 /messages의 위치를 /api/messages로
  • 메시지 가져오는 get 메서드 /api/messages 추가
  • MessageController에서 @RequestMapping 어노테이션을 제거한다. (/api/로 URL을 변경하므로)

 

package app.sample.messages;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
//@RequestMapping("/messages")

public class MessageController {

    private MessageService messageService;

    public MessageController(MessageService messageService) {
        this.messageService = messageService;
    }

    //index 페이지 매핑 -> index 핸들러 메소드로 뷰 이름 반환
    @GetMapping("/messages")
    public String index(){
        return "index";
    }
    @GetMapping("/welcome")
    //리턴 값을 Http 응답본문으로 처리한다는 뜻 -> 타임리프로 대체
    //: 스프링이 핸들러 반환 값을 뷰 이름으로 사용해 타임리프로 응답 생성
    //@ResponseBody
    public String welcome(Model model){
    //public ModelAndView welcome(){
        //1. 볼드체 직접 사용
        //return "<strong>Hello, Welcome to Spring Boot!</strong>";

        //2. Model 인스턴스를 이용해 String 반환
        //메소드에 스프링이 생성하는 Model 인스턴스 전달 -> 템플릿의 키와 일치하는 메시지를 속성으로 추가
        model.addAttribute("message", "Hello, Welcome to Spring Boot!");

        return "welcome";

        //3. ModelAndView 인스턴스 생성
//        ModelAndView mv = new ModelAndView("welcome");
//        mv.addObject("message", "Hello, Welcome to Spring Boot!");


        //return mv;
    }

    //get 매핑 수행
    @GetMapping("/api/messages")
    @ResponseBody
    public ResponseEntity<List<Message>> getMessages() {
        List<Message> messages=messageService.getMessages();
        return ResponseEntity.ok(messages);
    }

    //Post 매핑 수행
    @PostMapping("/api/messages")
    @ResponseBody
    public ResponseEntity<Message> saveMessage(@RequestBody MessageData data) {
        Message saved = messageService.save(data.getText());
        if (saved == null) {
            return ResponseEntity.status(500).build();
        }
        return ResponseEntity.ok(saved);
    }

}

 

2. MessageService 변경

-> getMessages() 메소드 추가

    //메시지 가져오기는 항상 읽기만 하므로
    @Transactional(readOnly = true)
    public List<Message> getMessages() {
        return repository.getMessages();
    }

 

3. MessageRepository 변경

(1) getMessages() 메소드에서 하이버네이트 세션을 얻어서 메시지를 가져오는 HQL(Hibernate Query Langaguage) 생성

(2) 하이버네이트가 생성한 SQL 출력하기 위해 application.properties 변경 : logging.level.org.hibernate.SQL=DEBUG

(3) HQL의 실행을 위해 하이버네이트 Query 객체 생성 -> 쿼리 실행해 테이블의 레코드 반환을 위해 .list() 메소드 호출

-> 하이버네이트가 객체 관계 매핑을 관리하고 Message 객체 리스트를 제공한다.

 

    //HQL 실행하도록 하이버네이트 Query 객체 생성해 추가
    public List<Message> getMessages() {
        Session session = sessionFactory.getCurrentSession();
        String hql = "from Message";
        Query<Message> query = session.createQuery(hql, Message.class);
        return query.list();
    }

 


HTTP 클라이언트로 통신 (axios 이용)

HTTP 클라이언트로 백엔드와 통신하는 작업 수행을 위해 API 기반 프로미스 axios 사용

-> CDN에서 가져와 /resources/static에 추가

https://unpkg.com/browse/axios@0.2.1/dist/axios.min.js

 

UNPKG - axios

2var axios=function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){t.exports=n(1)},function(t,e,n

unpkg.com

 

 

 

1. index.html 변경

-> 메시지 페이지를 열었을 때 메시지를 가져오는 로직을 Vue 인스턴스의 created() 라이플 사이클 훅에 추가해 빠르게 보여주도록 한다.

 

 

변경 전 Index.html

<!DOCTYPE html>
<html>
<head>
  <title>Messages App</title>    
  <style>
    [v-cloak] {display: none;}
    body > div {width: 500px; margin: 0 auto;}
    textarea {width: 100%;}
    ul {padding: 0 15px;}
  </style>
</head>
<body>
  <div id="app" v-cloak>  
    <message-list :items="messages" @delete="deleteMessage"></message-list>
    <form @submit.prevent="addMessage">
      <textarea id="newMessage" v-model="newMessage" placeholder="Leave a message" v-focus></textarea>
      <div><button :disabled="addDisabled" type="submit">Add</button></div>
    </form>    
  </div>
  <script src="../vue.js"></script>  
  <script type="module">
    import MessageList from '../static/components/MessageList.js'
    import LifecycleLogger from '../static/plugins/lifecycle-logger.plugin.js'
    import '../static/directives/focus.directive.js'
    import '../static/filters/datetime.filter.js'

    Vue.use(LifecycleLogger, {beforeMount: false})

    window.vm = new Vue({
      el: '#app',
      name: 'MessagesApp',
      data: {
        messages: [],
        newMessage: ''
      },
      computed: {        
        addDisabled () {
          return this.messages.length >= 10 || this.newMessage.length > 50
        }
      },
      components: {
        MessageList
      },
      methods: {
        addMessage (event) {
          if (!this.newMessage) return
          let now = new Date()
          this.messages.push({id: now.getTime(), text: this.newMessage, createdAt: now})
          this.newMessage = ''
        },
        deleteMessage (message) {
          this.messages.splice(this.messages.indexOf(message), 1)
        }
      }
    })
  </script>
</body>
</html>

 

추가할 메서드

created () {
      axios.get('api/messages?_=' + new Date().getTime())
              .then((response) => {
                this.messages = response.data
              })
              .catch((error) => {
                console.log('Failed to get messages' + error);
              });
    },
    methods: {
      addMessage (event) {
        if (!this.newMessage) return

        axios.post('api/messages', {text: this.newMessage})
                .then((response) => {
                  this.messages.push(response.data)
                  this.newMessage = ''
                })
                .catch((error) => {
                  console.log(error);
                });
      },
      deleteMessage (message) {
        this.messages.splice(this.messages.indexOf(message), 1)
      }
    }

 

변경 후 Index.html

<!DOCTYPE html>
<html>
<head>
  <title>Messages App</title>
  <style>
    [v-cloak] {display: none;}
    body > div {width: 500px; margin: 0 auto;}
    textarea {width: 100%;}
    ul {padding: 0 15px;}
  </style>
</head>
<body>
<div id="app" v-cloak>
  <message-list :items="messages" @delete="deleteMessage"></message-list>
  <form @submit.prevent="addMessage">
    <textarea id="newMessage" v-model="newMessage" placeholder="Leave a message" v-focus></textarea>
    <div><button :disabled="addDisabled" type="submit">Add</button></div>
  </form>
</div>
<script src="../vue.js"></script>
<script src="../axios.v0.18.0.min.js"></script>
<script type="module">
  import MessageList from './components/MessageList.js'
  import LifecycleLogger from './plugins/lifecycle-logger.plugin.js'
  import './directives/focus.directive.js'
  import './filters/datetime.filter.js'

  Vue.use(LifecycleLogger, {beforeMount: false})

  window.vm = new Vue({
    el: '#app',
    name: 'MessagesApp',
    data: {
      messages: [],
      newMessage: ''
    },
    computed: {
      addDisabled () {
        return this.messages.length >= 10 || this.newMessage.length > 50
      }
    },
    components: {
      MessageList
    },
    created () {
      axios.get('api/messages?_=' + new Date().getTime())
              .then((response) => {
                this.messages = response.data
              })
              .catch((error) => {
                console.log('Failed to get messages' + error);
              });
    },
    methods: {
      addMessage (event) {
        if (!this.newMessage) return

        axios.post('api/messages', {text: this.newMessage})
                .then((response) => {
                  this.messages.push(response.data)
                  this.newMessage = ''
                })
                .catch((error) => {
                  console.log(error);
                });
      },
      deleteMessage (message) {
        this.messages.splice(this.messages.indexOf(message), 1)
      }
    }
  })
</script>
</body>
</html>

 

(1) Script에 axios CDN 추가

<script src="../axios.v0.18.0.min.js"></script>

(2)created() 라이프 사이클 훅 내부에 메시지 가져오는 로직 추가

(3) addMessage() 메소드에서 axios.post()로 새 메시지를 백엔드에 보낸다.

-> 요청 완료시 저장된 메시지를 메시지 리스트에 추가

 

 

2. addMessages() 메소드 수정

created () {
      axios.get('api/messages?_=' + new Date().getTime())
              .then((response) => {
                this.messages = response.data
              })
              .catch((error) => {
                console.log('Failed to get messages' + error);
              });
    },
    methods: {
      addMessage (event) {
        if (!this.newMessage) return

        axios.post('api/messages', {text: this.newMessage})
                .then((response) => {
                  this.messages.push(response.data)
                  this.newMessage = ''
                })
                .catch((error) => {
                  console.log(error);
                });
      },
      deleteMessage (message) {
        this.messages.splice(this.messages.indexOf(message), 1)
      }
    }

 

 

 


 

 

반응형