git push 하나로 서버가 알아서 빌드하고 배포하게 만들었다
GitHub에 push하면 서버가 webhook을 받아 pull, 설치, 빌드, 복사까지 알아서 하게 만들었다. 서명 검증과 private 저장소 인증, 그리고 도메인을 옮긴 뒤 배포가 조용히 멈춘 이야기까지 담았다.
#인프라 #CICD #GitHub #Webhook
포트폴리오 사이트 만들고 제일 귀찮았던 건 배포였다. 코드를 좀 고치면 서버에 접속해서 git pull을 받고, npm install을 돌리고, 빌드하고, 나온 dist를 서빙 폴더로 복사한다. 이걸 매번 손으로 하다 보니 사소한 수정 하나 올리는 것도 일이 됐다. 오타 하나 고치려고 터미널을 여는 게 귀찮아서 그냥 두는 날도 있었다. 그래서 GitHub에 push만 하면 서버가 알아서 다 하도록 자동 배포를 붙였다. 이 글에선 Webhook으로 이 파이프라인을 어떻게 짰는지, 그리고 나중에 이게 어떻게 멈췄는지까지 정리해본다. 전체 흐름 동작은 단순하다. 로컬에서 GitHub로 push하면 GitHub가 서버의 /webhook/deploy로 요청을 보낸다. 서버는 그 요청을 받아서 git pull, npm install, 빌드, dist 복사를 순서대로 자동 실행하고, 성공이든 실패든 결과를 이메일로 보내준다. 즉 내가 하는 건 git push 하나뿐이고, 나머지는 서버가 알아서 한다. Webhook 받는 엔드포인트 GitHub는 push가 일어나면 설정해둔 URL로 POST를 보낸다. 서버엔 그걸 받을 엔드포인트만 하나 열어두면 된다. app.post('/webhook/deploy', (req, res) = { const signature = req.headers['x-hub-signature-256']; // webhook secret 검증 if (process.env.WEBHOOK_SECRET) { const hmac = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET); const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex'); if (signature !== digest) { return res.status(401).json({ error: 'Invalid signature' }); } } res.json({ message: '배포 시작' }); deploy(); // 실제 배포는 비동기로 }); 여기서 신경 쓴 건 서명 검증이다. 이 엔드포인트는 인터넷에 그냥 열려 있으니까, 아무나 호출하면 마음대로 배포를 트리거할 수 있다. 그래서 GitHub가 보내는 x-hub-signature-256 헤더를 WEBHOOK_SECRET으로 직접 계산한 값이랑 비교해서, 진짜 GitHub가 보낸 요청인지 확인한다. GitHub 쪽 Webhook 설정에서 Secret에 같은 값을 넣어두면 된다. 그리고 응답은 검증 끝나면 바로 보내고, 실제 배포는 뒤에서 따로 돌린다. 빌드는 수십 초씩 걸리는데 그동안 GitHub를 기다리게 하면 타임아웃이 나기 때문이다. GitHub는 응답을 10초 안에 못 받으면 실패로 기록한다. 실제 배포 로직 배포 함수는 그냥 내가 손으로 치던 명령어를 순서대로 실행하는 거다. execSync로 하나씩 돌린다. async function deploy() { process.chdir(PORTFOLIO_DIR); // 1. 최신 코드 받기 execSync('git pull'); // 2. 의존성 설치 execSync('npm install'); // 3. 빌드 execSync('npm run build'); // 4. 빌드 결과물을 서빙 디렉토리로 복사 execSync(`cp -r dist/. ${DIST_DIR}/`); await sendMail('배포 성공', output); } execSync를 쓴 건 순서가 중요해서다. pull이 끝나기 전에 install이 돌면 안 되고, 빌드가 끝나기 전에 복사하면 안 된다. 하나라도 실패하면 예외가 나서 다음 단계로 안 넘어가고, 그 예외가 아래 실패 메일로 이어진다. 막혔던 건 private 저장소 인증 처음에 git pull에서 막혔다. private 저장소라 서버에서 그냥 pull하면 인증을 요구한다. 터미널에서는 내가 비밀번호를 치면 되지만 자동으로 도는 프로세스는 그럴 수 없다. 그래서 GitHub Personal Access Token을 발급받아서, remote URL에 토큰을 끼워 넣는 방식으로 풀었다. const githubToken = process.env.GITHUB_TOKEN; if (githubToken) { const urlWithToken = remoteUrl.replace( /https:\/\/(.*@)?github\.com\//, `https://${githubToken}@github.com/` ); execSync(`git remote set-url origin "${urlWithToken}"`); } 토큰은 .env에만 넣고 코드엔 안 박았다. 이런 토큰이 코드에 같이 커밋되면 그 자체가 보안 사고라서, 인증 정보는 무조건 환경변수로 빼는 게 맞다. 토큰 권한도 저장소 읽기만 주면 충분하다. 이메일 알림이 생각보다 중요했다 자동 배포의 함정은 내가 배포 과정을 안 보게 된다는 거다. 손으로 할 때는 빌드 에러가 나면 터미널에 바로 보이는데, 자동으로 돌면 실패해도 모르고 넘어갈 수 있다. 그래서 배포 끝나면 성공이든 실패든 로그를 통째로 이메일로 보내게 했다. 이게 있으니까 push 했는데 사이트가 왜 안 바뀌지 싶을 때 메일함만 열어보면 어디서 깨졌는지 바로 보인다. 자동화는 결국 사람이 안 봐도 되게 만드는 건데, 그러려면 문제 생겼을 때 알려주는 장치가 꼭 같이 있어야 한다. 그리고 지금 이게 안 돌고 있다 이 글을 쓰고 한참 뒤에 애드센스 심사 때문에 루트 도메인을 포트폴리오 대신 블로그로 잠깐 돌려놨다. Apache 설정에서 ProxyPass 한 줄만 5015에서 5000으로 바꾼 거다. 그러고 나서 한동안 눈치를 못 챘는데, 어느 날 push를 했는데 사이트가 그대로였다. 원인은 단순했다. GitHub이 webhook을 보내는 주소가 루트 도메인이었는데, 그 도메인이 이제 블로그 서버로 간다. 블로그 서버엔 그런 라우트가 없으니 404다. 포트폴리오 서버의 app.js에는 /webhook/deploy가 그대로 살아 있는데, 요청이 거기까지 안 가는 거다. $ curl -X POST https://내도메인/webhook/deploy 404 $ pm2 logs | grep "Deployment completed" 2026-08-18 22:03 - 마지막으로 성공한 배포 자동배포가 조용히 멈춰 있었는데 아무 알림도 안 왔다. 실패 알림 메일은 배포가 시작돼야 나가는 거라, 아예 요청이 안 닿으면 보낼 것도 없었다. 자동화를 만들 때 돌다가 실패하는 경우만 생각했지 아예 호출이 안 되는 경우는 대비를 안 해뒀다. 이 글을 다시 손보는 2026년 9월 중순에 같은 curl을 다시 쳐봤는데 여전히 404이고, 루트 도메인의 ProxyPass도 아직 5000을 가리키고 있다. 그러니까 지금도 손으로 빌드하고 있다. webhook 주소를 도메인이 아니라 서버 쪽에 직접 두는 방향으로 옮기려고 한다. 그러면 도메인이 어디를 가리키든 배포는 영향을 안 받는다. 자동 배포를 붙였다면, 마지막 배포가 며칠 전인지 한 번씩 확인하는 습관도 같이 들여두길 추천한다.