9 분 소요

📌 Topic

  • 섹션 설명
  • Node JS 구성하기
  • React JS 구성하기
  • 리액트 앱을 위한 도커 파일 만들기
  • 노드 앱을 위한 도커 파일 만들기
  • DB에 관해서
  • MySQL을 위한 도커 파일 만들기
  • Nginx를 위한 도커 파일 만들기
  • Docker Compose 작성하기
  • Docker Volume을 이용한 데이터베이스 데이터 유지하기

01. 섹션 설명

실제로 애플리케이션을 구성할때는 프론트 부분만을 이용하는 것이 아닌 백엔드 서버도 필요하고 DB 등등 많은 것이 필요하다. 이번에는 이전보다는 더욱 더 복잡한 도커 환경을 구성하는 시간을 가져보자.

01-1. 풀스택 어플리케이션 (multi-container application)

full_stack.PNG

이번에는 요청(Request)은 Nginx, 프론트는 react, 백엔드는 node.js로 구성된 멀티 컨테이너 어플리케이션
환경을 구성 해보자. 실제로 어플리케이션이 모두 구현된 화면은 다음과 같다.

react_app.PNG

입력창에 데이터를 입력하면 TODO 리스트처럼 입력이 되는 간단한 어플리케이션. 해당 데이터는 MySQL에 저장 후 바로 보여주는 방식으로 진행이 된다. 또한 컨테이너를 재 시작하여도 DB에 남아있는 데이터는 그대로 유지되도록 만들 것이다.

이제 위와 같은 애플리케이션을 만들기 위한 2가지 설계 방식에 대해 알아보자. 이전과는 다르게 Nginx를 사용한 2가지 방식이 존재하는데 다음 내용을 살펴보자.

01-2. Nginx를 통한 2가지의 설계 방법

이번에는 Nginx를 통한 2가지 설계 방식에 대해 간략히 알아보자.

nginx_proxy.PNG

  • 라우팅 서버 역할 수행
  • 정적 파일을 반환하는 역할 수행
  • 클라이언트의 요청 URL에 따라 정적 파일, 백엔드로 구분한다
  • 호스트명, 포트를 변경하지 않아도 된다는 장점이 있다
  • 설계는 아래에 비해 조금 복잡함

nginx_static.PNG

  • 정적 파일을 반환하는 역할 수행
  • Port를 기반으로 구분한다
  • 호스트명, 포트를 상황에 따라 변경해야 한다는 단점이 있다
  • 설계는 조금 더 간단함

01-3. 풀스택 어플리케이션 구현 흐름

React, Node.js 기반 어플리케이션을 구현하기 위한 간단한 흐름은 다음과 같다

  1. 전체 소스 코드 작성 (Node.js, React)
  2. 개발 환경에 맞는 Dockerfile 작성 (dev, prod)
  3. Docker Compose 작성
  4. 깃헙 업로드
  5. Travis CI 진행
  6. Docker hub에 이미지 업로드
  7. AWS ElasticBeanStalk를 통해 이미지 배포 진행

이제 흐름을 정리 했으니 Node.js 백엔드 서버와 react 프론트 소스 작성을 이어나가보자.

02. Node JS 구성하기

이번 시간에는 Node.js 기반 백엔드(Back-end) 서버를 구성 해보자.

02-1. 프로젝트 생성

  • 프로젝트명 : docker-full-stack-app
  • 프로젝트 생성 후 아래 명령줄을 입력한다
# root 경로에 backend 디렉토리를 생성
mkdir backend && cd backend
# package.json 생성
npm init -y
// 생성  package.json
{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js", // 시작  사용할 스크립트 추가
    "dev": "nodemon server.js" // 재기동 관련 스크립트 추가
  },
  "dependencies": {
    "express": "4.16.3",
    "mysql": "2.16.0",
    "nodemon": "1.18.3",
    "body-parser": "1.19.0"
  },
  "author": "",
  "license": "ISC"
}
  • start, dev 스크립트를 추가 해준다
  • 라이브러리 목록은 다음과 같이 추가 해준다
    • express
    • mysql
    • nodemon
    • body-parser

