이번에는 React, Node, DB까지 도커를 이용해 어플리케이션을 만들어볼 예정이다.
설계하는 여러 방법이 있는데 무엇을 선택할 것인가?🤔
nginx의 기능을 크게 보면 두 가지가 있다. 정적파일 제공, 라우팅시켜주는 역할이 있는데 첫번째 설계에서는 두 가지 모두 이용한다.
만일 /api
로 시작하는 엔드포인트를 보낸다면 이건 다 백엔드 서버로 가는 요청들인 것이다.
ngix 설정, 전체 설계가 복잡하다.
클라이언트가 포트를 보고 요청을 보내는 형식이다.
설계가 다소 간단해 구현하는게 쉽다
host name이나 포트 변경 있을 때 Request URL도 변경 필요하다
여기에서는 1️⃣번을 선택해서 진행해보겠다.
Dockerfile.dev
Dockerfile
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"express": "4.18.2",
"mysql": "2.18.1",
"nodemon": "2.0.20"
},
"author": "",
"license": "ISC",
"type": "module"
}
import mysql from "mysql";
const pool = mysql.createPool({
connectionLimit: 10,
host: "mysql", #docker-compose.yml 서비스 이름인 mysql
user: "root",
password: "admin",
database: "myapp",
});
export default pool;
import express from "express";
import db from "./db.js";
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
db.query(
`CREATE table lists (id INTEGER AUTO_INCREMENT, value TEXT, PRIMARY_KEY (id))`,
(err, results, fileds) => {
console.log(`results: ${results}`);
}
);
app.get("/api/todos", (req, res, next) => {
db.query("SELECT * FROM lists", (err, results, fileds) => {
if (err) return res.status(500).send(err);
return res.json(results);
});
});
app.post("/api/todos", (req, res, next) => {
db.query(
`INSERT INTO lists (value) VALUES ${req.body.value}`,
(err, results, fileds) => {
if (err) return res.status(500).send(err);
else return res.json({ success: true, value: req.body.value });
}
);
});
app.listen(5000, () => console.log(`Server is running`));
npx create-react-app frontend
import React, { useState, useEffect } from "react";
import logo from "./logo.svg";
import "./App.css";
import axios from "axios";
function App() {
const [lists, setLists] = useState([]);
const [value, setValue] = useState("");
useEffect(() => {
axios.get("/api/todos").then((response) => {
console.log(response);
setLists(response.data);
});
}, []);
const changeHandler = (event) => {
setValue(event.currentTarget.value);
};
const submitHandler = (event) => {
event.preventDefault();
axios.post("/api/todos", { value: value }).then((response) => {
if (response.data.success) {
console.log(response);
setLists([...lists, response.data]);
setValue("");
} else {
alert("값을 넣는데 실패했습니다.");
}
});
};
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;
Dockerfile은 개발환경과 운영환경에서 다르게 구성하는게 좋기 때문에 개발환경을 위한 Dockerfile을 만들고 그 후 운영환경을 위한 Dockerfile을 만들 예정이다.
이 부분은 이전에 했던 것(Single Container를 배포해보자)과 거의 유사하다!!
하지만 복습 차원으로 여기서도 설명을 끄적거리겠다.(사실 까먹음)
frontend > Dockerfile.dev
FROM node:alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY ./ ./
CMD ["npm", "run", "start"]
FROM
: 베이스 이미지를 도커 허브에서 가져옴WORKDIR
: 해당 어플의 소스 코드들이 들어감COPY
: 소스 코드가 바뀔 때마다 종속성까지 다시 복사를 해주는 수고를 하지 않기 위해 먼저 종속성 목록을 담고 있는 package.json을 복사한다RUN
: package.json에 명시된 종속성을 다운받음COPY
: 모든 소스 코드들을 WORKDIR로 복사해줌CMD
: 이 컨테이너가 실행될 때 같이 실행할 명령어 지정frontend > Dockerfile
FROM node:apline as builder
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY ./ ./
RUN npm run build
FROM nginx
EXPOSE 3000
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/build /usr/share/nginx/html
첫 번째 부분은 Nginx가 제공을 해줄 Build 파일을 생성하는 단계(Builder Stage)이다.
두 번째 부분은 nginx를 가동하고, 첫 번째에서 생성된 Build 파일들을 제공해준다. 그리고 default.conf에서 해준 설정을 nginx 컨테이너 안에 있는 설정이 되게 복사 해준다.
(/etc/nginx/conf.d/default.conf
가 컨테이너 안에 있는 nginx 설정 경로)
정적파일을 제공해주기 위한 nginx를 위해서 frontend 안에 nginx 폴더를 생성하고, default.conf 파일을 생성한다.
server {
listen 3000;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
listen
: nginx가 listen하고 있는 포트root
: HTML 파일이 위치할 루트 설정(해당 부분에 build 폴더 들어가면 됨)index index.html index.htm
: 사이트의 index 페이지로 할 파일명 설정try_files
: React Router를 사용해서 페이지간 이동을 할 때 이 부분 필요React는 Single Page Application으로 index.html 하나의 정적 파일만 가지고 있다. 만약 {URL}/home 이렇게 접속을 하려고 할 때도 index.html 파일에 접근해 라우팅을 시켜야하는데 nginx에서는 자동으로 이걸 알 수가 없다. 그래서 /home으로 접속하려고 할 때 /home에 매칭되는 것이 없을 때 대안책으로 index.html을 제공해 /home으로 라우팅을 시킬 수 있게 임의로 설정한다.
FROM node:alpine
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY ./ ./
CMD ["npm", "run", "dev"]
FROM node:alpine
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY ./ ./
CMD ["npm", "run", "start"]
개발 환경에서는 도커 이미지를 이용해 설치를 해준다.
FROM mysql:5.7
Mysql을 시작할 때 Database와 Table이 필요한데, 그것들을 만들 장소를 만들어준다.
DROP DATABASE IF EXISTS myapp;
CREATE DATABASE myapp;
USE myapp;
CREATE table lists (
id INTEGER AUTO_INCREMENT,
value TEXT,
PRIMARY_KEY (id)
);
현재 상태에서 어떠한 글을 데이터베이스에 넣어줄 때 한글이 깨지면서 저장할 때 오류가 발생한다. 따라서 한글도 저장할 수 있도록 설정한다.
(여기서는 이모티콘까지 작성할 수 있도록 utf8mb4로 변경한다.)
[mysqld]
character-set-handshake=FALSE
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
[mysql]
default-character-set=utf8mb4
[client]
default-character-set=utf8mb4
위에 해준 설정을 실제 mysql 설정을 해주는 my.conf파일로 덮어씌어준다.
FROM mysql:5.7
ADD ./my.conf. /etc/mysql/conf.d/my.cnf
현재 nginx가 쓰이는 곳은 두 군데이며 서로 다른 이유로 쓰이고 있다.
하나는 Proxy를 이유로, 하나는 Static 파일을 제공해주는 역할을 하고 있다.
어떤 식으로 nginx가 프록시 기능을 해주는가?