핫 리로드 기능은 개발 환경에서의 생산성에 큰 영향을 미칩니다. 이 글은 도커 컨테이너에 올릴 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"]
이제는 잘 실행되는 우리 프론트엔드를 만나실 수 있습니다.