Hot Module Reloading Module in Docker

Docker의 Container 에서 Hot Module Reloading Module 실행하기

이전에 간단하게 Nodejs의 서버를 띄운 후, Docker의 이미지로 만들어 컨테이너를 실행시키는 간단한 예제를 한적이 있었다. 하지만 사실 그 당시 내가 원했던 것은 로컬 Dev 서버를 이미지로 만든 후, sync를 맞추는 것이었다. 현재 진행 중인 사이드 프로젝트의 개발 환경에 대한 셋팅이 많아 그것을 Docker의 이미지로 만들어 두어, 혹여라도 새로운 프론트/백엔드 개발자가 프로젝트에 합류한다면 쉽고 빠르게 개발 환경을 셋팅을 하게 하기 위해서이다. 물론 Docker를 이용한 배포 자동화 역시 안에 목적에 포함되어 있긴 하지만, 아직 배포 자동화까지는 고려할 단계가 아니라서 배제해두고 생각했다.

현재 진행 중인 프로젝트에 docker-compose를 이용해서 image를 만든 후, 컨테이너로 띄우는 것 까진 성공을 하였다. 물론 volume을 맞춰주어서 로컬에서 소스를 수정하면 컨테이너 안의 소스 역시 수정이 되긴 했다. 하지만 문제는 개발 소스는 수정이 되어도, hot reloading이 실행되지 않았다. 이번에는 간단하게 vuejs 프로젝트와 express 서버를 연결하여 어플리케이션을 컨테이너로 띄운 후, hot reloading module이 실행될 수 있게 하는 것이 목적이다. 외국의 관련된 자료들이 많아 참고하였으며 포스트 하단에 출처를 기재하였으니 혹여라도 원문을 보고 싶으면 하단의 링크를 참고하면 된다.

Github 링크는 다음과 같다.

Express 서버 구축

먼저 Express 서버를 cli를 이용하여 구축하면 쉽게 구축이 가능 지금 목적은 express 서버가 아니므로, 그냥 index.html에 route 만 연결해주는 용도로만 사용한다.(혹시나 프로젝트 스케폴딩을 위한 목적이라면 cli를 이용하여 설치하는 것을 추천한다.) 먼저 express와 nodemon을 설치한다.

1
$ npm install express nodemon --save

그 후 여타 필요한 최소한의 모듈만 설치해 준다.

1
$ npm install ejs-local body-parser --save-dev

필요한 모듈이 설치되었다면 프로젝트의 root에 app.js를 파일을 만든 후, 아래와 같이 코드를 작성해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const express = require('express');
const app = express();
const port = 4000;
const engine = require('ejs-locals');
const bodyParser = require('body-parser');

app.use(bodyParser.urlencoded({ extended: false }));
app.set('views', __dirname);
app.set('view engine', 'ejs');
app.engine('html', engine);

const route = (() => {
app.get('/', (req, res) => {
res.render('index.html');
})
})();

let server = app.listen(port, () => {
console.log(`Express server has started on port:${port}`);
});

주목적이 docker를 셋팅하기 위한 기본적인 세팅이기에 cli를 이용하여 서버 환경을 구성하지도 않거니와 따로 router를 물리적으로 분리시키지도 않았다. 위와 같이 설정을 한 후에 서버를 띄워주면 일단 기본적인 express를 이용한 구축 환경을 마무리 되었다.

1
2
3
4
5
$ nodemon ./app.js

[nodemon] 1.15.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*

일단 위와 같이 작성 되었으면 localhost:4000에서 서버가 띄워진 것을 확인할 수 있다.

VueJS 프로젝트 생성 후, Express와 연결하기

Vuejs 같은 경우는 CLI를 이용해서 프로젝트를 구성하였다. CLI로 이용해서 깔리는 Vue 프로젝트의 build 관련 옵션을 살펴보면 dist의 static 안에 해당 bundle된 파일이 쌓이는 것을 확인할 수 있다. 그래서 다시 Express의 app.js에서 해당하는 static 파일 들에 대한 옵션을 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express');
const app = express();
const port = 4000;
const engine = require('ejs-locals');
const bodyParser = require('body-parser');

app.use(bodyParser.urlencoded({ extended: false }));
app.set('views', __dirname);
app.set('view engine', 'ejs');
app.engine('html', engine);

const route = (() => {
app.get('/', (req, res) => {
res.render('index.html');
})
})();

// express의 static 디렉토리 경로를 추가
let server = app.use(express.static('dist')).listen(port, () => {
console.log(`Express server has started on port:${port}`);
});

이렇게 추가를 한 후, 빌드를 한 후 localhost:4000으로 접속하면 아래와 같은 화면이 나오는 것을 확인할 수 있다.

/images/vue/express-vue01.png

여기까지 확인이 되었다면 일단 Docker를 세팅하기 위한 준비는 마무리 되었다고 볼 수 있다.

배포용 이미지 파일 만들기

위와 같은 파일이 생성되었으면 이제 docker image로 만들기 위한 준비가 필요하다. 일단은 먼저 Dockerfile을 생성한 후 아래의 내용을 붙혀넣는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM node:carbon

RUN mkdir /usr/src/app
WORKDIR /usr/src/app
ENV NODE_ENV=local

ADD package.json /usr/src/app/package.json
RUN npm install nodemon -g

RUN npm install
EXPOSE 4000

