
Next.js는 Vercel 배포와 궁합이 좋다, Vercel이 편하다는 말은 정말 많이 들어왔고, 저 역시도 늘 Vercel을 애용해왔습니다. git push 한 번이면 빌드부터 배포까지 알아서 해주니, 그동안은 배포에 대해 딱히 고민할 일이 없었습니다.
사실 AWS 안에도 Amplify처럼 Vercel과 비슷하게 빌드와 배포를 알아서 처리해주는 서비스가 있습니다. 처음엔 이쪽을 먼저 알아봤습니다. 러닝커브가 낮다고들 하니 부담도 적었고, AWS를 써본다는 것만으로도 의미가 있을 것 같았습니다.
그런데 곰곰히 생각해보니 제가 궁금했던 건 지금까지 Vercel이 대신 처리해주던 것들이 실제로 어떻게 동작하는지였습니다. Amplify를 쓰면 그 부분은 여전히 안 보일 것 같았습니다.
또한 클라우드나 인프라 쪽에 관심은 계속 있었는데, 용어도 낯설고 뭐부터 해봐야 할지 몰라서 진입장벽이 높다고만 느끼고 경험해보지 않은 것도 사실입니다. 이번엔 그 장벽을 우회하지 않고, 서버 하나를 직접 만들고 하나씩 붙여보면서 제대로 부딪혀보고 싶었습니다.
그래서 이번엔 AWS EC2에 Next.js를 직접 올려보기로 했습니다.
서버 세팅, Nginx, 도메인 연결, HTTPS, GitHub Actions 자동 배포까지 하나씩 붙여나갔는데, 그 과정에서 부딪힌 이슈들과 배포 CI/CD 파이프라인 구축기를 적어보려 합니다.
참고 자료
Next.js 배포(feat. AWS EC2, 도메인 연결, 자동 배포 설정)
[AWS EC2] Next.js 프로젝트 배포하기 (ubuntu + nginx + pm2)
EC2(Elastic Compute Cloud)는 AWS에서 가상 서버를 빌려 쓸 수 있는 서비스입니다.
물리적인 컴퓨터를 직접 사서 관리하는 대신, AWS 데이터센터에 있는 컴퓨팅 자원을 시간 단위로 빌려서 쓰는 개념입니다.
Vercel이 "배포"라는 결과물만 다뤘다면, EC2는 그 결과물이 올라갈 서버 자체부터 직접 만드는 것부터 시작합니다.
AWS 콘솔에 로그인한 뒤, 검색창에 EC2를 입력해서 EC2 대시보드로 이동합니다.
가장 먼저 확인해야 할 건 우측 상단 리전입니다. 기본값이 미국 리전으로 잡혀 있는 경우가 많은데, 리전에 따라 지연 시간(latency)이 달라지기 때문에 실제 사용자가 접속할 위치와 가까운 리전을 선택해야 합니다. 저는 아시아 태평양(서울) ap-northeast-2로 변경했습니다.
리전을 서울로 맞춘 뒤 인스턴스 시작 버튼을 클릭합니다.
인스턴스를 구분할 수 있는 이름을 입력합니다. 여러 개를 운영하게 될 수도 있으니, 나중에 알아보기 쉬운 이름으로 지어두는 게 좋습니다.
저는 Ubuntu를 선택했습니다. Ubuntu, Amazon Linux 둘 다 프리 티어 대상 AMI지만, 상대적으로 사용자층이 넓고 자료가 많은 Ubuntu가 문제가 생겼을 때 검색으로 해결하기 수월하다고 판단했습니다.
인스턴스 유형으로는 t2.micro를 선택했습니다.
AWS 프리 티어 대상 인스턴스 유형으로, 신규 계정 기준 12개월 동안 월 750시간까지 무료로 제공됩니다.
한 달이 최대 744시간이니, 인스턴스 하나를 계속 켜놔도 시간 제한에 걸리지 않는 수준입니다.
다만 이 혜택은 신규 계정 기준이라, 예전에 AWS 계정을 만들었다가 해지한 이력이 있으면 다시 가입해도 프리 티어 대상에서 제외될 수 있습니다. 저도 이 케이스에 해당해서 실제로는 무료 혜택 없이 진행했는데, 다행히 t2.micro 자체가 워낙 저렴해서 청구 금액이 적었습니다.

