프론트엔드 개발자를 위한 Docker로 React 개발 및 배포하기

Wonkook Lee·2022년 10월 22일
89

Docker

목록 보기
1/1
post-thumbnail

Docker + React Tutorial: From Development to Production Workflow

원제: Docker + ReactJS tutorial: Development to Production workflow + multi-stage builds + docker compose

이 포스트는 Youtube의 Sanjeev Thiyagarajan라는 분이 올려주신 Docker + ReactJS tutorial 영상을 쉽게 따라하고 이해할 수 있도록 정리한 내용이다.

prerequisite

  • 원본 영상은 Windows, 본 설명은 Mac OS 환경에서 진행된다.
  • NodeJS
  • Docker
  • CRA 등을 통해 초기화된 리액트 앱 (또는 본인의 프로젝트)
  • Docker, Container 개념에 대한 기본 지식

1) Setting Docker Container

1-1) hub.docker.com

hub.docker.com은 docker에서 관리하는 컨테이너 이미지 레지스트리이다. node, nginx 등 기본 이미지를 쉽게 받을 수 있는 저장소 개념이다.

리액트 앱을 위한 컨테이너를 만들기 때문에 nodeJS를 컨테이너의 기본 이미지로 한다.

  • node 이미지를 다운로드 받아 이미지를 커스터마이징 하는 것
  • 특정 개발 환경을 제공하는 이미지들을 pre-built container라고 한다.

  • 프로젝트 root 디렉토리에 Dockerfile을 만든다.
  • Dockerfile은 이미지를 빌드하기 위한 모든 단계를 레이어별로 명시한다.

Dockerfile

# 가져올 이미지를 정의
FROM node:14
# 경로 설정하기
WORKDIR /app
# package.json 워킹 디렉토리에 복사 (.은 설정한 워킹 디렉토리를 뜻함)
COPY package.json .
# 명령어 실행 (의존성 설치)
RUN npm install
# 현재 디렉토리의 모든 파일을 도커 컨테이너의 워킹 디렉토리에 복사한다.
COPY . .

# 각각의 명령어들은 한줄 한줄씩 캐싱되어 실행된다.
# package.json의 내용은 자주 바뀌진 않을 거지만
# 소스 코드는 자주 바뀌는데
# npm install과 COPY . . 를 동시에 수행하면
# 소스 코드가 조금 달라질때도 항상 npm install을 수행해서 리소스가 낭비된다.

# 3000번 포트 노출
EXPOSE 3000

# npm start 스크립트 실행
CMD ["npm", "start"]

# 그리고 Dockerfile로 docker 이미지를 빌드해야한다.
# $ docker build .

2) Build Docker Image

리액트 앱 root 디렉토리에서 IDE 터미널 커맨드라인에서 아래 명령어를 실행한다.

$ docker build .

build 뒤의 구두점은 현재 디렉토리에 이미지를 만들겠다는 이야기다.
각 단계들은 한 번 실행되면 캐싱되기 때문에 두번째 빌드부터는 더 빠르게 실행된다.

다음 명령어로 도커 이미지를 찾아보자.

$ docker image ls

이미지에 이름을 정해주지 않아 <none>으로 나온다. docker 이미지를 지우고 다시 만들어본다.

$ docker image rm <docker-image-id>

rm 다음 도커 이미지의 이름 또는 아이디를 입력하여 이미지를 삭제한다.

$ docker build -t react-image .

-t 옵션으로 이름(태그)를 만들어 다시 빌드한다.

결과가 캐싱되어 있어 훨씬 빠르게 이미지가 빌드되는 것을 확인할 수 있다.


3) Create Docker Container

만들어진 이미지로 도커 컨테이너를 띄우려면 아래의 명령어를 실행한다.

$ docker run -d --name <container-name> <image-name>

만들어질 컨테이너 이름은 react-app, 이미지 이름은 react-image이므로 아래와 같이 실행하게 된다.

$ docker run -d --name react-app react-image

3-1) -d 옵션 (detached)