02-2. server.js 파일 생성

// server.js
const express = require("express");
const bodyParser = require("body-parser");

// Express
const app = express();

app.use(bodyParser.json());
app.listen(5000, () => {
  console.log(`애플리케이션이 5000번 포트에서 시작되었습니다.`);
});

기본적으로 express js와 bodyParser를 사용하여 간단한 백엔드 서버를 구성 하였다.
현재 백엔드 앱은 5000 포트(port)로 Listen을 할 것이다. 다음에는 이러한 node.js 백엔드에서
사용할 DB 관련 파일을 작성 해보자.

02-3. MySQL 관련 파일 생성

// db.js
const mysql = require("mysql");
const pool = mysql.createPool({
  connectionLimit: 10,
  host: "mysql",
  user: "root",
  password: "1234!",
  database: "myapp",
});
exports.pool = pool;

MySQL 관련 DB 설정을 Javascript를 통해 작성한다.

02-4. server.js CRUD 추가

// CREATE: 테이블 생성
db.pool.query(
    `CREATE TABLE lists(
    id INTEGER AUTO_INCREMENT,
    value TEXT,
    PRIMARY KEY (id)
)`,
    (err, result, fileds) => {
        console.log(`result =>`, result);
    },
);

---

// SELECT : DB에서 모든 정보 가져오기
app.get("/api/values", function (req, res) {
    db.pool.query(`SELECT * FROM lists;`, (err, result, fileds) => {
        if (err) {
            return res.status(500).send(err);
        } else {
            return rest.json(result);
        }
    });
});

---

// INSERT: 입력 값 DB에 등록
app.post("/api/value", function (req, res, next) {
    db.pool.query(
        `INSERT INTO lists (value) VALUES("${req.body.value}")`,
        (err, result, fileds) => {
            if (err) {
                return res.status(500).send(err);
            } else {
                return res.json({ success: true, value: req.body.value });
            }
        },
    );
});

---
  • 기본적인 CRUD 관련 소스를 작성 해준다.

02-5. server.js 완성된 소스 확인

// Get required modules
const express = require("express");
const bodyParser = require("body-parser");
const db = require("./db");

// Express
const app = express();
app.use(bodyParser.json());

// CREATE: 테이블 생성
db.pool.query(
  `CREATE TABLE lists(
    id INTEGER AUTO_INCREMENT,
    value TEXT,
    PRIMARY KEY (id)
)`,
  (err, result, fileds) => {
    console.log(`result =>`, result);
  }
);

// SELECT : DB에서 모든 정보 가져오기
app.get("/api/values", function (req, res) {
  db.pool.query(`SELECT * FROM lists;`, (err, result, fileds) => {
    if (err) {
      return res.status(500).send(err);
    } else {
      return rest.json(result);
    }
  });
});

// INSERT: 입력 값 DB에 등록
app.post("/api/value", function (req, res, next) {
  db.pool.query(
    `INSERT INTO lists (value) VALUES("${req.body.value}")`,
    (err, result, fileds) => {
      if (err) {
        return res.status(500).send(err);
      } else {
        return res.json({ success: true, value: req.body.value });
      }
    }
  );
});

app.listen(5000, () => {
  console.log(`애플리케이션이 5000번 포트에서 시작되었습니다.`);
});

Node.js와 MySQL을 이용한 기본적인 소스 작성을 완료가 되었다.
이제 백엔드가 완료 되었으니, 프론트(react) 소스 작성을 이어 나가보자.

03. React JS 구성하기

이전에 02장에서 Node.js를 기반으로 하여 간단한 백엔드 서버를 만들어 보았다.
이번에는 이러한 백엔드와 통신을 하기 위한 프론트 애플리케이션을 react를 기반으로 만들어보자.

