안녕하세요, 프론트엔드 개발자 제이입니다.
이 글에서는 하나의 EC2 인스턴스와 AWS CodePipeline을 활용해 기능 브랜치별 Preview 환경을 구성하고 Next.js 앱을 배포하는 방법을 소개합니다.
Next.js 앱을 개발해 보셨다면 Vercel이나 Netlify 등에서 제공하는 Preview Deployment 기능을 접해보셨을 겁니다. 각 브랜치에서 발생한 변경사항을 실제와 유사한 환경에 자동 배포하고 생성된 URL을 통해 팀원들과 구현 결과를 공유하는 기능입니다.
이러한 Preview 환경을 활용하면 디자이너나 기획자 등 협업 중인 팀원들이 구현된 화면을 즉시 확인하고 피드백을 줄 수 있어 개발 속도와 품질을 동시에 높일 수 있습니다.
저희 팀도 초기에 AWS Amplify를 사용하며 유사한 경험을 했습니다. Amplify는 브랜치 패턴 기반의 자동 배포 기능을 지원합니다. 예를 들어 feature-login
브랜치에 push하면 https://feature-login.appid.amplifyapp.com
과 같은 URL이 자동 생성됩니다.
하지만 이후 비용 절감과 유연한 인프라 제어를 위해 EC2 기반으로 이전하면서 해당 기능을 더 이상 사용할 수 없게 됐습니다. 제품 개발 우선 순위에 밀리며 한동안 직접 구축하지 못했습니다.
그러나 시간이 지나면서 아래와 같은 문제들이 반복됐습니다.
이런 비효율은 팀 전체에 부담으로 작용했고 결국 Preview 환경 자동화 작업이 시급하다는 공감대가 형성되었습니다.
다양한 대안을 검토한 결과 기존 운영 환경에서 사용하는 CodePipeline 기반 파이프라인을 재사용하는 방식을 선택했습니다. 학습 비용이 적고 기존 시스템과의 차이가 적어 팀원들이 쉽게 적응할 수 있기 때문입니다.
기본 동작은 다음과 같습니다.
feature/login
브랜치에서 작업하고 GitHub에 push합니다.login.example.com
과 같은 도메인으로 접근할 수 있게 됩니다. nginx는 해당 브랜치에 해당하는 앱으로 요청을 프록시합니다.이 구조의 핵심은 CodePipeline의 트리거 설정과 빌드 단계에서의 동적 설정 처리입니다. 파이프라인 실행 시점에 브랜치명을 포함한 여러 값을 환경변수로 전달하여 동적으로 포트, 디렉토리, 설정파일 등을 생성할 수 있습니다.
feature/*
브랜치에서 push하면 자동으로 파이프라인을 실행하는 예시를 단계별로 살펴보겠습니다. 실습을 위해 AL 2023 EC2 인스턴스와 배포 파이프라인을 미리 설정해두었습니다. 먼저 파이프라인 트리거를 추가합니다.
그 다음 소스 단계에서 GitHub 레포지토리를 연결합니다.
모든 코드는 nextjs-ec2-deploy 레포지토리에 공개되어 있습니다.
📦nextjs-ec2-deploy
┣ 📂public
┣ 📂src
┣ 📜appspec.yml # CodeDeploy 설정
┣ 📜buildspec.yml # CodeBuild 설정
┣ 📜deploy.sh # 배포 스크립트입니다.
┣ 📜ecosystem.config.js # PM2 설정
┣ 📜nginx.conf # nginx 리버스 프록시 설정
┗ 기타 Next.js 관련 설정 파일들
다음 단계로 CodeBuild를 파이프라인에 추가합니다. CodePipeline의 소스 단계는 브랜치명을 포함한 여러 출력을 변수로 제공합니다. 이를 활용해 빌드 시 브랜치별로 환경을 동적으로 구성합니다.
이제 CodeBuild 빌드 사양 파일을 작성할 차례입니다. 루트 디렉토리에 buildspec.yml
파일을 생성하고 아래 내용을 참고해 작성합니다.
(buildspec.yml)
version: 0.2
phases:
install:
runtime-versions:
nodejs: 22
pre_build:
commands:
- echo "🔄 빌드 환경 설정 중"
- npm install
- echo "🔄 브랜치 이름 추출 중"
- echo "🔄 CodePipeline 소스 작업에서 전달받은 브랜치 이름 $SOURCE_BRANCH" # e.g. feature/login
- export SOURCE_BRANCH=$(echo $SOURCE_BRANCH | sed 's/feature\///')
- echo "🔄 feature/ 제거된 브랜치 이름 $SOURCE_BRANCH" # e.g. login
- echo "🔄 랜덤 포트 생성 중 (3000~4999)"
- export RANDOM_PORT=$((3000 + $RANDOM % 2000))
- echo "🔄 선택된 포트 $RANDOM_PORT"
- echo "🔄 문자열 치환 중"
- sed -i "s/__BRANCH_NAME__/$SOURCE_BRANCH/g" deploy.sh
- sed -i "s/__BRANCH_NAME__/$SOURCE_BRANCH/g; s/__PORT__/$RANDOM_PORT/g" ecosystem.config.js
- sed -i "s/__PORT__/$RANDOM_PORT/g" nginx.conf
- echo "🔄 작업 결과 확인"
- cat appspec.yml
- cat deploy.sh
- cat ecosystem.config.js
- cat nginx.conf
build:
commands:
- npm run build
artifacts:
files:
- "**/*"
빌드 단계에서 각 설정 파일 내 플레이스홀더(언더바로 감싼 형식)를 브랜치 이름으로 치환하는 방식을 사용했습니다. 이를 통해 nginx, PM2, Shell script 설정이 브랜치별로 적용됩니다.
이제 CodeDeploy를 파이프라인에 추가합니다. 인스턴스에 CodeDeploy 에이전트를 설치한 후 앱 사양 파일을 아래 내용을 참고해 작성해 주세요.
(appspec.yml)
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/temp
files_exists_behavior: OVERWRITE
hooks:
AfterInstall:
- location: deploy.sh
timeout: 60
디렉토리는 우선 /home/ec2-user/temp
에 복사되고, 이후 AfterInstall 단계에서 deploy.sh
를 통해 브랜치명에 해당하는 디렉토리로 이동됩니다. 이는 CodeDeploy의 클린업 동작을 회피하기 위한 조치입니다. CodeDeploy 에이전트는 배포 전에 동일한 배포 그룹 ID를 가진 이전 배포 파일을 삭제합니다.
(deploy.sh)
#!/bin/bash
export HOME=/home/ec2-user
export PM2_HOME=$HOME/.pm2
echo "🔄 __BRANCH_NAME__ 브랜치로 배포 시작"
TEMP_DIR="/home/ec2-user/temp"
TARGET_DIR="/home/ec2-user/__BRANCH_NAME__"
echo "🔄 임시 디렉터리 $TEMP_DIR"
echo "🔄 배포 디렉터리 $TARGET_DIR"
echo "🔄 기존 디렉터리 삭제 중 $TARGET_DIR"
rm -rf "$TARGET_DIR"
echo "🔄 배포 디렉터리 생성 중 $TARGET_DIR"
mkdir -p $TARGET_DIR
echo "🔄 임시 디렉터리 복사 중 $TEMP_DIR"
cp -r "$TEMP_DIR/"* "$TEMP_DIR"/.* "$TARGET_DIR/"
echo "🔄 애플리케이션 시작 중"
cd $TARGET_DIR
pm2 start ecosystem.config.js
sudo nginx -s reload
echo "🔄 배포 완료"
해당 스크립트를 실행하려면 인스턴스에 Node.js, nginx, PM2가 설치되어 있어야 합니다. 루트 권한으로 설치했는지, ec2-user 등 일반 사용자 권한으로 설치했는지에 따라 동작이 달라질 수 있으니 주의해 주세요.
PM2 설정까지 완료하면 이제 CodePipeline 실행 준비가 완료됩니다.
(ecosystem.config.js)
module.exports = {
apps: [
{
name: `__BRANCH_NAME__`,
script: "./node_modules/next/dist/bin/next",
args: "start",
error_file: "/dev/null",
env: {
PORT: `__PORT__`,
},
},
],
};
아래는 feature/login
, feature/payment
, feature/user-profile
브랜치로 파이프라인을 실행한 예시입니다. 각 브랜치 명에 따라 디렉토리가 생성되고 해당 디렉토리 내에서 Next.js 애플리케이션이 PM2를 통해 개별 포트에서 실행됩니다.
마지막으로 클라이언트와의 통신 설정만 남았습니다. 브랜치 이름과 동일한 서브도메인으로 접속하면 nginx가 해당 브랜치에서 배포된 Next.js 애플리케이션으로 요청을 프록시하도록 구성해야 합니다. 이를 도식화하면 다음과 같은 구조입니다.
이를 위해 EC2에 설치한 nginx 설정 파일의 http
블록 내부에 include
문을 추가해 주세요.
(/etc/nginx/nginx.conf)
http {
# 브랜치별 설정 파일을 포함합니다.
include /home/ec2-user/*/nginx.conf;
}
브랜치별 설정 파일은 각 프로젝트 디렉토리 내에 이미 포함되어 있으므로 nginx를 재시작하거나 리로드하면 자동으로 적용됩니다.
(/home/ec2-user/*/nginx.conf)
server {
listen 80;
server_name __BRANCH_NAME__.jungeun.store;
location / {
proxy_pass http://localhost:__PORT__;
}
}
서브도메인 연결이나 ELB 규칙 등 별도의 네트워크 설정이 필요한 경우 상황에 맞게 추가로 구성해 주세요. 예를 들어 이번 실습에서는 Route 53에서 서브도메인을 허용하는 A레코드를 생성했습니다. 아래는 실습의 최종 결과입니다.
이 글의 핵심은 단일 파이프라인으로 브랜치 기반 개발을 진행하는 컨셉입니다. 각자의 상황과 환경에 맞게 적절히 변형하여 활용하시면 좋을 것 같습니다. 예를 들어 모노레포 환경에서는 파일 변경 기반 트리거를 적용할 수 있고, EC2 대신 컨테이너 기반 환경에서도 동일한 방식을 적용할 수 있습니다.
기능 브랜치 자동 배포 시스템을 재구축한 결과 협업 흐름이 크게 개선되어 논의와 결정 속도가 빨라졌습니다. 반복되던 인프라 작업이 줄어 개발에 더 많은 시간을 투자할 수 있었고, 배포 테스트가 체계화되면서 버그 발생률도 감소했습니다. Savings Plan을 적용하지 않은 상황이었다면 연간 $2,160에 달하는 EC2 비용을 절감할 수 있었습니다.
학습 비용이 적고 기존 시스템과의 차이가 크지 않아 팀원들이 빠르게 적응할 수 있을 것으로 판단해 기존 시스템을 기반으로 작업했는데 결과적으로 2주로 예상했던 작업을 1주 만에 완료할 수 있었습니다. 팀원들에게 공유했을 때도 일관성이 있어 복잡하지 않으면서 기능은 확실하다는 긍정적인 피드백을 받았습니다.
저희 팀은 제가 PoC로 진행한 nextjs-ec2-deploy를 기반으로 기능을 확장해 실제 운영 환경에 적용하고 있습니다.
주요 확장 기능은 다음과 같습니다.
글이 길어지면서 일부 내용은 생략하거나 다루지 못한 점 양해 부탁드립니다. 이 글이 도움이 되셨길 바라며 궁금한점이나 어려움이 있으면 댓글로 남겨 주세요. 아는 범위 내에서 최대한 도움을 드리겠습니다. 감사합니다.
CodeBuild 환경 변수와 함께 BranchName 변수 사용
AWS CodePipeline를 활용한 브랜치 기반 개발 및 모노레포 구성하기