CMD npm run server

위의 파일이 완료되었으면 docker-compose.yml 파일을 생성 후 아래의 내용을 붙혀준 후에 이미지를 빌드한다.

1
2
3
4
5
6
7
8
9
10
11
12
version: '2'
services:
frontend:
hostname: docker-vue-sample
working_dir: /usr/src/app
build:
context: ./
ports:
- "4000:4000"
restart: always
volumes:
- "./:/usr/src/app"

아래와 같이 compose를 이용해 docker 이미지 빌드한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
$ docker-compose build

Building frontend
Step 1/9 : FROM node:carbon
---> b87c2ad8344d
Step 2/9 : RUN mkdir /usr/src/app
---> Using cache
---> f61f9d1b391b
Step 3/9 : WORKDIR /usr/src/app
---> Using cache
---> c25a87610a2c
Step 4/9 : ENV NODE_ENV=local
---> Running in e73a68fc78c4
---> b08816909513
Step 5/9 : ADD package.json /usr/src/app/package.json
---> a8077b7c4c25
Step 6/9 : RUN npm install nodemon -g
---> Running in 56e90eef2476
/usr/local/bin/nodemon -> /usr/local/lib/node_modules/nodemon/bin/nodemon.js

> nodemon@1.15.0 postinstall /usr/local/lib/node_modules/nodemon
> node -e "console.log('\u001b[32mLove nodemon? You can now support the project via the open collective:\u001b[22m\u001b[39m\n > \u001b[96m\u001b[1mhttps://opencollective.com/nodemon/donate\u001b[0m\n')" || exit 0

Love nodemon? You can now support the project via the open collective:
> https://opencollective.com/nodemon/donate

npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.1.3 (node_modules/nodemon/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

+ nodemon@1.15.0
added 252 packages in 11.338s
---> 996861500b0e
Step 7/9 : RUN npm install
---> Running in d2eb13f862c2
npm WARN deprecated ejs@0.8.8: Critical security bugs fixed in 2.5.5

> uglifyjs-webpack-plugin@0.4.6 postinstall /usr/src/app/node_modules/webpack/node_modules/uglifyjs-webpack-plugin
> node lib/post_install.js


> nodemon@1.15.0 postinstall /usr/src/app/node_modules/nodemon
> node -e "console.log('\u001b[32mLove nodemon? You can now support the project via the open collective:\u001b[22m\u001b[39m\n > \u001b[96m\u001b[1mhttps://opencollective.com/nodemon/donate\u001b[0m\n')" || exit 0

Love nodemon? You can now support the project via the open collective:
> https://opencollective.com/nodemon/donate

added 1324 packages in 29.121s
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN ajv-keywords@3.1.0 requires a peer of ajv@^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.1.3 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

---> 6e75dabdf990
Step 8/9 : EXPOSE 4000
---> Running in bc87f0beb893
---> f3dba9f3c6e7
Step 9/9 : CMD npm run server
---> Running in 115f4f80ba9a
---> 63f07e977741
Removing intermediate container e73a68fc78c4
Removing intermediate container 56e90eef2476
Removing intermediate container d2eb13f862c2
Removing intermediate container bc87f0beb893
Removing intermediate container 115f4f80ba9a
Successfully built 63f07e977741
Successfully tagged dockersamplevuejs_frontend:latest

설치가 완료되었으면 image가 제대로 빌드되었는지 images 명령어를 이용하여 확인한다.

1
2
3
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dockersamplevuejs_frontend latest 63f07e977741 2 minutes ago 811MB

위와 같이 확인이 되었다면 이제 컨테이너로 실행을 시켜본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
$ docker-compose up

Creating dockersamplevuejs_frontend_1 ...
Creating dockersamplevuejs_frontend_1 ... done
Attaching to dockersamplevuejs_frontend_1
frontend_1 |
frontend_1 | > docker-sample-vuejs@1.0.0 server /usr/src/app
frontend_1 | > nodemon ./app.js
frontend_1 |
frontend_1 | [nodemon] 1.15.0
frontend_1 | [nodemon] to restart at any time, enter `rs`
frontend_1 | [nodemon] watching: *.*
frontend_1 | [nodemon] starting `node ./app.js`

위와 같이 컨테이너를 실행시켜주면 로컬에서 띄우던 화면과 동일한 화면이 뜨게 된다. 여기서 띄우는 화면은 docker의 IP로 접속해서 띄워야 한다. 혹시나 IP를 모르고 있다면 아래의 명령어를 이용해서 확인할 수 있다.

1
$ docker-machine ip

컨테이너에 해당하는 화면이 떴다면 일단 배포용 이미지는 완료가 되었다고 볼 수 있다.

개발용 이미지 생성 및 Hot reloading Module과 연동하기

위에까지 해서 배포용 이미지까지는 빌드가 완료되었다. 이제는 개발용 이미지를 빌드하면 된다.


출처

현재 이커머스회사에서 frontend 개발자로 업무를 진행하고 있는 Martin 입니다. 글을 읽으시고 궁금한 점은 댓글 혹은 메일(hoons0131@gmail.com)로 연락해주시면 빠른 회신 드리도록 하겠습니다. 이 외에도 네트워킹에 대해서는 언제나 환영입니다.:Martin(https://github.com/martinYounghoonKim
Vue의 Lifecycle
Express Server HMR