03-1. frontend react app 생성

npx create-react-app frontend

npx_react_created.PNG

위 명령어를 통해 기본적인 react 애플리케이션을 다운로드 받는다.
또한 frontend 키워드를 추가하면 디렉토리 역시 생성이 된다.

03-2. react 앱 구동 해보기

cd frontend
npm run start

---

Compiled successfully!

You can now view frontend in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://172.22.160.1:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

webpack compiled successfully

react_start.PNG

만약 위와 같은 성공 메시지가 출력이 되고 다음과 같은 화면이 나오게 되면
정상적으로 react 애플리케이션이 실행된 것이다.

우리는 해당 화면에 입력을 하고 저장을 할 수 있는 input box와 버튼을
추가적으로 넣어주기 위해 소스 수정을 진행 해보자.

03-3. App.js

import React, { useState, useEffect } from "react";
import logo from "./logo.svg";
import "./App.css";
import axios from "axios";

function App() {
  useEffect(() => {
    // DB 값을 가져온다
    axios.get("/api/values").then((response) => {
      console.log(`response => ${response}`);
      setLists(response.data);
    });
  }, []);

  const [lists, setLists] = useState([]);
  const [value, setValue] = useState("");

  const changeHandler = (event) => {
    setValue(event.currentTarget.value);
  };

  const submitHandler = (event) => {
    event.preventDefault(); // 원래 버튼 눌렀을때 이벤트 차단
    axios.post("/api/value", { value: value }).then((response) => {
      if (response.data.success) {
        console.log(`response =>`, response);
        setLists([...lists, response.data]); // 기존 데이터 다음에 삽입
        setValue("");
      } else {
        alert("값을 DB에 넣는데 실패했습니다.");
      }
    });
  };

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <div className="container">
          {lists &&
            lists.map((list, index) => <li key={index}>{list.value}</li>)}

          <form className="example" onSubmit={submitHandler}>
            <input
              type="text"
              placeholder="입력해주세요.."
              onChange={changeHandler}
              value={value}
            ></input>
            <button type="submit">확인</button>
          </form>
        </div>
      </header>
    </div>
  );
}

export default App;

현재 Docker를 위한 내용이기 때문에 react 부분은 설명을 생략하고 넘어감.

04. 리액트 앱을 위한 도커 만들기

현재 Node.js(백엔드), react(프론트)를 위한 소스 코드 작성을 완료 하였다.
이제 react app을 위한 Dockerfile을 작성 해보자.

04-1. Dockerfile 작성(Dev 환경)

frontend 폴더 안에 Dockerfile.dev, Dockerfile을 작성한다.
우선 개발 환경을 위한 Dockerfile을 먼저 작성 해보자.

Dockerfile.dev

# 베이스 이미지 지정(기준 혹은 근본이 되는 이미지라 생각하자)
FROM node:alpine

# 컨테이너의 Work Dir 경로 지정
WORKDIR /app

# 로컬 경로(frontend 폴더)에 존재하는 package.json 파일 -> 컨테이너에 복사
COPY package.json ./

# npm 종속성 파일 다운로드 -> 이미지 생성 전에 실행 됨
RUN npm install

# 로컬 경로(frontend 폴더)에 존재하는 내용 src, public.. 복사
COPY ./ ./

# react app 구동 커멘드 실행
CMD ["npm", "run", "start"]

일단 개발 환경을 위한 Dockerfile은 위와 같다.
하지만 위 파일만으로는 이해가 어려울 수 있으니 현재 아키텍처를
다음 사진을 통해 참고해보자.

04-2. react app 개발 및 상용 환경 아키텍처

develop_env_react.PNG

개발 환경에서의 react의 경우 다른점은 빌드(Build)가 되지않은 파일들로 실행이 되며
개발 서버가 따로 존재한다는 차이점이 존재한다.

