본문 바로가기

Docker, CI

Docker - Node +Mysql + React - 개발및 Dockerfile 생성하기

반응형

github.com/loy124/docker-react-fullstack

 

loy124/docker-react-fullstack

Contribute to loy124/docker-react-fullstack development by creating an account on GitHub.

github.com

풀스택 어플리케이션

선행 조건 : node.js, mysql, react에 대해 조금은 알고 있어야 수월하게 진행이 가능합니다.

 

브라우저 -> nginx -> react

브라우저 -> nginx -> node -> mysql 의 형태의 다중 컨테이너의 프로그램을 제작및 배포하는 것이 목표이다. 

 

도커및 배포에 초점이 맞춰져 있습니다.

 

위의 방식은  크게 2가지 방식으로 설계가 가능하다고 한다.

 

1. nginx의 proxy를 활용한 설계

client -> nginx -> front(nginx) -> html css js 

client -> nginx -> server -> mysql

  • Request를 보낼 떄 URL 부분을 host이름이 바뀌어도 변경시켜주지 않아도 된다
  • 포트가 바뀌어도 변경을 안해줘도 된다
  • 설정 및 설계가 복잡하다.

https://medium.com/@deresegetachew/serving-react-spa-using-nginx-and-reverse-proxy-4e5485c814a0

리버스 프록시

클라이언트로부터 요청을 받아서 웹서버로 요청을 전송한다 -> 웹서버는 응답을 클라이언트가 아닌 Reverse Proxy로 반환 -> Reverse Proxy는 클라이언트로 응답 반환 

따라서 외부 클라이언트가 내부 서비스에 접근할때 해당 리버스 프록시를 거쳐간다고 생각하면 좋을듯 싶다.

내부 서버를 숨길 수 있어 보안상의 이점이 있으며 서버 부하에 따라 요청을 분배할 수 있다.(사용자 증가에 따른 Web Server나 WAS를 유연하게 늘릴 수 있다)

 

 

 

2. nginx는 정적 파일만 제공해주는 설계 

client -> nginx -> front(nginx) -> html css js

client -> nginx -> server -> mysql

  • 설계가 간단하며 구현이 쉽다
  • host name이나 포트 변경이 있을때 Request URL도 변경시켜 주어야 한다.

https://medium.com/bb-tutorials-and-thoughts/how-to-serve-react-application-with-nginx-and-docker-9c51ac2c50ba

 

 

여기서는 1번방식을 활용해서 진행한다.

front -> server -> mysql -> nginx 

 

 

순서는 전체 소스 코드 작성 -> Dockerfile 작성 -> Docker-compose 작성 -> github push -> trabis CI -> Docker hub -> AWS elasticBeanStalk 

 

소스코드 작성 - Node.js

 

폴더 구조 

 

backend 폴더에서 npm init을 통해 package.json을 만들어 준다. 

 

 

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"
  },
  "author": "",
  "license": "ISC"
}

 

 

server.js를 생성해준다.

const express = require("express");

const app = express();

app.use(express.json());

app.listen(5000, () => {
    console.log("this server listening on 5000");
});

 

server.js에서 사용하기 위한 db.js를 생성해 준다. 

const mysql = require("mysql");
const pool = mysql.createPool({
    connectionLimit: 10,
    host: 'mysql',
    user: 'root',
    password: 'root',
    database: 'myapp',
});

exports.pool = pool;

 

db.js를 활용해서 server.js에 내용을 추가해준다.

 

값들을 가져오는 get방식의 /api/values와 값을 입력하기위한 posts방식의 /api/value 를 정의하였다.

const express = require("express");
const db = require("./db");

const app = express();

app.use(express.json());

// 테이블 생성하기 예시 
// mysql은 mysql폴더에서 데이터베이스및 table을 생성해줄 예정이다
//db.pool.query(`CREATE TABLE lists (
//    id INTEGER AUTO_INCREMENT,
//    value TEXT,
//    PRIMARY KEY (id)
//)`, (err, results, fields) => {
//    console.log('results', results);
//})

app.get("/api/values", (req, res, next) => {
  db.pool.query("SELECT *FROM lists;", (err, results, fields) => {
    if (err) return res.status(500).send(err);
    else return res.json(results);
  });
});

app.post("/api/value", (req, res, next) => {
  db.pool.query(
    `INSERT INTO lists (value) VALUES("${req.body.value}");`,
    (err, results, fields) => {
      if (err) return res.status(500).send(err);
      else return res.json({ success: true, value: req.body.value });
    }
  );
});

app.listen(5000, () => {
  console.log("this server listening on 5000");
});

 

여기까지 작성하면 backend 파트가 1차적으로 마무리 된다.

 

소스코드 작성 - React.js

 

 

해당 frontend 폴더에서 프로젝트를 진행한다.

npx create-react-app .

react 패키지를 설치해준다. 

 

 

frontend -> src의 App.js를 수정해준다. 

 

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