서버에 접속할 때 쓸 키 페어를 새로 만들기 위해 새 키 페어 생성을 누릅니다.

키 페어 이름을 정하고, 키 유형은 RSA, 형식은 .pem으로 선택 후 키 페어 생성 버튼으로 생성하면 됩니다.
다운로드되는
.pem파일은 이 서버에 접속할 수 있는 유일한 인증 수단입니다.
다시 다운로드할 수 없으니 안전한 위치에 보관해야 하고, 특히 Git 저장소에는 절대 올리면 안 됩니다.
인바운드 규칙에서 SSH(22)는 기본으로 열려있었고, 여기에 HTTP(80)·HTTPS(443)를 추가로 체크해서 허용했습니다.
이 시점엔 서버 안에 아직 아무 애플리케이션도 없기 때문에, 서버 접속용(SSH)과 나중에 웹사이트를 서빙할 포트(HTTP·HTTPS)만 미리 열어두고, 나머지는 필요해질 때 추가하려 했습니다.
기본값인 8GiB에서 30GiB까지 늘렸습니다. 나중에 패키지나 로그가 쌓일 걸 감안해서 여유 있게 잡아뒀습니다.
모든 설정을 확인하고 인스턴스 시작을 누르면 몇 초 안에 인스턴스가 생성됩니다.
pem 키를 다운로드한 폴더로 이동한 뒤, 접속에 필요한 두 가지 명령어를 순서대로 실행합니다.
# pem 파일 권한을 소유자만 읽기 가능하도록 제한
chmod 400 <키파일명>.pem
# pem 키로 EC2 서버에 SSH 접속
ssh -i <키파일명>.pem ubuntu@<퍼블릭 IP>
여기서
<퍼블릭 IP>자리에는 퍼블릭 IPv4 주소뿐 아니라 퍼블릭 IPv4 DNS(예:ec2-x-x-x-x.ap-northeast-2.compute.amazonaws.com형태)를 넣어도 됩니다. 둘 다 같은 서버를 가리키는 주소라 결과는 동일합니다.
chmod 400을 먼저 해준 이유는, SSH가 개인 키 파일의 권한이 너무 열려 있으면(다른 사용자도 읽을 수 있으면) 보안 문제로 접속 자체를 거부하기 때문입니다. 소유자만 읽을 수 있도록 권한을 좁혀줘야 합니다.
Ubuntu AMI는 계정명이 ubuntu로 고정되어 있어서 ubuntu@를 꼭 붙여야 합니다. 처음 접속할 때는 호스트를 신뢰할지 묻는데, 처음 만나는 서버라 당연한 절차이니 yes를 입력하면 됩니다.
접속에 성공하면 아래처럼 우분투 배너와 함께 프롬프트가 ubuntu@ip-... 형태로 바뀝니다.
# 패키지 목록 최신화
sudo apt-get update
# nodesource 저장소 등록 (20.x LTS)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
# Node.js 20 설치
sudo apt-get install -y nodejs
apt-get update로 패키지 목록을 최신화하고, 우분투 기본 저장소 대신 nodesource 스크립트로 설치한 이유는 기본 저장소의 Node.js 버전이 너무 낮기 때문입니다. 원하는 LTS 버전을 바로 지정해서 설치할 수 있어 훨씬 간단합니다.
저는 해당 프로젝트가 pnpm을 쓰고 있어서 pnpm 기준으로 정리했습니다.
npm이나 yarn을 쓰신다면 이후 나오는pnpm install,pnpm build,pnpm start같은 명령어를 각자 쓰시는 패키지 매니저에 맞게 바꾸시면 됩니다.
pnpm은 corepack으로 설치했는데, 막상 실행해보니 최신 pnpm이 Node.js 22.13 이상을 요구하고 있었습니다. (이 글을 보시는 시점의 pnpm 최신 버전 요구사항은 다를 수 있으니, Node 20으로 문제없이 되면 이 단계는 건너뛰셔도 됩니다.)
Node 20으로는 pnpm 명령어 자체가 안 먹혀서, 한 번 더 22.x LTS로 올려야 했습니다.
# nodesource 저장소 등록 (22.x LTS)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
# Node.js 22로 재설치
sudo apt-get install -y nodejs
# corepack 활성화 (pnpm/yarn 버전 관리 도구)
corepack enable
# 최신 pnpm 설치 및 활성화
corepack prepare pnpm@latest --activate
저는 이때 HTTPS 방식으로 clone했는데, 이게 나중에 GitHub Actions 자동 배포 단계에서 인증 문제를 일으켰습니다. 어떤 문제였는지, HTTPS와 SSH 중 뭘 쓰든 어떻게 해결하면 되는지는 5번에서 다루겠습니다.
git clone <레포 URL>
레포를 clone하려는데 이런 에러가 났습니다.
Public 저장소라면 이 에러 자체를 겪지 않으니 이 단계는 넘어가시면 됩니다.
저는 private 저장소를 쓰고 있어서 겪은 문제입니다.
GitHub는 2021년부터 비밀번호 인증을 막아뒀기 때문에, Username/Password를 물어볼 때 비밀번호 자리에 Personal Access Token(PAT) 을 입력해야 합니다.
Settings → Developer settings → Personal access tokens에서 repo 권한으로 발급받아 사용했습니다.
pnpm install
돌렸더니 이번엔 이런 에러가 났습니다.
[ERROR] Command was killed with SIGKILL (Forced termination)
이건 t2.micro(RAM 1GB)처럼 메모리가 작은 인스턴스에서만 겪는 문제입니다. 더 큰 인스턴스를 쓰신다면 이 단계는 필요 없을 수 있습니다.
원인은 메모리였습니다. 패키지를 780개 이상 한 번에 설치하려니 메모리가 부족해져서 리눅스 커널이 프로세스를 강제 종료(OOM Kill)시킨 것이었습니다.
물리 메모리가 부족할 때 디스크 일부를 메모리처럼 빌려 쓰게 해주는 스왑(swap) 메모리를 추가해서 해결했습니다.
# 2GB 크기의 스왑 파일 생성
sudo fallocate -l 2G /swapfile
# 소유자만 읽고 쓸 수 있도록 권한 제한
sudo chmod 600 /swapfile
# 해당 파일을 스왑 영역으로 포맷
sudo mkswap /swapfile
# 스왑 활성화
sudo swapon /swapfile
# 재부팅 후에도 스왑이 자동으로 켜지도록 등록
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
마지막 줄은 재부팅 후에도 스왑 설정이 유지되도록 등록하는 부분입니다. 스왑을 걸어준 뒤 free -h로 정상 적용됐는지 확인했습니다.
이후로는 pnpm install이 느리긴 해도 죽지 않고 끝까지 완료됐습니다.
pnpm build
pnpm start
http://<퍼블릭 IP>:3000으로 접속했는데 무한 로딩만 걸렸습니다. 서버는 3000번 포트에서 정상적으로 실행 중이었는데, 문제는 EC2 보안 그룹이었습니다.
처음 인스턴스를 만들 때 22, 80, 443번만 열어뒀기 때문에, 방화벽 바깥에서 3000번 포트로는 아예 접근이 막혀 있었던 것입니다.
따라서 인바운드 규칙에 사용자 지정 TCP 3000, 소스 0.0.0.0/0을 추가하니 바로 해결됐습니다.
http://<퍼블릭 IP>:3000에 정상적으로 접속되는 것을 확인할 수 있습니다.
pnpm start로 띄운 서버는 지금 연결된 SSH 세션 안에서만 살아있습니다. 터미널을 닫거나 SSH 연결이 끊기면 서버 프로세스도 같이 죽습니다. 그래서 PM2로 백그라운드 프로세스로 등록했습니다.
# PM2 전역 설치
sudo npm install -g pm2
# pnpm start를 PM2로 백그라운드 실행
pm2 start pnpm --name nextjs-app -- start
# 출력된 sudo 명령어를 그대로 한 번 더 실행 (재부팅 시 자동 시작 등록)
pm2 startup
# 현재 프로세스 목록 저장
pm2 save
pm2 startup은 EC2가 재부팅되더라도 서버가 자동으로 다시 켜지도록 시스템에 등록해주는 절차이고, pm2 save는 지금 실행 중인 프로세스 목록을 저장해서 재부팅 후에도 그대로 복원되게 해줍니다.
# 현재 PM2가 관리 중인 프로세스 목록과 상태 확인
pm2 list
pm2 list로 확인했을 때 상태가 online으로 뜨면 정상적으로 백그라운드에서 돌고 있는 것입니다.
:3000을 붙이지 않고 IP나 도메인만으로 접속되게 하려면, 80번 포트로 들어온 요청을 내부적으로 3000번 포트로 넘겨주는 중개자가 필요합니다. 이 역할을 Nginx가 맡습니다.
Nginx는 웹 서버이자 리버스 프록시로 널리 쓰이는 오픈소스 소프트웨어입니다.
리버스 프록시란, 외부에서 들어온 요청을 그대로 받는 대신 내부의 다른 서버(여기선 3000번 포트에서 돌아가는 Next.js)로 대신 전달해주는 중간 역할을 말합니다. 덕분에 실제 애플리케이션은 외부에 직접 노출되지 않고, Nginx가 앞단에서 트래픽을 정리해서 넘겨줍니다.
# Nginx 설치
sudo apt-get install nginx -y
# 기본 설정 파일 편집
sudo nano /etc/nginx/sites-available/default
server 블록 안의 location /을 아래 내용으로 교체합니다.
location / {
proxy_pass http://localhost:3000; # 요청을 3000번 포트(Next.js)로 전달
proxy_http_version 1.1; # 프록시 시 HTTP 1.1 사용 (업그레이드 헤더 처리에 필요)
proxy_set_header Upgrade $http_upgrade; # 원본 요청의 Upgrade 헤더 유지
proxy_set_header Connection 'upgrade'; # 커넥션 업그레이드(웹소켓 등) 허용
proxy_set_header Host $host; # 원본 요청의 Host 헤더 유지
proxy_cache_bypass $http_upgrade; # 업그레이드 요청은 캐시하지 않음
}
proxy_pass가 실제 요청을 3000번 포트로 전달하는 부분이고, 나머지 헤더 설정들은 요청 정보(원래 호스트, 업그레이드 헤더 등)가 프록시를 거치면서 유실되지 않도록 그대로 넘겨주는 역할입니다.
# 설정 파일 문법 검사
sudo nginx -t
# Nginx 재시작 (설정 반영)
sudo systemctl restart nginx
nginx -t로 문법 오류를 먼저 검사하고 재시작하는 게 안전합니다. 설정 파일에 오타가 있으면 재시작 자체가 실패하기 때문입니다.
이제 http://<퍼블릭 IP> 만으로 접속이 됩니다.
도메인은 팀에서 가비아로 이미 구매해둔 상태였습니다.
IP 주소를 사람이 기억하기 쉬운 도메인으로 연결하려면 DNS에 A 레코드를 등록해야 합니다. A 레코드는 도메인을 특정 IPv4 주소와 매핑해주는 설정입니다.
| 타입 | 호스트 | 값 |
|---|---|---|
| A | @ | EC2 퍼블릭 IP |
| A | www | EC2 퍼블릭 IP |
DNS 변경 사항이 전 세계에 전파되는 데는 보통 10분에서 1시간 정도 걸리는데, nslookup 명령어로 반영 여부를 확인할 수 있습니다.
# 도메인이 어떤 IP로 연결되는지 조회
nslookup <도메인>
A 레코드를 등록하기 전에는 이렇게 도메인을 찾을 수 없다는 응답이 돌아옵니다.
반영이 완료된 후에는 지정해둔 IP로 정상적으로 연결되는 것을 확인할 수 있습니다.
반영이 확인되면 Nginx의 server_name도 도메인으로 바꿔줍니다.
이 설정이 있어야 Nginx가 "이 도메인으로 온 요청은 이 서버 블록으로 처리해라"라고 정확히 인식합니다.
# IP 대신 도메인으로 들어온 요청도 이 서버 블록이 처리하도록 지정
server_name <도메인> www.<도메인>;
# 설정 파일 문법 검사
sudo nginx -t
# 설정만 다시 불러오기 (서비스 중단 없이 반영)
sudo systemctl reload nginx
이제 http://<도메인>으로도 접속이 됩니다.
HTTP는 데이터를 암호화하지 않고 그대로 주고받기 때문에, 실제 서비스라면 HTTPS 적용이 필수입니다.
Let's Encrypt의 무료 인증서를 Certbot으로 발급받아 적용했습니다.
# Certbot과 Nginx 연동 플러그인 설치
sudo apt-get install certbot python3-certbot-nginx -y
# SSL 인증서 발급 및 Nginx 자동 설정
sudo certbot --nginx -d <도메인> -d www.<도메인>
실행하면 순서대로 몇 가지를 물어봅니다.
Y 입력 후 엔터 (동의하지 않으면 인증서 발급 자체가 진행되지 않음)Y/N 아무거나 원하는 대로 선택하면 됨
이 과정이 끝나면 인증서 발급은 물론, Nginx 설정 파일까지 Certbot이 알아서 HTTPS용으로 고쳐줍니다.
https://<도메인>으로 접속했을 때 "연결이 안전함", "인증서가 유효함" 문구가 뜨면 성공입니다.
Let's Encrypt 인증서는 90일마다 만료되는데, Certbot이 갱신 작업을 자동으로 걸어줍니다.
아래 명령어로 실제 갱신 없이 갱신 로직이 정상 작동하는지 미리 테스트해볼 수 있습니다.
# 실제 갱신 없이 갱신 로직만 테스트
sudo certbot renew --dry-run
여기까지는 코드가 바뀔 때마다 EC2에 직접 SSH로 들어가서 git pull, pnpm build, pm2 restart를 손으로 해줘야 하는 반자동 상태였습니다.
팀 프로젝트에서 이 과정을 개발자가 매번 반복하는 건 비효율적이고 실수하기도 쉽다고 생각해서, main 브랜치에 push되면 자동으로 배포되도록 GitHub Actions를 구성했습니다.
워크플로우가 EC2에 SSH로 접속하려면 pem 키가 필요한데, 이걸 코드에 그대로 넣어버릴 수는 없으니 GitHub의 Secrets 기능을 이용해 안전하게 보관했습니다.
로컬 컴퓨터(EC2 서버 안이 아니라 pem 키를 다운로드한 그 컴퓨터)에서 pem 키 내용을 확인합니다.
# pem 키 파일 전체 내용을 터미널에 출력
cat <키파일명>.pem
----BEGIN RSA PRIVATE KEY-----부터 ----END RSA PRIVATE KEY-----까지, 시작과 끝 줄을 포함한 전체 내용을 복사합니다.
그 다음 GitHub 레포 → Settings → Secrets and variables → Actions → New repository secret으로 이동해서, Name에 EC2_PEM_KEY를 넣고 Value에 방금 복사한 pem 키 전체를 붙여넣습니다.
처음에는 pem 키를 워크플로우 실행 중에 파일로 만들어서 ssh -i로 접속하는 방식을 썼습니다. 그런데 이 방식은 키가 워크플로우 실행 도중 실제 파일로 워크스페이스에 남는다는 게 마음에 걸렸는데, 코드래빗 코드리뷰가 appleboy/ssh-action을 쓰는 방식을 제안해줬습니다. 이 액션은 키를 파일로 남기지 않고 내부적으로 처리해서 더 안전합니다.
같은 리뷰에서 concurrency 설정도 받았습니다. main에 짧은 간격으로 여러 번 push되면, 같은 작업 디렉터리에서 git pull / build / restart가 동시에 실행되면서 Git 잠금 충돌이나 빌드 산출물이 섞이는 문제가 생길 수 있는데, group으로 묶어두면 배포 작업이 항상 한 번에 하나씩, 순서대로만 실행됩니다.
프로젝트를 진행하다 보니 네이버 지도 API 키, API 서버 주소처럼 코드에 그대로 박아둘 수 없는 값들이 생겼습니다.
이런 값은 .env 파일로 관리하는데, .env는 Git에 커밋하지 않는 게 원칙이라 배포 서버에는 따로 전달해줘야 합니다.
방법은 pem 키를 등록했던 것과 똑같습니다. GitHub Secrets에 값을 등록해두고, 워크플로우 실행 시점에 EC2 서버 안 셸에 환경 변수로 주입해주면 됩니다.
한 가지 주의할 점은 PM2입니다. PM2는 프로세스를 처음 시작할 때의 환경 변수를 기억해두고 있어서, 그냥 pm2 restart만 하면 새로 바뀐 환경 변수가 반영되지 않습니다. --update-env 옵션을 붙여줘야 최신 환경 변수를 다시 읽어들입니다.
이 내용들을 모두 반영한 최종 워크플로우는 이렇습니다.
name: Deploy to EC2
on:
push:
branches:
- main # main 브랜치에 push될 때만 실행
jobs:
deploy:
runs-on: ubuntu-latest
concurrency:
group: deploy-ec2 # 같은 그룹의 작업은 동시에 실행되지 않고 순서대로 실행
cancel-in-progress: false # 진행 중인 배포는 취소하지 않고 끝까지 기다림
permissions:
contents: read
steps:
- name: Deploy to EC2 via SSH
uses: appleboy/ssh-action@v1.2.0 # SSH 접속을 대신 처리해주는 액션
with:
host: ${{ 인스턴스 퍼블릭 IP }}
username: ubuntu
key: ${{ secrets.EC2_PEM_KEY }} # 앞서 등록한 Secret 값 참조
script: |
cd ~/프로젝트폴더
export NEXT_PUBLIC_NAVER_MAP_NCP_KEY_ID="${{ secrets.NEXT_PUBLIC_NAVER_MAP_NCP_KEY_ID }}" # Secret 값을 환경 변수로 주입
export NEXT_PUBLIC_API_BASE_URL="${{ secrets.NEXT_PUBLIC_API_BASE_URL }}"
git pull origin main # 최신 코드 받아오기
pnpm install --frozen-lockfile # lockfile 기준으로 의존성 설치
pnpm build # 프로덕션 빌드 (이때 위 환경 변수가 반영됨)
pm2 restart all --update-env # 바뀐 환경 변수까지 반영해서 재시작
워크플로우는 초록색 체크(성공)로 떴는데, 실제로는 변경사항이 반영되지 않았습니다. 로그를 열어보니 이런 줄이 있었습니다.
fatal: could not read Username for 'https://github.com': No such device or address
Already up to date
git pull이 인증에서 막혀 실패했는데, 그 뒤 스텝들이 에러를 무시하고 그대로 진행되면서 전체 워크플로우는 "성공"으로 잘못 표시된 것이었습니다.
원인은 EC2 서버에 레포를 HTTPS 방식으로 clone해뒀던 것이었습니다. HTTPS 방식은 pull/push할 때마다 매번 인증을 요구하는데, GitHub Actions는 사람이 앞에서 아이디·비밀번호를 입력해줄 수 없으니 그 자리에서 막혀버린 것입니다.
처음에는 Deploy Key로 해결하려 했으나, Deploy Key는 SSH 방식일 때만 작동하는 인증 수단이기 때문에 HTTPS 방식으로 clone한 경우에는 조직에서 Deploy Keys 기능을 켜더라도 소용이 없습니다. 그래서 조직 설정을 건드리는 대신, Personal Access Token을 EC2 서버에 저장해두는 방식으로 우회했습니다.
HTTPS로 clone하셨다면: Deploy Key 자체를 쓸 수 없으니, 아래처럼
credential.helper store로 PAT를 한 번 저장해두는 방법을 쓰시면 됩니다.SSH로 clone하셨다면: 조직 설정에서 Deploy Keys를 활성화(Member privileges → Deploy keys → Enabled)한 뒤, 레포의
Settings → Deploy keys에서 공개키를 등록하시면 별도 인증 설정 없이 바로git pull이 됩니다. 이쪽이 더 깔끔한 방법이라, 처음부터 SSH로 clone하실 계획이면 이 방식을 추천드립니다.
# 인증 정보를 서버에 저장하도록 설정
git config --global credential.helper store
# Username / PAT 최초 1회 입력 (이후 자동 저장)
git pull origin main
credential.helper store를 설정해두면, 최초 1회 인증한 정보가 서버 안에 저장되고, 이후 같은 서버에서 실행되는 모든 git pull은 이 저장된 인증정보를 자동으로 사용합니다.
GitHub Actions도 결국 이 EC2 서버 안에서 git pull을 실행하는 것이기 때문에, 한 번만 설정해두면 그 다음부터는 개발자가 다시 개입할 필요가 없습니다.
처음에 "왜 Vercel이 편하다고 하는지 제대로 들여다본 적이 없다"는 생각으로 시작했는데, EC2에 직접 배포해보고 나니 그 답이 꽤 명확해졌습니다.
지금까지 한 걸 되짚어보면 이렇습니다.
Vercel은 이 모든 과정을 git push 한 번 뒤로 숨겨놓은 것이었습니다.
서버 프로비저닝, 프로세스 관리, 리버스 프록시, SSL, CI/CD는 프론트엔드 개발자가 신경 쓰지 않아도 되는 영역이 아니라, 누군가는 반드시 처리해야 하는 일이었고, Vercel이 그걸 대신 떠맡아주고 있었던 것입니다.
직접 다 해보고 나니, "편하다"는 말 한마디에 얼마나 많은 작업이 압축되어 있었는지 체감하게 됐습니다.
그리고 왜 사이드 프로젝트나 빠르게 배포해야 하는 상황에서는 Vercel이 합리적인 선택인지도 이제는 이해가 됩니다.
다만 이번처럼 직접 인프라를 만져보는 경험도, 트래픽이 커지거나 커스터마이징이 필요한 순간에는 분명 도움이 될 거라 생각합니다.
EC2 인스턴스를 띄우고, Node·pnpm 환경을 갖추고, PM2와 Nginx로 상시 서빙 가능한 상태를 만들고, 도메인과 HTTPS를 붙이고, 마지막으로 GitHub Actions로 배포까지 자동화하면서 하나의 파이프라인을 완성했습니다.
appleboy/ssh-action)git push 한 번으로 끝나던 배포 뒤에 이렇게 많은 레이어가 있다는 걸 눈으로 직접 확인한 것만으로 충분한 값어치가 있었습니다. 메모리 부족으로 프로세스가 죽고, 포트가 막혀서 무한 로딩이 걸리고, 인증이 꼬여서 배포가 성공한 척 실패하는 걸 하나씩 겪으면서, 평소엔 보이지 않던 층위들을 눈앞에서 마주한 느낌이었습니다.
롤백이나 무중단 배포처럼 아직 손대지 않은 부분도 남아있습니다. 지금 당장 급한 건 아니지만, 이 구조를 계속 붙잡고 있다 보면 언젠가 필요해질 문제라는 것도 이번에 알게 됐습니다.
평소 Vercel을 아무렇지 않게 써오셨다면, 한 번쯤 EC2로 직접 배포해보시는 것도 추천드립니다.