prod_env_react.PNG

운영 환경에서의 react는 빌드(Build) 파일을 생성한 후에 해당 빌드 파일을 이용하고
개발 서버는 Nginx로 대체가 된다는 차이가 존재한다.

04-3. Nginx 설정

nginx_proxy.PNG

현재 우리가 다루고자 하는 Nginx는 Proxy를 담당하는 Nginx가 아닌
Front 영역에서 정적 파일을 제공해주는 Nginx에 대한 설정을 진행하는 중이다.

이러한 Nginx는 설정을 따로 잡아주어야 한다.
아래와 같이 frontend 폴더 밑에 nginx 폴더를 생성한다.

nginx_dir.PNG

server {
  listen 3000;

  location / {
    # HTML 파일이 위치할 루트 설정
    root /usr/share/nginx/html;

    # 사이트의 index 페이지로  파일명 설정
    index index.html index.htm;

    # React Router를 사용해서 페이지간 이동을   아래 부분이 필요
    try_files $uri $uri/ /index.html
  }
}
  • Nginx 설정 파일은 위와 같다. 이제 상용 Dockerfile을 작성 해보자

04-4. Dockerfile 작성(Prod 환경)

FROM node:alpine as builder
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build

# nginx 베이스 이미지
FROM nginx
EXPOSE 3000

# 컨테이너 안에 있는 Nginx의 설정 파일
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf

# /app/build 파일의 내용을 /usr/share/nginx/html 경로에 복사
COPY --from=builder /app/build /usr/share/nginx/html
  • Prod 전용 Dockerfile 작성
  • 다음에는 Node.js를 위한 Dockerfile 작성을 이어나가보자

05. 노드 앱을 위한 도커 파일 만들기

이번 시간에는 노드 앱을 위한 도커 파일을 작성 해보자.

05-1. Dockerfile 작성(Dev 환경)

FROM node:alpine

WORKDIR /app

COPY ./package.json ./

RUN npm install

COPY . .

CMD ["npm", "run", "dev"]

05-2. Dockerfile 작성(Prod 환경)

FROM node:alpine

WORKDIR /app

COPY ./package.json ./

RUN npm install

COPY . .

CMD ["npm", "run", "start"]

지금까지 백엔드를 구성하기 위한 Node.js, 프론트앤드를 구성하기 위한 react app 소스를 작성 후
개발환경에 따라 Dockerfile 역시 생성을 해주었다. 다음으로는 DB에 관하여 간략하게 정리한 후에
MySQL, Nginx를 위한 도커 파일을 만들어보자.

06. DB에 관해서

이번에는 애플리케이션을 위한 DB 환경을 구성 해보자

06-1. DB 환경 분리

  1. 개발 환경 : 도커 한경 이용
  2. 운영 환경 : AWS RDS 이용

DB 작업은 중요한 데이터들을 보관하고 이용하는 부분이기에 조금의 실수로도 안 좋은 결과를 얻을 수 있다. 그렇기에 상용의 경우 도커 환경의 DB보다는 RDS를 사용하여 데이터를 관리하자.

07. MySQL을 위한 도커 파일 만들기

이번에는 MySQL을 위한 도커 파일을 작성 해보자.
DB는 도커 환경에서의 DB, AWS RDS의 환경 2가지로 구분하여 작성한다.

07-1. MySQL Dockerfile 작성

mkdir mysql
# Dockerfile
FROM mysql:5.7

# encoding type 설정
ADD ./my.cnf /etc/mysql/conf.d/my.cnf

07-2. MySQL 데이터베이스 생성

DROP DATABASE IF EXISTS myapp;

CREATE DATABASE myapp;
USE myapp;

CREATE TABLE lists (
    id INTEGER AUTO_INCREMENT,
    value TEXT,
    PRIMARY KEY (id)
)
  • 데이터베이스 생성 후 테이블 생성

