처음엔 역시 리액트 프로젝트부터 시작했다.
npx create-react-app my-app 한 줄이면 기본 뼈대가 뚝딱 만들어진다. Vite나 Next.js로도 충분히 대체 가능하다.
프로젝트 폴더가 생성되면, npm install 로 필요한 라이브러리들을 쭉 깔아준다.
개발 서버는 npm run dev로 바로 확인 가능.
(개발 중엔 이 명령어를 거의 손에 달고 살게 된다.)

프로덕션 빌드는 npm run build로 진행.
이 과정에서 소스코드가 최적화된 정적 파일(HTML, JS, CSS 등)로 변환된다.
build 혹은 dist 폴더에 결과물이 생기는데, 이게 바로 실제 배포에 올라가는 파일들이다.
Docker 빌드할 때 불필요한 파일들(node_modules, .git 등)이 이미지에 포함되면 용량도 커지고 빌드도 느려진다.
.gitignore랑 비슷하게 .dockerignore 파일을 만들어서 이런 애들은 아예 빌드에서 제외시킨다.
컨테이너 이미지를 어떻게 만들지 설계도 그리듯 작성한다.
# 1단계: 빌드
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 2단계: Nginx로 서비스
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
1단계에서 Node.js로 빌드하고, 2단계에서 Nginx로 정적 파일을 서비스한다.
빌드 도구는 최종 이미지에 남지 않으니, 이미지는 가볍고 빠르다.
docker build -t my-react-app .
이렇게 하면 Dockerfile을 읽어서 이미지를 만든다.
실행은
docker run -d -p 8080:80 my-react-app
로컬 8080 포트로 접속하면 Nginx에서 리액트 앱이 서비스되는 걸 볼 수 있다.
쿠버네티스에서는 “이 앱을 어떻게, 몇 개나, 어떤 식으로 돌릴지”를 YAML 파일로 정의한다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: react-app
spec:
replicas: 3
selector:
matchLabels:
app: react-app
template:
metadata:
labels:
app: react-app
spec:
containers:
- name: react-container
image: my-react-app:latest
ports:
- containerPort: 80
replicas로 파드 개수를 지정하고, 이미지와 포트를 설정한다.
외부에서 내 앱에 접속하려면 Service 리소스를 만들어야 한다.
apiVersion: v1
kind: Service
metadata:
name: react-service
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 80
selector:
app: react-app
type이 LoadBalancer면 클라우드 환경에서 외부 IP가 할당된다.
(로컬 쿠버네티스에서는 NodePort나 Ingress로 대체 가능)
kubectl apply -f deploy.yaml
kubectl apply -f service.yaml
명령어 두 방이면 클러스터에 리소스가 생성된다.
kubectl get deployments
배포가 잘 됐는지 READY, AVAILABLE 등 상태를 꼭 확인하자.
코드가 바뀌면 자동으로 빌드/테스트/도커 이미지 생성/레지스트리 푸시/쿠버네티스 적용까지 쭉 이어주는 자동화 파이프라인을 만든다.
이 Jenkinsfile은 React 앱을 자동으로 빌드하고, Docker 이미지로 패키징해서 레지스트리에 푸시한 뒤, 쿠버네티스 배포 파일까지 자동으로 갱신해주는 풀-오토메이션 파이프라인이다.
| 단계(Stage) | 주요 역할 및 설명 |
|---|---|
| Clone Repository | 지정한 GitHub 저장소와 브랜치에서 소스 코드를 클론합니다. |
| Build React App | Node.js 컨테이너 환경에서 npm install 및 npm run build로 리액트 앱을 빌드합니다. |
| Docker Build & Push | 빌드된 소스코드로 Docker 이미지를 생성하고, 고유 태그로 이미지 레지스트리에 푸시합니다. |
| Update deploy.yaml and Push | deploy.yaml의 이미지 태그를 최신으로 갱신하고, GitHub 저장소에 자동 커밋/푸시합니다. |
실제 Jenkinsfile 예시는 아래와 같다.
pipeline {
agent any // 어떤 agent(노드)에서든 실행
environment {
// 파이프라인 전체에서 사용할 환경 변수 정의
GIT_URL = 'https://github.com/asroq1/my-42-app' // 소스 코드 저장소
GIT_BRANCH = 'main' // 기본 브랜치
GIT_ID = 'skala-github-id' // GitHub PAT credential ID
GIT_USER_NAME = 'asroq1' // GitHub 사용자 이름
GIT_USER_EMAIL = 'asroq7434@gmail.com'
IMAGE_REGISTRY = 'amdp-registry.skala-ai.com/skala25a' // 도커 이미지 레지스트리 주소
IMAGE_NAME = 'sk088-my-42-app' // 이미지 이름
IMAGE_TAG = '2.0.0' // 기본 태그
DOCKER_CREDENTIAL_ID = 'skala-image-registry-id' // 도커 레지스트리 인증 정보 ID
}
stages {
stage('Clone Repository') {
steps {
// GitHub에서 소스코드 체크아웃
git branch: "${GIT_BRANCH}",
url: "${GIT_URL}",
credentialsId: "${GIT_ID}"
}
}
stage('Build React App') {
agent {
docker {
image 'node:16-alpine' // Node.js가 설치된 컨테이너에서 빌드
reuseNode true // 워크스페이스 재사용
}
}
steps {
sh '''
npm install
chmod +x node_modules/.bin/react-scripts
npm run build
'''
// 의존성 설치 후, 리액트 앱 빌드
}
}
stage('Docker Build & Push') {
steps {
script {
// 빌드마다 유니크한 이미지 태그 생성 (버전-BUILD번호-해시)
def hashcode = sh(
script: "date +%s%N | sha256sum | cut -c1-12",
returnStdout: true
).trim()
def FINAL_IMAGE_TAG = "${IMAGE_TAG}-${BUILD_NUMBER}-${hashcode}"
echo "Final Image Tag: ${FINAL_IMAGE_TAG}"
// 도커 빌드 & 레지스트리 푸시 (amd64 아키텍처 지정)
docker.withRegistry("https://${IMAGE_REGISTRY}", "${DOCKER_CREDENTIAL_ID}") {
def appImage = docker.build("${IMAGE_REGISTRY}/${IMAGE_NAME}:${FINAL_IMAGE_TAG}", "--platform linux/amd64 .")
appImage.push()
}
// 이후 deploy.yaml에서 사용할 태그를 env에 저장
env.FINAL_IMAGE_TAG = FINAL_IMAGE_TAG
}
}
}
stage('Update deploy.yaml and Git Push') {
steps {
script {
// deploy.yaml의 image 라인만 새 태그로 치환
def newImageLine = " image: ${env.IMAGE_REGISTRY}/${env.IMAGE_NAME}:${env.FINAL_IMAGE_TAG}"
def gitRepoPath = env.GIT_URL.replaceFirst(/^https?:\/\//, '')
sh """
sed -i 's|^[[:space:]]*image:.*\$|${newImageLine}|g' ./argocd-k8s/deploy.yaml
cat ./argocd-k8s/deploy.yaml
"""
// 변경된 deploy.yaml을 커밋 준비
sh """
git config user.name "$GIT_USER_NAME"
git config user.email "$GIT_USER_EMAIL"
git add ./argocd-k8s/deploy.yaml || true
"""
// 깃허브 PAT로 푸시 (변경사항 있을 때만)
withCredentials([usernamePassword(credentialsId: "${env.GIT_ID}", usernameVariable: 'GIT_PUSH_USER', passwordVariable: 'GIT_PUSH_PASSWORD')]) {
sh """
if ! git diff --cached --quiet; then
git commit -m "[AUTO] Update deploy.yaml with image ${env.FINAL_IMAGE_TAG}"
git remote set-url origin https://${GIT_PUSH_USER}:${GIT_PUSH_PASSWORD}@${gitRepoPath}
git push origin ${env.GIT_BRANCH}
else
echo "No changes to commit."
fi
"""
}
}
}
}
}
}

ArgoCD는 내가 처음 써봤을 때 자동화의 끝판왕이구나 싶었던 도구다.
Git 저장소만 계속 바라본다.
실제 쿠버네티스 클러스터와 Git의 상태를 항상 비교한다.
수동 배포? 이제는 안녕
kubectl apply로 직접 배포했지만 이제는 Git에만 올리면 자동으로 배포된다.이 구조 덕분에, 코드만 고치고 푸시하면 배포까지 자동으로 쭉쭉 진행된다.
(실제로 써보면 이 맛에 GitOps 한다는 말이 절로 나온다.)
| 단계 | 주요 행위 | 상세 설명 |
|---|---|---|
| React 앱 구성 | 개발/빌드 | 소스코드 작성 및 정적 파일 생성 |
| Docker 컨테이너화 | .dockerignore, Dockerfile, 빌드 | 불필요 파일 제외, 이미지 설계, 빌드/실행 |
| Kubernetes 배포 | deploy/service.yaml, 배포 | 원하는 상태 정의, 네트워크 연결, 실제 클러스터에 배포 |
| CI/CD & GitOps | Jenkins/ArgoCD 자동화 | 코드 변경 → 자동 빌드/배포, Git 기반 배포 일관성 유지 |
| 문제 해결/최적화 | 권한, 환경, 헬스체크 등 | 각종 오류 해결 및 서비스 안정성, 효율성 강화 |
이번 프로젝트를 하면서 모던 앱 배포의 핵심 원리를 체험했다.
단순히 기술을 나열하는 게 아니라 실제로 써보고 겪은 시행착오와 개선점을 기록해두니 나중에 다시 봐도 큰 도움이 된다.
이 구조 덕분에 코드만 고치고 Git에 올리면 자동으로 빌드/배포까지 쭉 이어지는 환경이 완성됐다.
코드가 배포 되는 순간 개발자로서의 만족감은 언제나 짜릿하다.