📜 서론
솔직히 이번 프로젝트에서 가장 힘들었던 부분을 꼽으라 하면 배포 부분이라 할 수 있을 것 같다. 내 손으로 직접 CI/CD 과정을 구축하는 게 처음이었기 때문에 내가 하고 있는 작업이 제대로 될 지에 대한 확신이 없어서 심적으로 더 힘들었던 것 같다. 하지만 결국 해냈죠? 구글링을 통한 자료 조사에는 장사가 없었다.
🍃 MongoDB Atlas 연동
새로운 Flex Tier를 소개합니다! 온디맨드 버스트 용량과 예측 가능한 청구(월 $30로 제한)를 갖춘 새로운 비용 효율적인 클러스터 티어입니다. Flex Tier는 M2, M5 및 Serverless 인스턴스를 대체하여 MongoDB Atlas의 기능에 대한 전체 액세스와 가변적인 워크로드에 대한 유연한 용량 및 유연한 가격을 제공합니다.
일단 이게 제일 싸고 비용적으로 효율적인 요금제라 선택했다.(하지만 나중에 서비스 론칭하고 나서 연결량이 제한을 넘어 DB가 터져버리는 바람에 M10요금제로 업그레이드 하게 된다...ㅠ)
잡다한 정보들(집주소, 결제 방식) 설정해주면 클러스터 생성이 끝난다. 참고로 집 주소는 대충 적어도 된다. 중요한건 결제 방식이다. 꼭 마스터 카드 표시 있는 카드로 설정해라.
결제 방식이 제대로 설정되었다면 1달러가 결제되었다가 다시 취소되었다는 결제 내역이 해당 카드에 뜬다. 이 당시 환율이 약 1400원 정도였는데 아마 수수로 때문에 2100원이 뜬 것 같다.
이제 클러스터 생성이 완료되었으니 연결만 하면 된다. Connect
버튼을 누르면 위와 같은 창이 뜨는데, 3번 아래에 있는 URL을 복사해 클러스터 생성 직후에 설정했던 비밀번호를 비밀번호란에 대입해 서버 측 .env
코드에 적용하면 연결이 완료된다.
연결 후 게시물을 작성해보았다.
잘 연결된 걸 확인할 수 있다. 야호!
지금까지의 과정을 잘 정리한 튜토리얼이 있으니 자세하게 알고 싶다면 아래 링크를 참고하자.
https://www.codeit.kr/tutorials/70/mongodb-atlas
MongoDB Atlas 사용법 | 코드잇
MongoDB Atlas에 가입하고 데이터베이스 주소를 사용하는 방법에 대해 알아봅시다. | MongoDB Atlas를 사용하려면 가장 먼저 회원 가입을 해야 합니다. [Atlas 회원 가입 페이지](https://www.mongodb.com/cloud/atla
www.codeit.kr
💾 S3 연동
이제 이미지를 저장할 AWS S3 서비스를 연결해야 한다. 로컬 환경에서는 그냥 백엔드단 폴더에 이미지를 저장하는 식으로 테스트했지만, 이제 배포를 해야하기 때문에 이미지 저장용으로 S3를 선택했다.
📌 CloudFront
여기서 문제는 사용자가 이미지를 조회할 때의 이미지 URL이다. 그냥 S3의 퍼블릭 차단을 풀고 S3에 저장된 이미지의 URL을 통해 유저가 직접 S3에 접근해 이미지를 조회하게 하는 것은 기본이 안된 짓이기 때문에 사이트의 도메인 주소를 통해 접속할 수 있도록 해야 한다. 이를 위해 AWS CloudFront를 활용해야 한다. 다음과 같은 구조다.
📌 백엔드에 S3 연동 코드 작성
사용자가 게시물을 업로드하거나, 프로필 사진을 업데이트 했을 때 이미지 파일을 S3에 업로드 할 수 있도록 코드를 바꿔야 한다.
pnpm i multer-s3 aws-sdk
루트 사용자 액세스 키를 사용하는 대신, AWS IAM에서 사용자를 따로 생성해 액세스 키를 사용하는 방식을 선택했다.
aws-sdk
설치 후 로컬 환경에서 서버를 켜니 다음 문구가 터미널에 뜬다.
(node:21068) NOTE: The AWS SDK for JavaScript (v2) is in maintenance mode.
SDK releases are limited to address critical bug fixes and security issues only.
Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the blog post at https://a.co/cUPnyil
(Use `node --trace-warnings ...` to show where the warning was created)
aws-sdk
는 구버전인 v2
버전이니 v3
를 사용하라고 한다. 찾아보니 v3
버전은 모듈화가 되어 있어 필요한 기능만 받아서 쓰면 된다고 한다. 일단 지금 해야되는건 S3 연결이기에 S3와 관련된 패키지만 받아줬다.
pnpm i @aws-sdk/client-s3
import multer from "multer"; // 파일 업로드용
import path from "path"; // 파일 경로 설정용
import multerS3 from "multer-s3"; // S3 업로드용
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3"; // AWS S3 클라이언트 사용
// ...기존 코드...
// AWS S3 인스턴스 생성
const s3 = new S3Client({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: process.env.AWS_REGION,
});
// 파일 업로드 처리
export const upload = multer({
storage: multerS3({
s3: s3,
bucket: process.env.AWS_BUCKET_NAME,
// 파일 이름 설정
key: function (req, file, cb) {
const ext = path.extname(file.originalname); // 파일 확장자 추출
const originalName = Buffer.from(
path.basename(file.originalname, ext),
"latin1"
).toString("utf8"); // 추출한 파일 이름을 UTF-8로 인코딩
cb(null, originalName + Date.now() + ext); // 파일 이름 설정
},
}),
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
});
// ...기존 코드...
/**
- S3에 업로드된 파일 삭제
- @param {string[]} fileKeys - 삭제할 파일 키 배열
*/
export const deleteUploadedFiles = async (fileKeys) => {
for (const fileKey of fileKeys) {
const params = {
Bucket: process.env.AWS_BUCKET_NAME,
Key: fileKey,
};
try {
await s3.send(new DeleteObjectCommand(params));
console.log(`Successfully deleted file ${fileKey}`);
} catch (err) {
console.error(`Failed to delete file ${fileKey}:`, err);
}
}
};
파일 업로드 및 삭제가 잘 되는 것을 확인할 수 있었다!
📌 CloudFront 대체 도메인 설정
여기서 문제는 이미지의 url이다. Url의 도메인이 CloudFront의 기본 도메인이기 때문. 그래서 대체 도메인을 설정해야 한다. 사용할 도메인은 가비아에서 이미 구입을 해 놓은 상태이니, 가비아에서 구매한 도메인 기준으로 설명을 하겠다.
1. ACM을 통한 SSL 인증서 발급
인증서 요청 클릭
기본 도메인뿐만 아니라 api.re-view.my
, image.re-view.my
와 같은 서브 도메인을 사용할 예정이기 때문에 위와 같이 입력
나머지 설정 그대로 냅두고 넘어가면 위와 같이 뜸. 여기서 CNAME 이름과 값을 복사해서
가비아 도메인 관리 영역의 DNS 레코드 설정에 들어가 해당 값들을 입력해주고 검증을 기다리자.
10분에서 20분 정도 기다리면 되는듯
발급이 완료됐다.
2. 대체 도메인 설정
CloudFront로 돌아와 설정 편집을 해주자. 방금 발급 받은 인증서를 등록해야 하는데... 왜 안뜨지..?
CloudFront의 경우 SSL 인증서를 적용해야 할 경우 us-east-1
리전에서 발급받은 인증서만 가능하다고 한다...
이미지 도메인에 대해서만 us-east-1
리전에서 발급받은 걸 사용해야할 듯 하다.
그래서 다시 했다... 다행히 삭제 하고 나서 같은 옵션으로 하니까 바로 발급됐다.
암튼 이렇게 대체 도메인도 설정하고, 인증서도 가져와서 등록해주면
CloudFront에 대한 대체 도메인 설정이 완료됐다!
참고로 가비아 DNS CNAME 설정도 해줘야 한다.(대체 도메인 - 원래 CloudFront url)
🌐 로컬 환경에서 서버리스 배포 테스트하기
다음 라이브러리들을 설치해주고 코드를 수정해주자.
pnpm i serverless-http
pnpm i -D serverless serverless-offline
import serverless from "serverless-http";
import express from "express";
// ...기존 server.js 코드
const app = express(); // express 인스턴스 생성
// ...기존 server.js 코드
export const handler = serverless(app); // 서버리스 함수 내보내기
다음과 같이 서버리스 환경을 세팅하는 serverless.yml
파일 생성 후 serverless offline
을 실행했다.
service: re-view-app
useDotenv: true # 환경변수 자동 로드
provider:
name: aws
runtime: nodejs18.x
region: ${env:AWS_REGION}
deploymentBucket:
name: ${env:AWS_BUCKET_NAME}
# 환경변수 설정
environment:
#환경 변수들
# IAM 역할 설정
iam:
role:
statements:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:DeleteObject
Resource: "arn:aws:s3:::${env:AWS_BUCKET_NAME}/*"
functions:
app:
handler: server.handler
events:
- httpApi:
path: /{proxy+}
method: "*"
plugins:
- serverless-offline
참고로, 이 코드가 AWS 환경에 배포되면 events - httpApi
설정을 통해 Serverless Framework가 자동으로 API Gateway를 생성하고 Lambda와 연결한다.
로컬 환경에서 서버리스 환경을 띄워보려 하니 이런게 뜬다. 찾아보니 Serverless 프레임워크 V4 버전부터는 최소한 계정은 필요하다고 한다. 프레임워크 제작자들이 서버리스를 통해 일정 금액 이상의 수익을 얻는 사람들로부터 사용료를 받기 위한 것으로 보인다. 서버리스 홈페이지 가입 시 입력했던 정보들을 넣어주자.
다행히 실행이 잘 되는 것을 확인했다. 실시간 알림 기능과 이미지 가져오기를 빼면 말이다. 프론트 단에서 약간의 코드 수정만 거치면 해당 부분은 간단하게 해결되는 부분이기에 크게 문제되진 않는다.
이제 이 설정 그대로 Github Actions를 통해 배포하면 위와 같이 제대로 작동할 것이다.
🚀 Github Actions를 통한 자동화 배포 설정
📌 백엔드 배포 설정
작업에 앞서, 프로젝트 레포지토리 설정 > Security and variables
> Actions
에 들어가서 배포 환경에서 쓰일 환경 변수들을 미리 설정해주자.
name: Deploy to AWS Lambda
on:
push:
branches:
- main
paths:
- "back/**"
- ".github/workflows/backend-deploy.yml"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "18"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: Install Serverless
run: |
npm install -g serverless
- name: Install dependencies
run: |
cd back
npm install
- name: Deploy to Lambda
env:
# 환경 변수들
run: |
cd back
npx serverless deploy
그런 다음 위와 같이 Github Actions를 통해 백엔드 코드를 AWS Lambda로 배포할 수 있도록 설정해주는 backend-deploy.yml
파일을 생성했다.
이제 AWS 콘솔로 이동해 앞에서 IAM을 통해 생성한 사용자에 대해 다음 권한을 추가로 부여한다.
AWS Lambda 서비스에서 함수를 생성해주고, 배포를 해보자.
위 에러가 떠서 찾아보니 환경 변수에 서버리스 Access key를 설정하지 않아 발생한 문제였다. 로컬에 띄울 경우 사용자가 직접 로그인 정보를 입력하면 되지만 자동화 배포 시에는 알아서 인증을 할 수 있도록 해야하기 때문에 서버리스 홈페이지 가입 시 유저에게 부여되는 Access key를 통해 로그인을 할 수 있도록 해야한다.
참고로 람다와 게이트웨이의 timeout 시간을 동일하게 맞춰놔야한다. 그러지 않으면 위와 같은 에러가 뜬다.
📌 배포 후 SSL 인증서 설정해 도메인 연결
앞서 생성한 SSL 인증서는 us-east-1 리전에서 생성한 인증서이기 때문에 지금 이 작업에서는 사용이 불가능하다.
생성한 API Gateway의 리전은 서울이므로 서울 리전에서 SSL을 다시 생성해야한다.
api 도메인과 기본 도메인에 대한 SSL 인증서를 생성해줬다.
이제 가비아에 DNS 레코드를 등록해주자.
생성했던 커스텀 도메인의 API Gateway 도메인 이름을 복사해 값으로 넣으면 된다.
이제 람다에 API 매핑을 하면 끝이다.
조금 기다리니 제대로 작동하는 것을 확인할 수 있었다.
📌 프론트엔드 배포 설정
name: Deploy Frontend to Vercel
on:
push:
branches:
- main
paths:
- "front/**"
- ".github/workflows/frontend-deploy.yml"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "18"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: Install Dependencies
run: |
cd front
pnpm install
- name: Build Project
run: |
cd front
pnpm build
- name: Deploy to Vercel
run: |
vercel deploy --prod --token=${{ secrets.VERCEL_ACCESS_TOKEN }} --yes
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_ACCESS_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
백엔드와 마찬가지로 배포 설정인 frontend-deloy.yml
파일을 작성해준다. 환경 변수는 vercel 사이트에서 따로 설정해 넣어주면 끝이다.
참고로 위 코드의 환경 변수들 중 VERCEL_ORG_ID
라는 게 있는데, 배포 시 org id가 필요하다고 뜬다. 난 팀으로 계정을 생성한 게 아니기 때문에 그냥 가입 시 발급된 유저 아이디를 복사해서 넣으면 된다.
추가적으로 가비아 DNS 설정에서 대체 도메인 설정해주는 것도 잊지 말자. Vercel 사이트 자체에서 "이렇게 하면 돼요" 라고 정말 친절하게 잘 알려준다.
배포한 페이지에서 페이지 이동을 하니 이렇게 뜬다
찾아보니 vercel의 기본 라우팅 방식 설정이 서버 사이드 라우팅 방식으로 설정되기 때문이라 한다.
그리고 이 프로젝트는 Next.js가 아니라 순수 리액트로 만든 프로젝트이기 때문에 다음과 같이 vercel.json
을 설정해줘야 한다.
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
이렇게 모든 배포 과정이 끝났다.
💭 회고
그냥 단순히 기술 스택만 선정하고 막 만드는 게 아니라 전반적인 아키텍쳐를 먼저 확실하게 짠 다음에 개발을 시작할 걸 그랬다. 하지만 이렇게 맨땅에 머리 박아가면서 배우는 방법도 나쁘진 않다고 생각한다. 다만 더 힘들 뿐..
다음부턴 그냥 CI/CD부터 구축한 다음에 프로젝트를 시작하는 게 좋을 것 같다.
📝 참고 자료
- AWS S3로 이미지 배포하기
- AWS API Gateway에 커스텀 도메인 연결하기
- Fixing React Router Issues on Vercel: How to Handle Client-Side Routing and 404 Errors
- AWS S3 적용하기 - IAM & 프로젝트 설정
- AWS :: S3 연결하기
- Lambda API 구축
- AWS Lambda 구축 및 Github Action을 통한 자동 배포
- AWS Lambda에 Node.js Express 애플리케이션 배포하기
- Serverless의 이해 (API Gateway, AWS Lambda)
- 가비아에서 domain을 Vercel의 내 프로젝트와 연결하기
'프로젝트 > Re|view' 카테고리의 다른 글
[Re|view] 11. React Router loader + Tanstack Query로 네트워크 폭포수 방지하기 (0) | 2025.02.27 |
---|---|
[Re|view] 9. 리스트 가상화 기법으로 리뷰 리스트 페이지 최적화하기(feat. Tanstack Virtual) (0) | 2025.02.01 |
[Re|view] 8. 무한 스크롤 적용기 (0) | 2025.01.30 |
[Re|view] 7. FSD 구조 적용 실패기 (0) | 2025.01.28 |
[Re|view] 6. 카카오 로그인 적용기 - Redux Toolkit으로 사용자 정보 관리하기 & 로그아웃 (1) | 2024.09.04 |