d, --detach Run container in background and print container ID

  • 컨테이너를 백그라운드(detached 모드)에서 실행하고, 실행 결과로 컨테이너 ID를 출력하는 옵션
  • 이 옵션 없이 실행하면 터미널에 컨테이너 로그가 출력되고, 종료하기 위해 Ctrl + C를 입력해야 한다.

3-2) docker ps

  • docker 컨테이너의 리스트를 보여주는 명령어로 현재 가동중인 컨테이너만 보인다. 만약 모두 보고 싶다면 -a 옵션을 붙인다.

4) Run Container and Port Fowarding

$ docker run ~ 로 컨테이너를 실행했는데 리액트 로컬 서버인 3000번 포트에 접속이 되지 않는다 왜일까?

  • 컨테이너는 호스트 환경과 격리된 파일 시스템과 네트워크를 가지기 때문
  • 호스트에서 컨테이너로 접근 가능하도록 포트 포워딩을 시켜줘야 한다.

4-1) docker rm <container-name> -f

  • 위 명령어는 컨테이너를 제거할 것이다.
  • 보통 컨테이너를 삭제하기 전에 실행을 중단시킨 후 삭제한다.
  • 중단은 $ docker stop <container-name>을 사용한다.

4-2) 포트 포워딩

이미지로 컨테이너를 띄울때 커맨드라인에 -p hostPort:ContainerPort 옵션으로 포트 포워딩을 할 것을 명시한다.

$ docker run -d -p 3306:3000 --name <container-name> <image-name>

결과를 구분하기 위해 호스트 포트를 3306으로 지정했다.
위 명령에서 -p 의 콜론을 사이에 둔 숫자에 주목해야한다. -p 3306:3000의 의미는 로컬 머신(127.0.0.1)의 3000번 포트로 접근하는 모든 트래픽을 도커 컨테이너의 3000번 포트로 보낸다는 뜻이다.

브라우저에서 로컬호스트 3306 포트로 접속하면 컨테이너 환경에서 실행된 리액트 로컬 서버가 앱을 잘 서빙해주는 것을 확인할 수 있다.

4-3) docker exec

  • docker 컨테이너 환경에서 커맨드라인(bash) 띄우기
$ docker exec -it <container-name> bash

ls 명령어로 확인해보니 컨테이너의 리액트 앱 루트 폴더가 잘 확인된다.

  • 여기서 docker exec은 명령어를 실행하는 것
$ docker exec [option] <container-command>
  • exec-it 옵션을 쓰는 이유
    • -i: 표준 입출력 STDIN을 열겠다는 의미
    • -t: 가상 TTY(Pseudo TTY)를 통해 접속하겠다는 의미

참고) 표준 스트림, 표준 입출력에 대해 알아보자

4-4) docker ignore

도커 컨테이너에 포함시키지 않을 파일들을 명시한다. .gitignore와 개념이 같다.

# .dockerignore
node_modules
Dockerfile
.git
.gitignore
.dockerignore
.env

.dockerignore에 명시된 파일은 컨테이너에 포함되지 않은 것을 볼 수 있다.


5) Volume and Bind Mount

참고) Docker 컨테이너에 데이터 저장 (볼륨/바인드 마운트)

5-1) 코드 변경 후 어떻게 컨테이너의 프로젝트를 업데이트할까?

  • 컨테이너 종료 > 다시 이미지 빌드 > 컨테이너 다시 띄우기는 너무 귀찮다.
  • 코드 수정이 즉각적으로 반영되어야 개발이 편하다. Volume과 Bind Mount 개념을 알면 가능하다.

5-2) Volume / Bind Mount

Docker 컨테이너의 라이프 사이클과 상관 없이 도커 단에서 데이터를 저장하는 방법

  • Volume 일반적인 상황에서 권장
  • Bind Mount 로컬 개발 환경에서 권장

우리는 로컬 개발 환경에서 실시간으로 수정 사항이 화면에 반영이 되어야 하기 때문에 호스트 환경의 프로젝트 폴더를 바인드 마운트 방식으로 컨테이너에 연결시킬 것이다.

-v dirlocaldirectory:containerdirectory

