회원과 할일 데이터베이스와 기본적인 API를 구현했다.
이제 배포를 한 뒤에 해당 API들을 React 앱에서 사용해보려고 한다. 배포 방법으로는 정말 다양한 방법이 많지만 docker를 이용해서 배포해볼 생각이다.
사용하기에 앞서 docker 이 친구가 무엇이고 어떤 역할을 해주는지 공식문서를 읽어보고 이해해보는게 중요하다. 이번 글은 docker에 대한 글이 아니고 사용하는 과정이기 때문에 설명은 생략한다.
docker를 선택하게 된 계기로는 벡엔드에 초보인 나에게 배포는 하나의 거대한 산이었고 프론트엔드에서 내가 만든 API를 연동하기 위해서 간편한 docker를 선택하였다. 다음은 docker의 간단한 장점이다.
독립된 개발 환경 보장
Container는 격리된 공간이므로 그 자체에 특별한 권한을 주지 않는 한
내부에서 무엇을 하든 Host OS에는 영향을 끼치지 않는다.
개발/운영 환경의 통합
Container 내부 작업을 배포하기 위해서는 해당 Container를 docker image라는 하나의 패키지로 만들어 운영 서버에 전달하기만 하면 된다.
서비스를 개발했을 때의 환경을 다른 서버에서도 똑같이 복제할 수 있기 때문에
각종 라이브러리 설치 등으로 인한 의존성을 걱정할 필요가 없다.
배포 신속성 및 H/W 효율
Kernel을 포함하고 있지 않기 때문에 image 크기가 비교적 작다.
따라서 Application의 배포 속도가 매우 빨라지며, H/W 용량을 작게 차지한다.
기존의 어플리케이션 동작 환경을 수작업으로 작업하는 것을 대신하여 손쉽게 기록하고 수행하는 docker build 명령어가 존재한다.
완성된 이미지를 생성하기 위해 설치되어야 하는 요소들을 DockerFile을 통해 읽고 컨테이너에서 작업을 수행 후 이미지를 만든다.
즉 Dockerfile은 도커 이미지를 빌드하는 방법을 정의하는 스크립트이다. Docker에서는 환경 정보를 저장하는 파일을 Dockerfile이라고 한다. 이 Dockerfile에는 컨테이너의 구동에 필요한 정보가 작성되어 있다.
DockerFile -- (Build) --> Image -- (Create) --> Container
먼저, Express API를 Docker 컨테이너로 만들기 위한 Dockerfile을 작성해야 한다.
이 Dockerfile은 Node.js 환경을 기반으로 애플리케이션을 실행하며, 포트 7777에서 Express 서버를 시작하게 해준다.
그 이후에 docker-compose.yml 파일을 설정해준다. 작성한 파일은 다음과 같다.
# 베이스 이미지로 Node.js 사용
FROM node:21-alpine3.19
# 작업 디렉토리 설정
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn --pure-lockfile
# 애플리케이션 파일들을 컨테이너로 복사
COPY . .
# 애플리케이션이 사용할 포트 (Express 서버가 실행될 포트)
EXPOSE 7777
# 서버 시작 명령어
CMD [ "yarn", "start" ]
docker-compose는 단일 서버에서 여러개의 컨테이너를 하나의 서비스로 정의해서 컨테이너 묶음으로 관리할 수 있다.
user.js 와 todo.js 의 API를 관리하는 Express API 서버와 MySQL 데이터베이스라는 두 개의 컨테이너로 구성되어 있습니다. Docker Compose를 사용하면 이 두 컨테이너를 한 번에 손쉽게 설정하고 관리할 수 있다. 구성한 파일은 다음과 같다.
version: "3.9"
services:
mysql:
image: mysql:8.0
platform: linux/arm64
container_name: planner-mysql-container
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: todo_app
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
api:
build:
context: .
dockerfile: Dockerfile
container_name: express-api
environment:
DB_HOST: mysql
DB_USER: root
DB_PASSWORD: root
DB_DATABASE: todo_app
ports:
- "7777:7777"
depends_on:
- mysql
volumes:
mysql-data:
‼️ 위와 같이 작성하고 build를 시켜주면 문제가 발생하였다. ‼️
MySQL 서비스는 반드시 먼저 빌드 및 실행되어야 한다. API 서비스는 MySQL이 실행된 후에 빌드 및 실행된다. 하지만, 두 서비스를 순차적으로 빌드하기보다, docker-compose가 depends_on을 고려하여 자동으로 순서를 조정하게 하였다. 빌드 후 docker-desktop을 보니 express-api 컨테이너는 작동이 안되고 있었다.
오류를 확인해보니 오류 메시지는 Express 서버에서 MySQL 데이터베이스에 연결하려고 할 때 발생한 문제로, ECONNREFUSED는 MySQL 서버에 연결을 시도했지만 거부되었을 때 발생하는 오류였다. 일반적으로 이 오류는 MySQL 서버가 아직 완전히 준비되지 않았거나, API 서비스가 MySQL에 연결할 때 문제가 생겼을 때 발생한다.
따라서 해결방안을 찾아본 결과 MySQL 컨테이너의 준비 상태를 확인하는 healthcheck를 추가하면 MySQL 서버가 완전히 준비되기 전에 API 서비스가 실행되지 않도록 할 수 있었다. Health check는 Docker에서 컨테이너의 상태를 주기적으로 확인하는 방법으로, 서비스가 제대로 작동하는지 감지하고 그에 따라 적절한 조치를 취한다. 변경 후는 다음과 같다.
services:
mysql:
image: mysql:8.0
platform: linux/arm64
container_name: planner-mysql-container
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: todo_app
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
// test: mysqladmin ping -h localhost 명령을 실행하여 MySQL 서버가 응답하는지 확인
// interval: 이 명령은 10초마다 실행
// timeout: 명령이 5초 안에 응답하지 않으면 실패로 간주
// retries: 최대 3번 재시도하여 여전히 실패하면 컨테이너는 Unhealthy 상태로 간주
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 3
api:
build:
context: .
dockerfile: Dockerfile
container_name: express-api
environment:
DB_HOST: mysql
DB_USER: root
DB_PASSWORD: root
DB_DATABASE: todo_app
ports:
- "7777:7777"
depends_on:
mysql:
condition: service_healthy
volumes:
mysql-data:
이렇게 한 후 docker desktop을 열어서 확인하면

