공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 08 / 22

Vue 3 SPA 와 Java 서버 연동

정적 서빙, 같은 출처 API, 폐쇄망 라이브러리
섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 SPA 와 서버 렌더링 차이

JSP·Thymeleaf 는 요청마다 서버가 완성된 HTML 문자열을 만들어 내려줍니다. SPA 는 처음에 빈 HTML 한 장만 받고, 그 안의 JS 가 API 를 호출해 받은 JSON 으로 화면을 그립니다. 서버가 하던 "HTML 생산" 역할이 브라우저 쪽으로 옮겨간 셈입니다.

항목 JSP·Thymeleaf Vue SPA
화면 생성 주체 서버(템플릿 엔진) 브라우저(JS)
서버 응답 완성된 HTML 빈 HTML + JSON
새로고침 시 동작 서버가 다시 그려서 반환 JS 가 다시 API 호출
서버 코드 위치 컨트롤러 + 뷰 템플릿 컨트롤러(API) + 정적 파일

서버 입장에서는 결국 "정적 파일 몇 개"와 "JSON 을 내려주는 API" 두 가지 책임만 남습니다. 이 레슨의 Main 이 두 책임을 경로별로 나눈 이유입니다.

SPA 는 검색 엔진 노출(SEO)이나 첫 화면 표시 속도에서 서버 렌더링보다 불리할 수 있습니다. 사내 업무 시스템은 검색 노출이 필요 없고 로그인 뒤에만 쓰이므로 이 단점이 거의 문제가 안 됩니다. 반대로 화면 전환이 잦은 목록·상세·모달 같은 UI 는 SPA 가 압도적으로 편합니다.

2.2 배포 구조 두 가지

배포 방식은 크게 두 가지입니다.

  • (A) 같은 출처: Java 서버 하나가 정적 파일과 API 를 함께 서빙합니다. 브라우저 기준으로 둘 다 같은 도메인·포트라 CORS 설정이 필요 없고, 배포도 서버 하나만 올리면 끝입니다. 이 레슨의 구조입니다.
  • (B) 분리: Nginx 가 Vue 빌드 산출물을 서빙하고, Java 는 API 만 담당합니다. 도메인·포트가 다르면 CORS 또는 리버스 프록시가 필요합니다.

개발 중에는 Vite dev server 의 proxy 설정으로 (B) 처럼 동작시키다가, 배포할 때 빌드 산출물을 Java 서버의 정적 폴더로 옮겨 (A) 로 합치는 방식이 실무에서 흔합니다. Spring Boot 는 src/main/resources/static 폴더에 파일을 두면 별도 설정 없이 그대로 서빙합니다.

javascript
// vite.config.js: 개발 중에만 8080 으로 API 요청을 대신 전달
export default {
  server: { proxy: { '/api': 'http://localhost:8080' } },
};

이 설정은 개발용 dev server(보통 5173 포트)에서 실행할 때만 적용됩니다. npm run build 로 만든 산출물에는 포함되지 않으므로, 배포 시점에는 반드시 (A) 나 (B) 중 하나로 실제 서빙 경로를 정해야 합니다.

배포 전에 아래 표로 구조를 다시 확인하면 CORS 설정을 빼먹는 실수를 줄일 수 있습니다.

확인 항목 (A) 같은 출처 (B) 분리 배포
CORS 설정 불필요 API 서버에 허용 출처 지정
정적 파일 위치 Java 서버의 static 폴더 Nginx 문서 루트
배포 단위 서버 1개 서버 2개(또는 Nginx+Java)
SPA 폴백 담당 Java 서버 Nginx try_files

2.3 정적 파일 서빙의 세 가지 책임

정적 파일 서버는 단순해 보이지만 세 가지를 반드시 챙겨야 합니다.

  • MIME 타입: Content-Type 없이 JS 를 내려주면 브라우저가 nosniff 정책 아래에서 실행을 거부할 수 있습니다. 확장자별로 타입을 정확히 지정해야 합니다.
  • 캐시 정책: 외부 라이브러리(Vue 본체)는 내용이 안 바뀌니 max-age=31536000, immutable 로 1년 캐시해도 됩니다. index.html 은 배포마다 바뀌므로 no-cache 로 매번 서버에 확인시킵니다.
  • 경로 탐색 방지: .. 가 섞인 경로로 폴더 밖 파일을 읽으려는 시도를 막아야 합니다.

StaticHandler 는 이 세 가지를 한 메서드에서 처리합니다.