pwd로 마운트시킬 호스트 파일 시스템의 프로젝트 경로를 바인딩한다.

$ docker run -v $(pwd)/src:/app/src -d -p 8080:3000 --name react-app react-image

위 명령을 풀어쓰면 아래와 같다.

  • docker run 도커 컨테이너를 띄울(실행) 것이다.
  • -v $(pwd)/src:/app/src $(현재 경로)/src 폴더가 컨테이너의 /app/src 경로로 동기화 되도록 바인드 마운트 할 것이다.
  • -d 컨테이너 프로세스를 백그라운드에서 실행시킬 것이다. (detached)
  • -p 8080:3000 호스트 환경에서 8080으로 접속되는 트래픽을 컨테이너의 3000번 포트로 포워딩 할 것이다.
  • —name react-app 컨테이너의 이름은 react-app으로 명한다.
  • react-image를 docker 이미지로 사용할 것이다.

5-3) Read-only Bind Mount

의도치 않게 컨테이너 환경에서 소스 코드를 수정할 수도 있다. 이 경우 도커 컨테이너에서 호스트를 수정하지 못하도록 읽기 전용 모드를 사용하면 양방향 Sync에서 호스트 ⇒ 컨테이너로 동기화된다.

바인드 마운트 명령어에 :ro (read-only)만 붙여주면 된다.

$ docker run -v $(pwd)/src:/app/src:ro -d -p 8080:3000 —name react-app react-image

위와 같이 컨테이너 환경 CLI로 파일 시스템 수정이 불가하다.


6) Environment Variables

도커 환경에서 환경 변수를 어떻게 설정할 수 있을까?

6-1) Dockerfile에 하드코딩

컨테이너 환경에서 리액트 서버를 띄우기 전 라인에서 직접 환경 변수를 선언할 수 있다.

6-2) 커맨드 라인에서 하드코딩

컨테이너를 띄우는 시점(run)에 커맨드 라인에서 환경 변수를 세팅할 수도 있다.

-e ENV_VARIABLE_NAME=value

커맨드 라인에서 설정된 환경 변수는 Dockerfile.env에 선언된 같은 이름의 환경 변수를 덮어쓰기 때문에 주의해야 한다.

6-3) .env 파일로 설정

REACT_APP_NAME=Wonkook Lee
REACT_APP_TITLE=Kiwi

환경 변수 파일을 .env 파일로 관리하고 CLI에서 환경 변수 파일을 지정해주면 사용 가능하다.

--env-file <ENV_FILE_DIRECTORY>
--env-file ./.env

6-4) 우선 적용 순위

CLI > .env > Dockerfile

순위가 높은 쪽이 순위가 낮은 쪽을 덮어 씌운다.


7) Docker Compose

Docker-compose란 여러 개의 컨테이너로부터 이루어진 서비스를 구축, 실행하는 순서를 자동으로 하여, 관리를 간단히 하는 기능이다.

여러개의 컨테이너를 관리하면서 수 많은 명령어를 하나 하나 실행시키는 것이 여간 귀찮은 일이 아니다.
루트 디렉토리에 docker-compose.yml 파일을 만들고 명령어를 정리한다.

# docker 컨테이너 버전을 명시
version: "3"

# services는 컨테이너
services:
	react-app:
		# -it 옵션을 위해 사용됨 (표준입출력)
		stdin_open: true
		tty: true
		# 현재 경로에 이미지 빌드
		build: .
		# 포트 포워딩
		ports:
			- "8080:3000"
		# 호스트 디렉토리에 바인드 마운트
		volumes:
			- ./src:/app/src:ro
		# 환경 변수 설정 - opt.1(하드코딩)
		environment:
			- REACT_APP_NAME=wonkook
			- REACT_APP_TITLE=kiwi
		# 환경 변수 설정 - opt.2(.env)
		env_file:
			- ./.env

docker-compose.yml을 작성하고 서비스 컨테이너를 생성하고 실행하기 위해 docker-compose up 명령어를 사용한다.

참조) Docker Compose 커맨드 사용법

7-1) docker-compose up -d

docker-compose.yaml에 명시된 모든 서비스 컨테이너를 생성하고 실행시켜주는 명령어

