κ΄€λ ¨ μ†ŒμŠ€μ½”λ“œλŠ” Github spring-boot-integration-vuejs λ¦¬ν¬μ§€ν† λ¦¬μ—μ„œ μ œκ³΅ν•©λ‹ˆλ‹€.

μ΅œκ·Όμ—λŠ” μŠ€ν”„λ§ ν”„λ ˆμž„μ›Œν¬λ‘œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 개발 μ‹œ ν”„λ‘ νŠΈμ—”λ“œ ν΄λΌμ΄μ–ΈνŠΈλ₯Ό Vueλ₯Ό ν™œμš©ν•˜μ—¬ κ°œλ°œν•˜κ³  λ°°ν¬ν•©λ‹ˆλ‹€. 이번 κΈ€μ—μ„œλŠ” μŠ€ν”„λ§ λΆ€νŠΈ ν”„λ‘œμ νŠΈλ₯Ό μ‹œμž‘ν•˜κ³  Vue CLIλ₯Ό 톡해 ν”„λ‘ νŠΈμ—”λ“œ ν΄λΌμ΄μ–ΈνŠΈλ₯Ό κ°œλ°œν•  λ•Œ μ–΄λ–»κ²Œ μ§„ν–‰ν•˜λŠ”μ§€ μ„€λͺ…ν•©λ‹ˆλ‹€. μ œκ°€ μ•Œλ €λ“œλ¦¬λŠ” 방법과 κ΅¬μ‘°λŠ” μ •ν™•ν•œ 정닡은 μ•„λ‹˜μ„ 미리 λ°νžˆλŠ” λ°” μž…λ‹ˆλ‹€.

Spring Initializr

μŠ€ν”„λ§ λΆ€νŠΈ ν”„λ‘œμ νŠΈλŠ” Spring Initializrμ—μ„œ μ‰½κ²Œ μ—¬λŸ¬λΆ„μ΄ μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ‹œμž‘ν•  수 μžˆλ„λ‘ μ§€μ›ν•©λ‹ˆλ‹€. μ €λŠ” 메이븐(Maven)이 μ•„λ‹Œ κ·Έλž˜λ“€(Gradle) ν”„λ‘œμ νŠΈλ₯Ό μ„ ν˜Έν•˜λ©° μ–Έμ–΄λŠ” Java둜 κ°œλ°œν•©λ‹ˆλ‹€.

그리고 μœ„μ™€ 같이 ν”„λ‘œμ νŠΈμ—μ„œ μ‚¬μš©ν•  μ˜μ‘΄μ„±(Dependencies)λ₯Ό μ°Ύμ•„ μ„ νƒν•©λ‹ˆλ‹€. μ΄λ ‡κ²Œ λ§Œλ“€μ–΄μ§€λŠ” μŠ€ν”„λ§ λΆ€νŠΈ ν”„λ‘œμ νŠΈλŠ” λ‹€μŒκ³Ό 같은 디렉토리 ꡬ쑰λ₯Ό κ°€μ§€κ²Œ λ©λ‹ˆλ‹€.

src
 β”œβ”€ main
 β”‚ β”œβ”€ java
 β”‚ └─ resources
gradle
build.gradle

Vue CLI

μ•žμ„œ Spring Initializrλ₯Ό 톡해 μŠ€ν”„λ§ λΆ€νŠΈ ν”„λ‘œμ νŠΈλ₯Ό κ΅¬μ„±ν•˜μ˜€μŠ΅λ‹ˆλ‹€. μ΄μ œλŠ” μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜κ³Ό ν•¨κ»˜ κ°œλ°œν•  Vue ν”„λ‘œμ νŠΈλ₯Ό κ΅¬μ„±ν•©λ‹ˆλ‹€. Vue ν”„λ‘œμ νŠΈλŠ” Vue CLIλ₯Ό ν™œμš©ν•˜μ—¬ μ‰½κ²Œ μ‹œμž‘ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

# Vue CLIλŠ” NPM으둜 μ„€μΉ˜ν•©λ‹ˆλ‹€.
npm i -g @vue/cli
# 그리고 create λͺ…λ Ήμ–΄λ‘œ ν”„λ‘œμ νŠΈλ₯Ό μ‹œμž‘ν•©λ‹ˆλ‹€.
vue create --preset kdevkr/vue-preset [project-name]

