[ Spring + Vite ] Spring에 Front-end Framework 연동하기

Lutica_·2024년 8월 19일

개요

  • 최근에는 Web개발에 있어서 Front-end단과 Back-end단을 분리하여 개발하는 경우가 많다.
  • 가끔씩 Back-End에 Front-End를 합치려는 경우가 있는데, 이 포스트에는 그 경우에 대비한 간단한 설명을 작성하려 한다.

개념적 정리

통신절차

  • 일반적인 웹 통신은 HTML 문서를 받은 뒤, 거기있는 문서로 JS와 이미지를 받고 데이터를 주고받는 방식인데, 잘못하면 링크가 꼬일 위험이 있다.
  • 유저입장에서는 exposed된 url만 맞춰주면 되므로, 중간과정은 생각하지 않는 방법으로 진행한다.

상세 사양

요구사양

    1. HTML에서 요구하는 자원은 서버로 향하는 위치이므로, 최종적인 컴파일 시 서버로 전달하는 url만 변형되면 된다.
    1. 그러므로, URL의 변형은 VITE에서 담당하고, 배포되는 HTML에는 변형된 링크가 최종적으로 저장되어야 한다.
    1. 서버는 이에 맞춰서 static 파일에 대한 루트를 바꿔줘야 한다.

VITE에서 적용하기

import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [svelte()],
  build:{
    outDir: "../main/resources/static/frontend", // 빌드 위치 설정
    
  },
  experimental: { // 실험적 기능
    renderBuiltUrl(filename, { hostId, hostType, type }) { // 빌드시 Url 변형
      if (type === 'public') {
        return '/static/frontend/' + filename
      } else if (path.extname(hostId) === '.js') {
        return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` }
      } else {
        return '/static/frontend/' + filename
      }
    },
  },
})

Spring에서의 적용

  • JVM계열 언어는 Compile 언어이므로, Compile시 HTML문서를 Build하게 해줘야 한다.
  • 그러므로, compile과 build이 같이 이뤄지는 설정을 진행한다.
import com.github.gradle.node.npm.task.NpmTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
	id("org.springframework.boot") version "3.3.2"
	id("io.spring.dependency-management") version "1.1.6"
	kotlin("jvm") version "1.9.24"
	kotlin("plugin.spring") version "1.9.24"
	id("com.github.node-gradle.node") version "7.0.2"
}

...

kotlin {
	compilerOptions {
		freeCompilerArgs.addAll("-Xjsr305=strict")
	}
}

...
node {
	// 이 설정이 true인 경우 node를 새로 다운 받음
    download.set(false)
    
   
     //download가 true일 경우에만 사용
     //version에 명시한 버전으로 Node.js 다운로드 및 설치
     //workDir에 설치됨
     
    //version.set('20.12.1')
    
     // npm의 버전
    //npmVersion.set('9.2.0')

}

val frontendInitTask = tasks.register<NpmTask>("frontendInitTask") {
// npm install 실행
    args.set(listOf("install"))
}

val frontendTask = tasks.register<NpmTask>("frontendTask") {

    dependsOn(frontendInitTask)
    // npm run build 실행
    args.set(listOf("run", "build"))
}

tasks.withType<KotlinCompile>{
	dependsOn(frontendTask)
	
	kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = java.sourceCompatibility.toString()
    }
}

tasks.withType<Test> {
	useJUnitPlatform()
}
  • 그렇다면, Spring 서버측은 이에 대응하여 아래의 조치를 취해야 한다.
    application.properties에서, spring.mvc.static-path-pattern=이하 도메인을 맞춰준다.
  • 본 예제와 같은 경우, static/frontend/산하로 접속하면 되게 했으므로, spring.mvc.static-path-pattern=static/**로 설정했다.

그런데.., 이렇게 하면 더럽다면?

  • 이렇게 한번 해보자.
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping


@Controller
// 예제이므로 API로 넘기는 부분은 감안하지 않았다.
// 이 wildcard는 `모든` request를 이 핸들러로 넘기므로, 사용시 주의가 필요하다.  
// 이 예제에서는 '/' route로 갔을 때 static SPA를 띄우는 것이 목표이다.
@RequestMapping("**") 
class RootApiRoot {
    @GetMapping("/")
    fun root(): String {
        return "static/frontend/index.html"
    }
}
  • 이러면, RequestMappingstatic/frontend/index.html라우터의 내용을 전달해주면서 깔끔한 메인이 나타나게 된다.

  • 하지만 알다시피, SEO측면에서는 딱히 좋은 방법은 아니다. Lazy Loading 문제가 있기 때문이다.

결론

  • Node.js하자 그러면 꼬이지는 않는다
  • 하지만 가끔 둘 다 해야할 테니 이글도 의미가 있다고 생각한다.
profile
해보고 싶고, 하고 싶은 걸 하는 사람

1개의 댓글

comment-user-thumbnail
2024년 8월 19일

사실 이걸 응용하면 cdn으로 분산 가능한데 피곤해서 쓰진 않았네요 졸려...

답글 달기