java
// StaticHandler.handle 핵심부
Path file = root.resolve(uri.substring(1)).normalize();
if (!file.startsWith(root)) { send(ex, 404, "text/plain", "not found"); return; }
// ... 확장자별 MIME, /js/plugin/ 이면 1년 캐시, 아니면 no-cache

resolve 로 합친 경로를 normalize 로 정리한 뒤 root 로 시작하는지 검사합니다. ../../Main.java 같은 경로는 정규화 후에도 root 밖으로 나가므로 여기서 걸러집니다.

Cache-Control 값도 파일의 성격에 따라 다르게 정해야 합니다. 값을 잘못 고르면 "라이브러리를 매번 새로 받아 느리다"거나 "배포했는데 화면이 안 바뀐다" 둘 중 하나가 됩니다. 이 판단 기준은 2.5 에서 다시 다룹니다.

StaticHandler.MIME 이 아는 확장자 목록을 표로 보면 어떤 파일이 어떤 타입으로 나가는지 한눈에 보입니다.

확장자 Content-Type
html text/html; charset=utf-8
js application/javascript; charset=utf-8
css text/css; charset=utf-8
json application/json
svg image/svg+xml
woff2 font/woff2

목록에 없는 확장자는 application/octet-stream 으로 내려가 브라우저가 다운로드로 처리합니다. 새 파일 형식을 쓰게 되면 이 맵에 항목을 추가해야 합니다.

2.4 SPA history 모드 폴백

Vue Router 의 history 모드를 쓰면 주소창에 /todos/3 같은 경로가 그대로 보입니다. 이 상태에서 새로고침하면 브라우저는 서버에 /todos/3 을 요청합니다. 서버에는 그런 파일이 없으니, 확장자가 없는 경로는 index.html 을 대신 돌려주고 Vue Router 가 브라우저에서 그 경로를 다시 해석하게 합니다.

반대로 확장자가 있는 경로, 예를 들어 /css/none.css 는 진짜로 없는 파일이므로 404 를 그대로 유지합니다. 이 구분이 없으면 오타 난 .js 경로가 HTML 로 내려와 콘솔에 알아보기 힘든 문법 오류가 뜹니다.

Spring Boot 에서는 컨트롤러로 같은 일을 합니다.

java
@Controller
class SpaFallbackController {
    // 확장자 없는 모든 경로를 index.html 로 전달
    @GetMapping("/{path:[^\\.]*}")
    String fallback() { return "forward:/index.html"; }
}

2.5 폐쇄망 라이브러리 관리

<script src="https://unpkg.com/vue@3"> 같은 CDN 스크립트는 폐쇄망에서 인터넷이 없으므로 무조건 실패합니다. 해결은 간단합니다. 인터넷이 되는 PC 에서 Vue 의 전역 빌드 파일 하나를 내려받아 static/js/plugin/ 에 두고, <script src="/js/plugin/vue.global.prod.js"> 로 로드합니다.

전역 빌드는 window.Vue 객체 하나를 만들고, 템플릿 컴파일러까지 포함해서 별도 빌드 도구 없이 동작합니다. .vue 단일 파일 컴포넌트를 쓰려면 Vite 같은 빌드 도구가 필요하지만, 이 레슨처럼 HTML 안에 템플릿을 직접 쓰는 방식은 필요 없습니다.

빌드 종류 특징 언제 쓰는가
vue.global.prod.js window.Vue, 컴파일러 포함 빌드 도구 없이 HTML 에 직접
vue.esm-browser.js ES 모듈, import 문 사용 빌드 도구 없이 모듈만 나누고 싶을 때
SFC + Vite .vue 파일, 빌드 필요 컴포넌트가 많고 빌드 환경이 있을 때

라이브러리 파일명에 버전을 넣거나 plugin/vue-3.5.13/ 처럼 버전 폴더로 관리하면, 버전을 올릴 때 경로 자체가 바뀌어 브라우저 캐시가 자동으로 무효화됩니다. 파일명을 그대로 덮어쓰면 오래된 캐시가 남아 있는 브라우저가 새 버전을 못 받을 수 있습니다.

폐쇄망에 파일을 들여오는 절차도 정해 두면 편합니다. 인터넷이 되는 PC 에서 공식 배포처의 dist/vue.global.prod.js 를 내려받고, 해시나 버전 번호를 기록해 둔 뒤, 반입 절차(보안팀 승인, 매체 검사 등)를 거쳐 폐쇄망으로 옮깁니다. 사내에 이미 반입 이력이 있는 버전을 재사용하면 이 절차를 반복하지 않아도 됩니다.

