언어·프레임워크·데이터베이스

[Docker] 핫 리로드 적용 기록: Spring Boot와 Vite+React

devracoon 2026. 1. 27. 16:18

핫 리로드 기능은 개발 환경에서의 생산성에 큰 영향을 미칩니다. 이 글은 도커 컨테이너에 올릴 Spring boot 프로젝트와 Vite + React 프로젝트를 위한 핫 리로드를 설정하면서 겪은 시행착오에 대한 기록입니다.

 

스프링부트에 핫리로드 적용하기

1. spring-boot-devtools 의존성

먼저, Spring Boot에서의 핫리로드 기능은 spring-boot-devtools에서 지원하는 것이기 때문에, 의존성 추가 및 설치를 해줍니다.

// build.gradle.kts
dependencies {
    developmentOnly("org.springframework.boot:spring-boot-devtools")
}​

 

2. 바인드 마운트 설정

spring-boot-devtools에서 변경을 감지하고 앱을 재시작하도록 하려면,

우리가 변경하는 소스가 즉시 컨테이너로 옮겨지도록 하는 설정이 필요합니다.

# compose.yml의 일부
services:
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    ports:
      - "8080:8080"
    volumes:
      - ./backend:/app # << 요것
      - gradle-cache:/root/.gradle

 

제 프로젝트는 리포지토리의 루트 아래에 /backend, /frontend가 있는 구조이기 때문에 build.context를 ./backend로 지정했습니다.

핵심이 되는 설정은 `./backend:/app` 부분입니다.

이걸 바인드마운트라고 하는데, 호스트, 그러니까 제 로컬 컴퓨터의 `./backend` 디렉토리를 컨테이너가 `/app`로 인식하게 합니다.

그러면 제가 로컬에서 작업한 변경사항을 컨테이너의 /app에서 시작된 스프링부트가 즉시 알 수 있게 되겠죠.

 

3. bootRun

마지막 단계입니다.

Dockerfile에서 프로세스를 올릴 때, 반드시 bootRun으로 실행해줘야 spring-boot-devtools의 핫리로드 기능이 활성화됩니다.

# Dockerfile
FROM eclipse-temurin:21-jdk

WORKDIR /app

COPY gradlew .
COPY gradle gradle
COPY build.gradle.kts settings.gradle.kts ./

RUN ./gradlew dependencies || true

CMD ["./gradlew", "bootRun"]

 

 

Vite+React에 핫리로드 적용하기

스프링부트처럼 하면 Vite에서도 마찬가지로 동작할 줄 알았습니다. 그러나 그렇지가 않습니다.

compose up을 하고 접속이 안됩니다. 로그를 확인해보면 에러를 확인할 수 있습니다.

/app/node_modules/rollup/dist/native.js:86
                throw new Error(
                      ^

Error: Cannot find module @rollup/rollup-linux-x64-musl. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). Please try `npm i` again after removing both package-lock.json and node_modules directory.
    at requireWithFriendlyError (/app/node_modules/rollup/dist/native.js:86:9)
    {{...중략}}
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:202:7) {
  [cause]: Error: Cannot find module '@rollup/rollup-linux-x64-musl'
  Require stack:
  - /app/node_modules/rollup/dist/native.js
      at Function._resolveFilename (node:internal/modules/cjs/loader:1383:15)
      {{...중략}}
  }
}

SpringBoot는 됐는데 왜 node는 안되는가

왜냐하면, springboot의 의존성은 유저의 홈 디렉토리 아래에 저장됩니다. (`~/.gradle`)

반면에 `node_modules` 의존성은 프로젝트 루트 아래에 생성됩니다.

우리가 방금 했던 것처럼 바인드 마운트를 해주면, 프로젝트 루트의 모든 내용을 컨테이너와 공유하게 됩니다.

그리고 제 로컬의 윈도우용 `node_modules`가 컨테이너의 리눅스용 `node_modules`를 덮어쓰게 만듭니다.

 

1. 익명 볼륨 설정으로 덮어씌워짐 방지하기

컨테이너의 `node_modules`를 익명 볼륨으로 설정하는 것으로 이 문제를 해결할 수 있습니다.

컨테이너의 어떤 디렉토리를 익명볼륨을 설정하면, 그 디렉토리는 바인드 마운트에 우선하게 됩니다.

즉, 덮어씌워지지 않습니다. (그러나 수명 주기는 컨테이너와 같이 합니다.)

# compose.yml의 일부
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    ports:
      - "5173:5173"
    volumes:
      - ./frontend:/app
      - /app/node_modules

 

익명 볼륨은 컨테이너 down시에 -v 옵션을 주지 않고 내리면, 주인을 잃고 디스크 용량 먹는 괴물이 된다고 하니 주의합시다.

뭐, 괴물이 돼도 `docker volume prune`으로 해결할 수 있습니다.

 

2. npm run dev

FROM node:22-alpine
WORKDIR /app

COPY package.json .
RUN npm install

COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]

 

이제는 잘 실행되는 우리 프론트엔드를 만나실 수 있습니다.