이렇게 떠있다. 그럼 실행을 시켜주고 프론트와 연결 시도를 하면 된다.
2개의 컨테이너를 모두 실행시킨 후 프론트로 돌아와서 연결을 시도하였다.
늘 하던 API 연결 과정이라 어려움은 없었고 다음과 같이 진행했다. 테스트 용도이기 때문에 App 컴포넌트에서 진행하였다.
import { useEffect, useState } from "react";
import Header from "./components/Header";
import Input from "./components/Input";
import axios from "axios";
const signUp = async () => {
const body = {
userId: "gunyongbokId4",
password: "qhrrjsdyd4",
name: "gunyongbokname4",
};
try {
const response = await axios.post(
"http://localhost:7777/users/signup",
body
);
console.log("response >>", response);
} catch (err) {
console.log("Err >>", err);
}
};
function App() {
useEffect(() => {
signUp();
}, []);
return (
// ~~~ 기타 코드
}
export default App;
다음과 같이 작성하고 빌드해보니 아래와 같은 CORS 오류가 발생하였다.

구글링을 통해 간단히 알아보니 CORS는 함축 단어로써 이를 풀면 Cross-Origin Resource Sharing 이라는 단어로 이루어져 있다. 여기서 교차 출처라고 하는 것은 (엇갈린) 다른 출처 때문에 발생하는 것이었다.
우선은 모두에게 허용해주는 방법을 사용하여 server.js의 코드에 아래 코드를 추가해주어서 해결하였다.
const cors = require("cors");
app.use(cors());
추가로 특정 도메인에만 허용해줄 수 있을 수도 있다. 후에 프론트를 배포한 다음에는 수정해주어야겠다.
정말 기본적인 API 설계 및 배포를 진행하며 크고 작은 에러를 접하고 배운점도 많다.
이제 시작이고 예외처리와 인증/인가 및 토큰 처리 등 프로젝트를 하며 가져다 사용했던 기능들을 하나씩 직접 만들어볼 예정이다.
번외로 프론트에서 추가한 데이터가 데이터베이스에 저장되는 것을 눈으로 직접 확인하니 신기했다.
그리고 지금 데이터베이스를 터미널에서 접근해서 확인하고 있는데 상당히 불편하다. 다른 방법이 없는지 찾아봐야겠다.