μœ„ μ˜ˆμ‹œμ²˜λŸΌ Vue ν”„λ‘œμ νŠΈλ₯Ό μ‹œμž‘ν•  λ•Œ 자주 μ‚¬μš©ν•˜λŠ” ν”ŒλŸ¬κ·ΈμΈμ„ 프리셋 ν˜•νƒœ(preset.json)둜 κ΅¬μ„±ν•˜μ—¬ 지정할 수 μžˆμŠ΅λ‹ˆλ‹€. μ €μ˜ kdevkr/vue-preset 프리셋은 λ‹€μŒμ˜ λΌμ΄λΈŒλŸ¬λ¦¬λ“€μ„ ν¬ν•¨ν•˜λŠ” Vue ν”„λ‘œμ νŠΈλ₯Ό κ΅¬μ„±ν•©λ‹ˆλ‹€.

  • Babel
  • ESLint + Prettier
  • SCSS (with dart-sass)
  • Vuex
  • Vue Router
  • Bootstrap Vue
  • Fontawesome

src/main/vue

보톡은 ν”„λ‘œμ νŠΈ 루트 κ²½λ‘œμ— Vue ν”„λ‘œμ νŠΈλ₯Ό ꡬ성해도 μƒκ΄€μ—†μœΌλ‚˜ μ €λŠ” src/main/java처럼 src/main/vue둜 κ΅¬μ„±ν•˜λŠ” 것을 μΆ”μ²œν•©λ‹ˆλ‹€. 이제 src/main/vue ν΄λ”μ—μ„œ ν”„λ‘ νŠΈμ—”λ“œ ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œλ₯Ό κ΄€λ¦¬ν•˜κ²Œ λ©λ‹ˆλ‹€.

cd src/main
vue create --preset kdevkr/vue-preset vue

Vue CLI에 μ˜ν•΄ src/main/vue에 Vue ν”„λ‘œμ νŠΈκ°€ λ§Œλ“€μ–΄μ§‘λ‹ˆλ‹€.

Vue Configuration

Vue ν”„λ‘œμ νŠΈμ— λŒ€ν•œ 섀정은 vue.config.jsλ₯Ό 톡해 λ³€κ²½ν•  수 μžˆμŠ΅λ‹ˆλ‹€. Vue ν”„λ‘œμ νŠΈλŠ” λ§Œλ“€μ—ˆμ§€λ§Œ λͺ‡κ°€μ§€ 섀정을 μ§„ν–‰ν•΄μ•Όν•©λ‹ˆλ‹€. κ°€μž₯ λ¨Όμ € Vueλ₯Ό 톡해 κ°œλ°œν•  λ•ŒλŠ” webpack-dev-serverλ₯Ό μ‹€ν–‰ν•΄μ„œ κ°œλ°œν•©λ‹ˆλ‹€.

μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ 별닀λ₯Έ 섀정이 μ—†μœΌλ©΄ 8080 포트λ₯Ό μ‚¬μš©ν•˜κ²Œ λ©λ‹ˆλ‹€. λ”°λΌμ„œ, Vue 개발용 μ„œλ²„λŠ” 8081 포트λ₯Ό μ‚¬μš©ν•˜λ„λ‘ ν•©λ‹ˆλ‹€.

module.exports = {
  devServer: {
    port: 8081,
    proxy: 'http://localhost:8080',
    disableHostCheck: true
  }
}

이제 http://localhost:8081으둜 μ ‘μ†ν•˜μ—¬ μ›Ή νŽ˜μ΄μ§€λ₯Ό κ°œλ°œν•  수 있게 되고 ν”„λ‘μ‹œ 섀정을 톡해 Vue 개발용 μ„œλ²„κ°€ μ²˜λ¦¬ν•˜μ§€ λͺ»ν•˜λŠ” λͺ¨λ“  μš”μ²­μ€ 8080 포트둜 μš”μ²­ν•©λ‹ˆλ‹€. λ”°λΌμ„œ, Vue μ»΄ν¬λ„ŒνŠΈ λ‚΄μ—μ„œ μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μ œκ³΅ν•˜λŠ” APIλ₯Ό ν˜ΈμΆœν•  수 있게 λ©λ‹ˆλ‹€.

API ν˜ΈμΆœμ„ μœ„ν•΄ jQuery.ajax λ˜λŠ” axiosλ₯Ό μ‚¬μš©ν•˜λŠ” 것은 λ³Έ κΈ€μ˜ 주된 관심사가 μ•„λ‹™λ‹ˆλ‹€.

Production build