07-3. 인코딩 설정

[mysql]
character-set-server=utf8

[mysql]
default-character-set=utf8

[client]
default-character-set=utf8

08. Nginx를 위한 도커 파일 만들기

nginx_proxy.PNG

현재 만들고자 하는 Nginx는 Client의 요청을 받는 Nginx다.
지금부터 Proxy 역할을 하는 Nginx 도커 파일을 생성 해보자.

08-1. Nginx 설정 파일 작성

nginx_dir.PNG

우선 위와 같이 프로젝트 루트(root) 경로에 nginx 디렉토리를 생성 한다.
후에 디렉토리 안에 이전과 같이 default.conf 파일과 Dockerfile을 작성 해보자.

08-2. Nginx 설정 파일인 default.conf 작성

# 3000번 포트에서 frontend가 돌고 있는 것을 명시
upstream frontend {
  server frontend:3000;
}

# 5000번 포트에서 backend가 돌고 있는 것을 명시
upstream backend {
  server backend:5000;
}

server {
  # Nginx의 서버 포트 80번으로 open
  listen 80;

  # location에는 우선 순위가 존재, '/' 루트의 경우 우선 순위가 낮음
  # 즉, /api를 먼저 찾고 해당 경로가 없을 경우 '/' 경로를 찾는다
  # Docker 환경이 아닌 경우 실제 IP를 적어주어야 한다
  location / {
    proxy_pass http://frontend;
  }

  # /api로 들어오는 요청을 http://backend로 보낸다
  # 실제로는 docker-compose에 등록된 이름 'backend'를 통해 맵핑 시키는 것
  location /api {
    proxy_pass http://backend;
  }

  # 아래 부분은 개발 환경에서의 react를 사용하는 경우 필요한 부분
  # 해당 내용이 없으면 에러가 발생함
  location /sockjs-node {
    proxy_pass http://frontend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_update;
    proxy_set_header Connection "Upgrade";
  }
}

08-3. Nginx Dockerfile 작성

FROM nginx
COPY ./default.conf /etc/nginx/conf.d/default.conf

이로써 Proxy 서버 역할을 하는 Nginx의 기본적인 설정과
Dockerfile 작성이 완료 되었다. 이제 docker-compose 작성을 이어나가자.

09. Docker Compose 작성하기

09-1. docker-compose.yml 파일 작성

version: "3"
  services:
    frontend:
      build: # 개발 환경을 위해 실제 Dockerfile의 위치를 기재 한다
        dockerfile: Dockerfile.dev # dev 환경의 Dockerfile
        context: ./frontend # Dockerfile 현재 경로
      volumes: # 코드 수정 후 다시 이미지 build 없이 수정된 코드 반영
        - /app/node_modules # 참고하지 않도록 지정
        - ./frontend:/app
      stdin_open: true # react app 종료 시 나오는 버그를 잡는다

    nginx:
      restart: always # 재시작 정책 지정 (no, always, on-failure, unless-stopped)
      build:
        dockerfile: Dockerfile
        context: ./nginx
      ports:
        - "3000:80"

    backend:
      build:
        dockerfile: Dockerfile.dev
        context: ./backend
      container_name: app_backend
      volumes:
        - /app/node_modules
        - ./backend:/app

    mysql:
      build: ./mysql
      restart: unless-stopped
      container_name: app_mysql
      ports:
        - "3306:3306"
      volumes:
        - ./mysql/mysql_data:/var/lib/mysql
        - ./mysql/sqls/:/docker-entrypoint-initdb.d/
      environment:
        MYSQL_ROOT_PASSWORD: 1234!
        MYSQL_DATABASE: myapp

09-2. docker-compose

docker-compose up
  • 모든 이미지를 빌드하여 컨테이너 구동

10. Docker Volume을 이용한 DB 데이터 유지하기

참고 자료

댓글남기기