이 레슨의 데모에서 vue.global.prod.js 는 154807자, 약 150KB 입니다. Vue Router·Pinia 같은 부가 라이브러리를 더할수록 파일 수와 반입 절차가 늘어나므로, 팀에서 실제로 쓸 라이브러리 목록을 먼저 정하고 한 번에 반입하는 편이 낫습니다.

2.6 fetch 래퍼와 에러 규격

app.js 의 api() 함수는 모든 호출이 지나가는 통로입니다. 07 레슨의 에러 규격 {"status","message"} 을 여기서 한 번에 처리합니다.

javascript
async function api(method, url, body) {
  const res = await fetch(url, { method, /* ... */ });
  if (res.status === 204) return null;      // DELETE 본문 없음
  const data = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${data.message}`);
  return data;
}

axios 를 쓴다면 이 역할은 응답 인터셉터가 대신합니다. res.ok 로 성공·실패를 가리고, 실패면 서버가 보낸 message 를 그대로 Error 에 담아 호출한 쪽에서 catch 로 받게 합니다.

이 레슨의 화면은 변경 요청 후 항상 목록을 다시 조회합니다. 서버가 데이터의 단일 진실 원천이라는 원칙을 지키는 가장 단순한 방식입니다. 응답 지연을 줄이려는 낙관적 UI(먼저 화면을 바꾸고 실패하면 되돌리기)는 더 복잡하므로 뒤에서 따로 다룹니다.

같은 출처 배포에서는 브라우저가 CORS 검사를 생략합니다. 요청을 보내는 페이지의 출처(스킴·호스트·포트)와 API 의 출처가 같기 때문입니다. 분리 배포(2.2 의 B)에서는 API 서버가 Access-Control-Allow-Origin 헤더로 어느 출처의 요청을 허용할지 명시해야 브라우저가 응답을 JS 에 넘겨줍니다.

PATCH·DELETE 처럼 단순 요청 범위를 벗어나는 메서드는 브라우저가 실제 요청 전에 OPTIONS 로 preflight 요청을 먼저 보냅니다. 서버가 OPTIONS 에 정상 응답을 안 하면 본 요청 자체가 나가지 않아, 07 레슨의 CORS 핸들러처럼 OPTIONS 전용 처리가 따로 필요합니다.

2.7 Vue 3 Composition API 최소 요소

이 레슨에서 쓰는 Vue API 는 다섯 개뿐입니다. createApp 으로 앱을 만들고, setup() 안에서 상태와 함수를 정의해 반환합니다. ref 는 반응형 값 하나를 감싸고, computed 는 다른 값에서 파생되는 값을 정의하며, onMounted 는 화면이 그려진 뒤 한 번 실행할 코드를 등록합니다.

템플릿에서 쓰는 디렉티브도 다섯 개로 충분합니다. v-model 은 입력과 상태를 양방향으로 묶고, v-for 는 목록을 반복하며 :key 가 필수입니다. v-if/v-else-if 는 조건 분기, @submit.prevent 는 폼 기본 동작(새로고침)을 막고 핸들러만 실행하며, :disabled 는 속성값을 동적으로 바인딩합니다.

반응성의 핵심은 두 문장으로 요약됩니다. ref 로 만든 값은 JS 코드에서 항상 .value 로 접근해야 합니다. 다만 템플릿 안에서는 Vue 가 자동으로 언래핑하므로 .value 없이 todos.length 처럼 바로 씁니다.

이 다섯 API 와 다섯 디렉티브만 표로 정리하면 이 레슨의 화면 코드 전체를 설명할 수 있습니다.

이름 종류 역할
createApp API 앱 인스턴스를 만들고 .mount() 로 DOM 에 붙인다
ref API 값 하나를 반응형으로 감싼다, JS 에서는 .value
computed API 다른 반응형 값에서 파생되는 값을 정의한다
onMounted API 화면이 처음 그려진 뒤 한 번 실행한다
v-model 디렉티브 입력과 상태를 양방향으로 묶는다
v-for 디렉티브 목록을 반복한다, :key 필수
v-if/v-else-if 디렉티브 조건에 따라 다른 요소를 그린다
@submit.prevent 디렉티브 폼 기본 제출(새로고침)을 막는다
:disabled 디렉티브 속성값을 데이터에 맞춰 동적으로 바꾼다
핵심 원리
  • 2.1 SPA 와 서버 렌더링 차이
  • 2.2 배포 구조 두 가지
  • 2.3 정적 파일 서빙의 세 가지 책임
  • 2.4 SPA history 모드 폴백
  • 2.5 폐쇄망 라이브러리 관리
  • 2.6 fetch 래퍼와 에러 규격
  • 2.7 Vue 3 Composition API 최소 요소
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제