Vue ν”„λ‘œμ νŠΈλ‘œ κ°œλ°œν•œ ν”„λ‘ νŠΈμ—”λ“œ ν΄λΌμ΄μ–ΈνŠΈλŠ” build λͺ…령을 μ‚¬μš©ν•˜μ—¬ λΉŒλ“œν•  수 μžˆμŠ΅λ‹ˆλ‹€.

npm run build

μœ„ λͺ…령을 톡해 λΉŒλ“œλ˜λŠ” νŒŒμΌμ€ dist/ 폴더에 μƒμ„±λ©λ‹ˆλ‹€. λ”°λΌμ„œ, src/main/vue/dist에 λ§Œλ“€μ–΄μ§€κ²Œ λ©λ‹ˆλ‹€. λ§Œμ•½, μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ λΉŒλ“œν•˜μ—¬ μ‹€ν–‰ν•˜μ˜€λ‹€λ©΄ ν•΄λ‹Ή νŒŒμΌλ“€μ€ μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ λ¦¬μ†ŒμŠ€λ₯Ό 읽어 배포할 수 μ—†κ²Œ λ©λ‹ˆλ‹€.

μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ 기본적으둜 λ¦¬μ†ŒμŠ€λ₯Ό 읽어 λ°°ν¬ν•˜λŠ” κ²½λ‘œλŠ” spring.web.resources.static-locations ν”„λ‘œνΌν‹°λ‘œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. μ €λŠ” src/main/resources/static/distλ₯Ό Vue ν”„λ‘œμ νŠΈμ˜ λΉŒλ“œ 경둜둜 작고 μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ ν•΄λ‹Ή λΉŒλ“œ νŒŒμΌμ„ 배포할 수 있게 μ„€μ •ν•˜λ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€.

λ¨Όμ € μ• ν”Œλ¦¬μΌ€μ΄μ…˜ ν”„λ‘œνΌν‹°μ— 클래슀패슀λ₯Ό κΈ°μ€€μœΌλ‘œ static/dist의 λ¦¬μ†ŒμŠ€λ₯Ό 읽을 수 있게 ν•©λ‹ˆλ‹€.
application.properties

spring.web.resources.static-locations=classpath:/META-INF/resources/, classpath:/resources/, classpath:/static/, classpath:/public/, classpath:/static/dist

그리고 Vue ν”„λ‘œμ νŠΈ λΉŒλ“œ κ²½λ‘œλŠ” outputDir둜 μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
vue.config.js

const path = require('path')

module.exports = {
  outputDir: path.resolve(__dirname, '../resources/static/dist')
}

Freemarker Template Engine

Vue ν”„λ‘œμ νŠΈλ‘œ λΉŒλ“œλœ ν”„λ‘ νŠΈμ—”λ“œ ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μœ„ν•œ νŒŒμΌλ“€μ€ html-webpack-plugin에 μ˜ν•΄ λ§Œλ“€μ–΄μ§€λŠ” index.html에 μžλ™μœΌλ‘œ νŒŒμΌλ“€μ΄ ν¬ν•¨λ©λ‹ˆλ‹€. 그런데 μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ ν…œν”Œλ¦Ώ 엔진을 μ‚¬μš©ν•˜μ—¬ νŽ˜μ΄μ§€μ— λŒ€ν•œ μ •λ³΄λ‚˜ μ„Έμ…˜ 정보λ₯Ό μ›Ή νŽ˜μ΄μ§€μ— ν¬ν•¨μ‹œν‚€κ³  싢을 수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, Spring Initializrμ—μ„œ ν”„λ¦¬λ§ˆμ»€λ₯Ό ν…œν”Œλ¦Ώ μ—”μ§„μœΌλ‘œ μ‚¬μš©ν•˜κΈ° μœ„ν•˜μ—¬ μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•˜μ˜€λ‹€λ©΄ src/main/resources/templates ν•˜μœ„μ— μœ„μΉ˜ν•œ *.ftlh을 View둜 μ œκ³΅ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ”°λΌμ„œ, λ‹€μŒκ³Ό 같이 indexPathλ₯Ό μ„€μ •ν•˜μ—¬ index.ftlh이 λ§Œλ“€μ–΄μ§€λŠ” μœ„μΉ˜λ₯Ό 지정할 수 μžˆμŠ΅λ‹ˆλ‹€.
vue.config.js

module.exports = {
  indexPath: '../../templates/index.ftlh'
}

html-webpack-plugin