7-2) docker-compose down

모든 서비스 컨테이너를 한 번에 정지시키고 삭제한다.

7-3) docker-compose up —build -d

이미지를 다시 빌드해서 컨테이너를 띄워야 하는데 docker-compose는 멍청해서 이미지 이름만 같아도 새롭게 빌드하지 않는다. 기존 이미지와 컨테이너를 stale 시키고 다시 빌드하기 위해 —build 옵션을 사용한다.


8) Multi-Stage Build for Production with NGINX

8-1) Development Environment

개발 환경에서는 CRA가 리액트 Dev 서버를 로컬 머신 3000번 포트에 띄워 앱을 서빙한다.
하지만 리액트 개발 서버는 개발용이기 때문에 실제 웹서버로써 활용할 수 없다.

8-2) Production Environment

프로덕션 환경에서는 빌드 후 생성된 정적 리소스를 업로드하게 된다.

8-3) NGINX: Production Grade Web-Server

정적 파일을 서빙하기 위해 프로덕션용 웹서버를 NGINX(엔진엑스)로 띄운다. 반드시 NGINX여야만 하는 것은 아니며 필요에 따라 아파치도 사용할 수 있다. 뭐가 됐든 정적 파일을 요청했을때 서빙해줄 서버가 필요하다.

8-4) NGINX를 사용한 프로덕션용 다단계 빌드

8-5) Use an external image as a “stage”

  • 개발 환경을 위한 도커 이미지 빌드를 위해 Dockerfile.devDockerfile.prod를 분리한다.
  • 빌드 패턴을 사용하기 위해 builder와 실제 실행될 이미지 두개의 Dockerfile이 필요하다.

Dockerfile.dev

FROM node:14
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

Dockerfile.prod

FROM node:14 AS build
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build

FROM nginx
COPY --from=build /app/build /usr/share/nginx/html
  • 리액트 코드 Production 빌드
    • node:14을 베이스 이미지로 설치하고, 이 빌드 스테이지를 build로 명명한다.
    • 워킹 디렉토리 설정, package.json 복사 및 의존성 설치, 의존성 파일 컨테이너에 복사
    • 컨테이너 환경에서 npm run build로 프로젝트를 빌드한다.
    • 그러면 루트 패스에 build(또는 dist) 디렉토리가 생성되고 변환된 정적 파일들이 들어있다.
  • NGINX 웹서버로 리액트 앱 서빙하기(Hosting simple static content)
    • nginx를 베이스 이미지로 설치하는 프로덕션 빌드 스테이지를 만든다.
    • 이때 COPY —from=<stage-name> 명령을 사용해서 이전 스테이지에서 산출한 빌드된 정적 바이너리를 NGINX의 /usr/share/nginx/html 디렉토리로 복사한다.
    • 간단한 정적 컨텐츠는 아래와 같이 웹서버 컨테이너에 추가한다.
FROM nginx
COPY static-html-directory /usr/share/nginx/html

nginx - Official Image | Docker Hub

COPY —from의 역할

The COPY --from=0 line copies just the built artifact from the previous stage into this new stage.

  • COPY instruction에서 —from 옵션을 사용하면 이전 스테이지에서 Build를 통해 생성된 결과만을 복사할 수 있다.

AS <Alias-Name>의 역할

  • 특정 빌드 스테이지의 별칭(alias)을 정할 수 있다.
  • FROM instruction 레이어에서 사용 가능하고, COPY—from=<name> 옵션으로 참조한다.

Multi-stage builds

8-6) 멀티 스테이지 빌드의 장점

멀티 스테이지 빌드란 컨테이너 이미지를 만들면서 빌드 등에는 필요하지만, 최종 컨테이너 이미지에는 필요 없는 환경을 제거할 수 있도록 단계를 나누어 기반 이미지를 만드는 방법이다.

  • 빌드에 사용한 파일 및 디렉토리 등 의존 파일을 삭제하고 컨테이너를 실행할 수 있어서 훨씬 가볍다.
  • 앱을 실행하기 위해 최소한으로 필요한 실행 모듈만 배치하여 컴퓨팅 리소스 효율화 및 보안에 도움이 된다.

