Open LangVE opened 2 years ago
8장 읽었습니다.
첫번째 설계 방식은 엔진엑스를 두 가지 역할로 이용 엔진엑스에 있는 프록시 기능과 정적 파일을 제공하는 기능 클라이언트에서 요청을 보낼 때 요청이 프런트엔드 서버를 위한 것이면 프런트엔드 서버 쪽으로 보내고 백엔드 서버를 위한 요청이라면 백엔드 서버로 보낸다. 정적 파일을 제공하는 기능은 프런트엔드 서버에 많은 정적 파일이 있는데, 클라이언트에서 이러한 파일을 요청하면 전달.
요청을 보낼 때 호스트의 이름이 바뀌더라도 경로 부분을 변경하지 않아도 되는 점 포트 자체를 넣지 않아도 되므로 요청을 보내는 경로의 포트가 바뀌어도 포트를 바꿔주지 않아도 된다는 점
엔진엑스의 설정하는 부분이 다소 복잡하며 전체적인 설계도 다소 복잡
설계가 다소 간단해서 구현하기가 더 쉽다
호스트 이름이나 포트 변경이 있을 떄 모든 요청의 경로를 변경해야 한다는 점
폴더생성 /docker-multi-app
backend
backend 폴더 아래에 package.json 파일을 생성 npm init 명령어를 이용 or 직접 package.json 파일을 만든 다음 필요한 소스 코드를 작성
package.json 파일에 스크립트와 사용할 모듈 명시
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "",
"start": "nodeserver.js",
"dev": "nodemonserver.js"
},
"dependencies": {
"express": "4.16.3",
"mysql": "2.16.0",
"nodemon": "1.18.3",
"body-parser": "1.19.0"
},
"author": "",
"license": "ISC"
}
시작점이 되는 server.js 파일 생성
server.js
// 핑요한 모듈 가져오기
const express = require("express");
// 익스프레스 서버 생성
const app = express();
// JSON 형태로 전달되는 요청의 본문을 해석할 수 있게 등록
app.use(express.json());
app.listen(5000, () => {
console.log('애플리케이션이 5000번 포트에서 시작됐습니다.');
});
Node,js 애플리케이션과 Node,js 애플리케이션을 연결하기 위한 db.js 파일을 생성
생성한 db.js 파일에 호스트, 유저 이름, 비밀번호, 데이터베이스 이름을 명시해 pool을 생성. 그리고 생성한 pool을 다른 파일에서도 사용할 수 있게 익스포트.
const mysql = require("mysql");
const pool = mysql.createPool({
connectionLimit: 10,
host: 'mysql',
user: 'root',
password: 'password',
database: 'myapp',
port: 3306
});
exports.pool = pool;
익스포트한 pool을 server.js 에서 불러온다. db.js 파일 안에 pool이 들어 있기 때문에 db.js 파일을 가져오면 pool을 가져오게 된다.
...
const db = require(./db)
...
애플리케이션에서 필요한 두 가지 API 구현
...
// DB의 lists 테이블에 있는 모든 데이터를 프런트엔드 서버로 보내주기
app.get('/api/values', function (req, res) {
// DB에서 모든 정보 가져오기
db.pool.query('SELECT * FROM lists;',
(err, results, fileds) => {
if(err)
return res.status(500).send(err);
else
// 프런트엔드에 DB에서 가져온 정보를 json 형식으로 보냅니다.
return res,json(results);
})
})
// 클라이언트에서 입력한 값을 DB의 lists 테이블에 넣어주기
app.post('/api/value', function (req, res, next) {
// 데이터베이스에 값 넣어주기
db.pool.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});
})
})
create-react-app으로 리액트 애플리케이션 생성
npx create-react-app frontend
명령어 실행하면 리액트 애플리케이션 생성
App.js 파일에 UI를 위한 소스 코드를 작성
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return {
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<div className="container">
<form className="example">
<input type="text" placeholder="입력해주세요..." />
<button type="submit">확인</button>
</form>
</div>
</header>
</div>
};
}
export default App;
UI에 CSS를 적용하기 위해 App.css 파일에 다음과 같이 CSS 코드를 추가
...
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
tp {
transform: rotate(360deg);
}
}
.container {
width: 375px;
}
form.example input {
padding: 10px;
font-size: 17px;
border: 1px solid grey;
float: left;
width: 74%;
background: #f1f1f1;
}
form.example button {
float: left;
widh: 20%;
padding: 10px;
background: #2196F3;
color: white;
font-size: 17px;
border: 1px solid grey;
border-left: none;
cursor: pointer;
}
form.example button:hover {
background: #ob7dda;
}
form.example::after {
content: "";
clear: both;
display: table;
}
데이터의 흐름을 위한 스테이트를 생성
import React, {useState} from 'react';
import logo from './logo.svg';
import './App.css';
// 백엔드 서버와 비동기 통신을 하기 위해 axios 라이브러리를 가져온다
import axios from 'axios';
function App() {
//DB에 저장된 값을 가져와서 화면에 보여주기 전에 이 lists state에 넣어줌
const [lists, setLists] = useState([]);
// Input 박스에 입력한 값이 이 value State에 들어감
const [value, setValue] = useState("");
... 생략 ...
리액트에서는 State를 이용해 데이터의 흐름을 처리. 그리고 State를 이용하려면 react 라이브러리에서 useState 훅을 불러와야 한다. 이 useState 훅을 이용해 lists와 value State를 생성. 그리고 나서 lists State를 배열 형식으로 입력하면 입력한 글들이 lists에 하나씩 들어감. 그리고 value State는 문자열 형식으로 현재 입력하는 문자열이 들어감.
App.js에서는 axios라는 브라우저와 Node.js를 위한 비동기 통신 라이브러리를 가져옴 그러기 위해서는 package.json 파일에 axios 라이브러리를 명시
...
"dependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"axios": "0.19.2"
}
...
데이터베이스에서 데이터를 가져오는 데 필요한 useEffect추가
// useEffect를 사용하기 위해 react 라이브러리에서 가져온다.
import React, {useState, useEffect} from 'react';
import logo from './logo,svg';
import './App.css';
import axios from 'axios';
function App() {
//DB에 저장된 값을 가져와서 화면에 보여주기 전에 이 lists State에 넣어줍니다.
const [lists, setLists] = useState([]);
//Input 박스에 입력한 값이 이 value State에 들어갑니다.
const[value, setValue] = useState("");
useEffect(() => {
// 여기에 데이터베이스에 있는 값을 가져옵니다.
}, [])
}
애플리케이션의 기능 구현
import React, {useState} from 'react';
import logo from './logo.svg';
import './App.css';
// 백엔드 서버와 비동기 통신을 하기 위해 axios 라이브러리를 가져온다
import axios from 'axios';
function App() {
//DB에 저장된 값을 가져와서 화면에 보여주기 전에 이 lists state에 넣어줌
const [lists, setLists] = useState([]);
// Input 박스에 입력한 값이 이 value State에 들어감
const [value, setValue] = useState("");
useEffect(() => {
// 여기에 데이터베이스에 있는 값을 가져옵니다.
axios.get('/api/values')
.then(response => {
console.log('response', response);
setLists(response.data);
})
}, []);
// Input 박스에 값을 입력(onChange 이벤트가 발생) 할 대마다
// Value State를 변경합니다.
const chageHandler = (event) => {
setValue(event.currentTarget.value);
};
// Input 박스에 값을 입력하고 확인 버튼을 누르면
// 입력한 값이 데이터베이스에 저장되고
// 화면에 값을 보여줍니다.
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>
))}
<br/>
안녕하세요
// 확인 버튼을 누르면 onSubmit 이벤트가 발생하고
// submitHandler 함수를 호출합니다.
<form className="example" onSubmit={submitHandler}>
<input type="text" placeholder="입력해주세요..."
// 값을 입력할 때마다 onChange 이벤트가 발생하고
// changeHandler 함수를 호출합니다.
onChange={changeHandler}
// Input 박수의 value를 state의 value로 컨트롤합니다.
value={value}
/>
<button type="submit">확인</button>
</form>
</div>
</header>
</div>
};
export default App;
}
frontend 폴더에 두 개의 도커 파일을 생성. Dockerfile.dev dockerfile
개발 환경을 위한 도커 파일을 작성
# 베이스 이미지를 도커 허브에서 가져옵니다
FROM node:alpine
# 해당 어플리케이션의 소스 코드가 이 디렉터리로 들어갑니다
WORKDIR /app
# 소스 코드가 바뀔 때마다 종속성까지 다시 복사하지 않도록
# 먼저 종속성 목록을 담고 있는 package.json을 복사합니다.
COPY package.json ./
# package.json에 명시된 종속성을 설치합니다.
RUN npm install
# 로컬에 있는 모든 소스 코드를 WORKDIR로 복사합니다.
COPY ./ ./
# 컨테이너가 시작되면 실행할 명령어를 명시합니다.
CMD ["npm", "run", "start"]
운영 환경을 위한 도커 파일을 작성.
FROM node:alpine as builder
WORKDIR /app
COPY package.json ./
RUN npm install
COPY ./ ./
RUN npm run build
# 엔진엑스를 기동하고 앞서 생성한 빌드 파일을 제공합니다.
# default.conf에 해준 설정을 엔진엑스 컨테이너 안으로 복사합니다
FROM nginx
EXPOSE 3000
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from-builder /app/build /usr/share/nginx/html
엔진엑스 설정 파일을 작성
엔진엑스 설정을 작성할 파일을 생성 frontend/nginx/default.conf
default.conf 설정작성
server {
listen 3000;
location / {
# HTML 파일이 위치할 루트 경로를 성정
root /usr/share/nginx/html;
# index 페이지의 파일명을 설정
index index.html index.htm;
# 리액트 라우터를 사용해 페이지를 이동할 때
# 이 부분이 필요합니다.
try_files $uri $uri/ /index,html;
}
}
mysql 폴더를 생성하고, 도커 파일 생성
도커 파일에 소스 코드를 작성
# mysql 베이스 이미지를 도커 허브에서 가져옵니다.
FROM mysql:5,7
MySQL을 시작할 때 데이터베이스와 테이블이 필요하므로 이를 작성할 파일 생성 ../sqls/initialize.sql 파일 생성
initialize.sql 파일에 데이터베이스와 테이블을 생성하는 코드 작성
DROP DATABASE IF EXISTS myapp;
CREATE DATABASE myapp;
USE myapp;
CREATE TABLE lists (
id INTEGER AUTO INCREMENT,
value TEXT,
PRIMARY KEY (id)
);
한글로 저장할 수 있게 설정 mysql 폴더 아래에 my.cnf 파일 생성 엔진엑스 설정은 default.conf 파일에 작성
한글이 깨지는 현상을 막기 위해 my.cnf 파일 인코딩 설정
[mysqld]
character-set-server=utf8
[mysql]
default-character-set=utf8
[client]
default-character-set=utf8
실제 컨테이너 안에서 MySQL 설정을 하는 my.cnf 파일을 앞서 작성한 파일로 덮어씀
FROM mysql:5.7
ADD ./my.cnf /etc/mysql/conf.d/my.cnf
설정이 잘 됐는지는 8.9절에서 도커 컴포즈를 작성한 후에 도커 컴포즈로 MySQL 컨테이너를 실행해 확인
nginx 폴더 생성하고, nginx 폴더에 설정 파일과 도커 파일을 생성. 설정파일 default.conf 도커 파일의 파일명 Dockerfile 지정
default.conf 파일에 프록시 기능을 구현하기 위한 코드 작성
# 3000번 포트에서 프런트엔드가 작동하고 있다는 것을 명시
upstream frontend {
server frontend: 3000;
}
# 5000번 포트에서 백엔드가 작동하고 있다는 것을 명시
upstream backend {
server backend: 5000;
}
server {
listen 80;
#'/' 경로로 시작하는 요청은 http://frontend로 보냅니다.
location / {
proxy_pass http://frontend;
}
# '/api' 경로로 시작하는 요청은 http://backend로 보냅니다.
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";
}
}
엔진엑스를 위한 도커 파일을 작성
# 엔진엑스 베이스 이미지를 가져옵니다
FROM nginx
# default.conf에 작성된 것을 컨테이너에서 실행될 nginx에도
# 적용될 수 있게 COPY 명령어로 복사
COPY ./default.conf /etc/nginx/conf.d/default.conf
최상위 디렉토리에 도커 컴포즈 파일인 docker-compose-dev.yml 생성
서비스를 위한 기본 구조 작성
frontend 서비스를 위한 설정 작성
nginx 서비스를 위한 설정을 작성
backend 서비스를 위한 설정을 작성
MySQL 서비스를 위한 설정을 작성
모든 설정이 끝났다면 도커 컴포즈로 애플리케이션을 시작
8.7절 인코딩 설정 확인
version: "3"
service:
frontend:
# 개발 환경을 위한 도커 파일이 위치한 경로를 알려줍니다.
build:
dockerfile: Dockerfile.dev
context: ./frontend
# 볼륨을 설정합니다.
volumes:
- /app/node_modules
- ./frontend:/app
# 리엑트 애플리케이션에서 발생하는 버그를 해결합니다.
stdin_open: true
nginx:
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:
- "3306:3306"
volumes:
- ./mysql/mysql_data:/var/lib/mysql
- ./mysql/sqls/:/docker-entrypoint-initdb.d/
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: myapp
목표