function App() {
  
  // db로부터 화면에 보여주는 리스트 
  const [lists, setLists] = useState([]);
  // input 박스로 입력한 값 
  const [value, setValue] = useState("");

  useEffect(() => {
    // DB에 있는 값을 가져온다.
    axios.get(`/api/values`).then(response => {
      console.log('response', response.data);
      setLists(response.data);
    });
  },[]);
  const ChangeHandler = (e) => {
    setValue(e.currentTarget.value);
  }

  const submitHandler =(e) => {
    e.preventDefault();
    axios.post('/api/value', {value: value}).then(response => {
      if(response.data.success){
        console.log('response.data', response.data);
        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}
             />
            <button type="submit">확인</button>
          </form>
        </div>
      </header>
    </div>
  );
}

export default App;

 

App.test.js 또한 수정해준다.

 

주석 처리를 하는 이유는 만든 부분과 테스트 할 부분이 일치하지 않기 때문에 주석처리로 넘겨준다. 

import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  // render(<App />);
  // const linkElement = screen.getByText(/learn react/i);
  // expect(linkElement).toBeInTheDocument();
});

 

 

app.css또한 수정해준다.

.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}
form.example{
  display: flex;
}

form.example input{
  padding: 10px;
  font-size: 17px;
  border: 1px solid grey;
  width: 74%;
  background: #f1f1f1;
}