μ•žμ„œ index νŒŒμΌμ€ html-webpack-plugin에 μ˜ν•΄ λ§Œλ“€μ–΄μ§„λ‹€κ³  ν•˜μ˜€μŠ΅λ‹ˆλ‹€. μš°λ¦¬κ°€ μœ„μ—μ„œ indexPathλ₯Ό μ§€μ •ν•œ 것은 λ‹¨μˆœνžˆ ν™•μž₯자λ₯Ό .ftlh둜 λ°”κΎΌ 것과 λ‹€λ₯Ό λ°” μ—†μŠ΅λ‹ˆλ‹€. ν…œν”Œλ¦Ώ 엔진을 μ‚¬μš©ν•˜λŠ” λͺ©μ μ€ ν•΄λ‹Ή ν…œν”Œλ¦Ώ μ—”μ§„μ—μ„œ μ§€μ›ν•˜λŠ” λ¬Έλ²•μœΌλ‘œ 정보λ₯Ό ν‘œν˜„ν•˜κΈ° μœ„ν•¨μž…λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ ν”ŒλŸ¬κ·ΈμΈμ— λŒ€ν•œ μ˜΅μ…˜μ„ λ³€κ²½ν•˜λ„λ‘ ν•©λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄, <html> νƒœκ·Έμ— lang 속성에 μš”μ²­μ— λ”°λ₯Έ 언어값을 λΆ€μ—¬ν•˜κ³  싢을 수 μžˆμŠ΅λ‹ˆλ‹€. 그러면 λ‹€μŒκ³Ό 같이 index.ftlhκ°€ λ§Œλ“€μ–΄μ Έμ•Ό ν•©λ‹ˆλ‹€.

<!DOCTYPE html>
<html lang="${.locale?split("_")[0]}">
</html>

μ•„μ‰½κ²Œλ„ html-webpack-plugin은 μœ„ λ‚΄μš©μ„ 읽닀가 값을 μ²˜λ¦¬ν•  수 μ—†μ–΄ 였λ₯˜λ₯Ό λ³΄μ—¬μ€λ‹ˆλ‹€. λ‹€ν–‰μŠ€λŸ½κ²Œλ„ μ•½κ°„μ˜ νŠΈλ¦­μ„ μ“°λ©΄μ„œ ν”ŒλŸ¬κ·ΈμΈ μ˜΅μ…˜μ„ κ±΄λ“œλ €μ„œ κ°€λŠ₯ν•˜κ²Œ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

index.html

<!DOCTYPE html>
<html lang="<%= '\${.locale?split(\"_\")[0]}' %>">
</html>

vue.config.js

module.exports = {
  chainWebpack: config => {
    config.plugin('html')
        .tap(args => {
          args[0].minify = false
          args[0].interpolate = true
          return args
        })
  }
}

minify μ˜΅μ…˜μ„ λ„λŠ” 것은 μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ ν”„λ¦¬λ§ˆμ»€ ν…œν”Œλ¦Ώ μ—”μ§„μœΌλ‘œ index.ftlh을 읽을 λ•Œ λ°œμƒν•˜λŠ” 였λ₯˜λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•¨μž…λ‹ˆλ‹€.

이제 λΉŒλ“œλœ index.ftlhλŠ” λ‹€μŒκ³Ό 같이 λ§Œλ“€μ–΄μ§‘λ‹ˆλ‹€.

<!DOCTYPE html>
<html lang="${.locale?split("_")[0]}">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="/favicon.ico">
    <title></title>
  <link href="/js/about.d6d714df.js" rel="prefetch"><link href="/css/app.f94ee837.css" rel="preload" as="style"><link href="/css/chunk-vendors.01c183df.css" rel="preload" as="style"><link href="/js/app.247cb7a3.js" rel="preload" as="script"><link href="/js/chunk-vendors.65fb301b.js" rel="preload" as="script"><link href="/css/chunk-vendors.01c183df.css" rel="stylesheet"><link href="/css/app.f94ee837.css" rel="stylesheet"></head>
  <body>
    <noscript>
      <strong>We're sorry but vue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  <script type="text/javascript" src="/js/chunk-vendors.65fb301b.js"></script><script type="text/javascript" src="/js/app.247cb7a3.js"></script></body>
</html>

이제 μ—¬λŸ¬λΆ„λ„ μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ Vue와 ν•¨κ»˜ κ°œλ°œν•  수 있게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€. κ°μ‚¬ν•©λ‹ˆλ‹€. πŸ˜€