9) Development vs. Production Workflow

Dockerfile & docker-compose
Dockerfile은 사용자가 이미지를 어셈블하기 위해 호출할 수 있는 명령이 포함된 간단한 텍스트 파일인 반면 Docker Compose는 다중 컨테이너 Docker 앱을 정의하고 실행하기 위한 도구다.

docker-compose 파일을 dev와 prod 두 버전으로 나눈다.

9-1) 공통 부분

  • docker-compose.yml
    • 공통적인 configuration을 설정한다.
version: '3'
services:
	react-app:
		build:
			context: .

9-2) Development 환경

  • docker-compose-dev.yml
version: '3'
services:
	react-app:
		build:
			context: .
			dockerfile: Dockerfile.dev
		ports:
			- '4000:3000'
		volumes:
			- ./src:/app/src:ro
		env_file:
			- ./.env.local
  • Dockerfile.dev
FROM node:14
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
  • 이미지 빌드 및 컨테이너 마운트시 -f 옵션으로 configuration file을 지정하면 된다.
$ docker-compose -f docker-compose.yml -f docker-compose-dev.yml up -d --build

9-3) Production 환경

  • docker-compose-prod.yml
version: '3'
services:
	react-app:
		build:
			context: .
			dockerfile: Dockerfile.prod
			# npm run build시 환경 변수 참조가 안되어 args로 전달하고, Dockerfile에서 변수로 사용한다.
			args:
				- REACT_APP_NAME=Wonkook-prod
				- REACT_APP_TITLE=Kiwi-prod
			ports:
				- "8080:80"
				# 80: HTTP Port for NGINX Web Server
  • Dockerfile.prod
FROM node:14 AS build
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
# docker-compose-prod에 명시된 args(변수)를 이미지 빌드시 환경 변수로 사용한다.
ARG REACT_APP_NAME
ENV REACT_APP_NAME=${REACT_APP_NAME}
# 인수를 환경 변수에 할당하는 순서를 잘 지켜야 한다.
ARG REACT_APP_TITLE
ENV REACT_APP_TITLE=${REACT_APP_TITLE}
RUN npm run build

FROM nginx
COPY --from=build /app/build /usr/share/nginx/html
  • 이미지 빌드 및 컨테이너 마운트
$ docker-compose -f docker-compose.yml -f docker-compose-prod.yml up -d --build

9-4) 정리

  • 4000번 포트는 dev 환경의 리액트 앱이 실행되는 컨테이너 3000번 포트로 포워딩
  • 8080번 포트는 prod 환경의 nginx가 실행되고 있는 컨테이너 80번 포트로 포워딩

9-5) Build Target

특정 빌드 스테이지만 실행시킬 수 있다. 예를 들어 필요한 경우 아래 build라는 별칭을 가진 리액트 앱 빌드 프로세스만 이미지로 빌드할 수 있다.

FROM node:14 AS build
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
ARG REACT_APP_NAME
ENV REACT_APP_NAME=${REACT_APP_NAME}
ARG REACT_APP_TITLE
ENV REACT_APP_TITLE=${REACT_APP_TITLE}
RUN npm run build

# FROM nginx
# COPY --from=build /app/build /usr/share/nginx/html

build —target뒤에 타겟 이름을 붙이고, DockerfileDockerfile.prod로 지정한다.

$ docker build --target build -f Dockerfile.prod -t multi-stage-example .




🙏🏻

Wonkook Lee
Frontend Developer
LinkedIn

profile
© 가치 지향 프론트엔드 개발자

3개의 댓글

comment-user-thumbnail
2024년 2월 28일

감사합니다. 내용 잘 정리해주셔서 유익하게 읽었었습니다

답글 달기
comment-user-thumbnail
2024년 6월 13일

안녕하세요, 혹시 production 환경에서 volume을 별도로 설정 안한 이유가 혹시 있을까요?

답글 달기
comment-user-thumbnail
2024년 9월 9일

좋은 내용 감사합니다~!

답글 달기