form.example button{
  
  width: 20%;
  padding: 10px;
  background: #2196F3;
  color:white;
  font-size: 17px;
  border:1px solid grey;
  
}
form.example button:hover{
  background: #0b7dda;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

 

여기까지 진행하면 일단 frontend 파트는 마무리 된다. 

 

 

Dockerfile 생성하기 - Node

먼저 Dockerfile.dev를 만들어 준다

 

FROM node:alpine

COPY ./package.json ./

RUN npm install

COPY ./ ./

#nodemon 사용 
CMD ["npm", "run", "dev"]

 

 

그후 

dockerfile을 생성해준다. 

 

FROM node:alpine

COPY ./package.json ./

RUN npm install

COPY ./ ./

#nodemon 사용 
CMD ["npm", "run", "start"]

여기까지 설정하면 node에 대한 설정은 간단하게 마무리된다.

 

Dockerfile 생성하기 - MySQL

DB구성은 또한 개발 환경과 운영환경을 사용할 것이다

 

개발환경 - Docker 환경

운영환경 - AWS RDS

 

먼저 개발 환경의 mysql 폴더를 생성하고 sqls 폴더를 생성한다

 

sqls 폴더안에는 Mysql을 실행 할 때 Database와 Table이 필요한데 그것들을 만들 장소를 만들어준다

 

initialize.sql

DROP DATABASE IF EXISTS myapp;

CREATE DATABASE myapp;
USE myapp;

CREATE TABLE lists(
    id INTEGER AUTO_INCREMENT,
    value TEXT,
    PRIMARY KEY (id)
)

 

또한 인코딩문제(한글깨짐)을 해결하기 위한 설정인 my.cnf를 설정해 준다.(Docker에서 덮어 씌울 예정) 

 

 

my.cnf

[mysqld]
character-set-server=utf8

[mysql]
default-character-set=utf8

[client]
default-character-set=utf8

 

이제 Dockerfile을 생성해준다 .

 

FROM mysql:5.7

#mysql내 my.cnf 설정을 my.cnf 파일로 덧씌운다. 
ADD .my.cnf /etc/mysql/conf.d/my.cnf

 

Dockerfile은 개발환경에서만 사용해주기 때문에 이번에는 Dockerfile.dev만 생성해주도록 하였다 

 

 

 

 

 

 

Dockerfile 생성하기 - React

 

먼저 Dockerfile.dev를 생성해준다.

 

Dockerfile.dev

FROM node:alpine

WORKDIR /app

COPY package.json ./

RUN npm install

COPY ./ ./

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

 

Dockerfile.dev를 통해 정상적으로 실행되면 

 

Dockerfile을 build하는 방식이다 

 

react로 build된 app은 nginx내에서 돌아가게 되는데 이에 따른 react 설정을 

nginx에서 지정해줘야한다. 

 

nginx폴더와 default.conf파일을 생성한다.

 

 

default.conf

 

server{
    listen 3000;

    location / {
        #HTML 파일이 위치할 루트 설정 
        root /usr/share/nginx/html;
        #사이트의 index페이지로 할 파일명 설정 
        index index.html index.htm 
        #react router을 사용해서 페이지간 이동을 할 때 필요한 부분
        #react는 SPA기 때문에 하느이 index.html만 가지고 있기 때문에 
        #nginx에서는 자동으로 라우팅을 할 수 없기 때문에 
        #/home 등 특정 라우터에 접속 하려고 할때 매칭이 되는것이 없을 경우 
        #index.html을 제공해서 /home으로 라우팅을 시킬 수 있게 임의로 설정해주는것 
        #1.$uri가 정확하게 일치하는 것이 있는지 파악한다.
        #2.그다음 $uri/가 정확히 일치하는지 파악한다.
        #3.없으면 root/index.html을 실행. 
        try_files $uri $uri/ /index.html;
    }
}

 

default.conf를 다 만들었다면 이제 Dockerfile을 생성해준다. 

 

Dockerfile

FROM node:alpine as builder

WORKDIR /app

COPY package.json ./

RUN npm install

COPY ./ ./

RUN npm run build

FROM nginx
EXPOSE 3000
#default.conf에서 해준 설정을 nginx컨테이너 안에 있는 설정이 되게 복사를해준다.
#현재 frontend/nginx에 있는 default.conf를 복사해준다.
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
#builder로 부터 /app/에 build파일이 생기면 /usr/share/nginx/html 파일에 복사하는 설정 
COPY --from=builder /app/build /usr/share/nginx/html

 

 

Dockerfile 생성하기 - Nginx

react내부 build파일을 실행하는 nginx가 아닌 reverse proxy방식의 nginx를 위한 방식이다 

 

https://medium.com/@deresegetachew/serving-react-spa-using-nginx-and-reverse-proxy-4e5485c814a0

 

/api로 요청이 오게 되면 express 단으로 보내주고 

그외 / 로 오게되면 react단으로 나누어 준다. 

이러한 기능을 위한 nginx설정을 해준다.

nginx 폴더를 만든 후 default.conf 파일을 만들어 준다. 

 

 

default.conf

 

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

server {
    #nginx 포트를 80번으로 열어준다. 
    listen 80;
    
    # /로 시작하는 부분이 우선순위가 가장 낮다 
    # /로 들어오는 요청은 http://frontend로 보내준다
    # frontend라는 이름은 docker-compose에서 정의해줄 예정 
    location / {
        proxy_pass http://frontend
    }

    # /api로 들어오는 요청은 http://backend로 보내준다
    # backend라는 이름은 docker-compose에서 정의해줄 예정 
    location /api {
        proxy_pass http://backend;
    }
    
    #아래 부분을 만들어 줘야 개발환경 내에서 에러가 발생하지 않는다. 
    location /sockjs-node {
        proxy_pass http://frontend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
    }
}

 

이제 Dockerfile을 만들어 준다.

개발환경과 운영환경이 같으므로 Dockerfile 하나만 만들어준다. 

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

 

여기까지 진행하면 전반적인 docker 설정이 마무리된다

docker-compose 파일 만들기

docker file들을 생성했으니 이제 docker-compose 파일을 생성해서 하나로 묶어준다.

 

기본 틀 

docker-compose.yml

 

version: "3"
services: 
  frontend:
    build:
      dockerfile: Dockerfile.dev
      context: ./frontend
    #코드 수정후 다시이미지 build없이 코드가 반영 될 수 있게 volume을 사용한다.
    volumes:
      - /app/node_modules
      - ./frontend:/app
    stdin_open: true

  nginx:
    #재시작 정책
    #no: 어떠한 상황에서도 재시작을 하지 않습니다
    #always: 항상 재시작
    #on-failure: 에러코드와 함께 컨테이너가 멈추었을 때만 재시작 
    #unless-stopped 개발자가 임의로 멈추려고 할때 빼고는 항상 재시작
    restart: always
    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:
      - "3307:3307"
    volumes:
      - ./mysql/mysql_data:/var/lib/mysql
      - ./mysql/sqls/:/docker-entrypoint-initdb.d/
    environment:
      MYSQL_USER: root
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: myapp
      MYSQL_TCP_PORT: 3307
    #mysql의 root 비밀번호와 db를 설정해준다.
    

      

 

msyql 파트는 볼륨을 활용해서 진행해 주었다

컨테이너를 삭제 할 떄 컨테이너 안의 db 값까지 지워지게 된다

이에 따라 volume을 활용해준다 

 

 

여기 까지 완료 하였다면

 

정상적으로 실행이 되는가 테스트를 진행한다

 

 

 

 

 

 

만약 정상적으로 실행이 되지 않는다면 docker ps -a 를 통해 컨테이너들을 지워주고

docker images에 있는 해당 프로젝트와 관련해서 만들어진 images를 모두 지워주고 다시 실행한다. 

(에러 해결하는데 꽤 시간을 소요 한 것 같다)

 

여기까지 docker파일 및 docker-compose를 활용해서 1차적으로 마무리하였고 이제 배포 부분이 남았다.

 

 

 

 

 

 

배포는 해당 파트에서 이어진다. 

 

loy124.tistory.com/361

 

Docker - Node +Mysql + React - AWS 배포하기(Travis CI)

loy124.tistory.com/360 Docker - Node +Mysql + React - 개발및 Dockerfile 생성하기 github.com/loy124/docker-react-fullstack loy124/docker-react-fullstack Contribute to loy124/docker-react-fullstack de..

loy124.tistory.com

 

 

 

 

위 글은 

www.inflearn.com/course/%EB%94%B0%EB%9D%BC%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EB%8F%84%EC%BB%A4-ci/dashboard

해당 강의를 듣고 정리하는 내용입니다.

 

 

반응형