본문 바로가기
개발자/실무 개발 현장

내가 짠 코드가 운영 중 터졌을 때의 경험담

by 나무011 2026. 7. 20.
개발자 현직자 솔직 경험담

내가 짠 코드가
운영 중 터졌을 때의 경험담

새벽 2시 슬랙 알림 — 세상에서 가장 무서운 소리에 대하여

Frontend Developer 2026년 6월 약 14분 읽기

그 날 새벽 2시 17분

자고 있었다. 핸드폰이 울렸다. 슬랙 알림이었는데, 보통은 진동 무음으로 해놓는데 그날따라 소리를 켜두고 잤다. 눈을 뜨고 화면을 보니 CTO가 보낸 메시지였다.

"결제 페이지 안 되는 거 확인해줄 수 있어요?"

잠이 한 번에 깼다. 결제 페이지. 지난주에 내가 배포한 코드가 있었다. 주문 완료 후 리다이렉트 로직을 수정한 부분이었다. 노트북을 열면서 이미 위장이 조여드는 걸 느꼈다. Vercel 대시보드를 열었더니 에러 로그가 빽빽하게 쌓여 있었다.

내가 짠 코드가 운영 중 터졌을 때의 경험담
내가 짠 코드가 운영 중 터졌을 때의 경험담
 
 
 
Vercel Functions Log — 새벽 02:17

ERROR [02:09:14] TypeError: Cannot read properties of undefined (reading 'orderId')

ERROR [02:09:21] TypeError: Cannot read properties of undefined (reading 'orderId')

ERROR [02:09:38] TypeError: Cannot read properties of undefined (reading 'orderId')

at /api/payment/complete line 34

at processTicksAndRejections (node:internal/process/task_queues:95)

ERROR [02:11:02] TypeError: Cannot read properties of undefined (reading 'orderId')

WARN [02:11:02] Payment webhook received but order not found — orderId: undefined

ERROR [02:11:55] TypeError: Cannot read properties of undefined (reading 'orderId')

─────────────────────────────────────────────────

→ 동일 에러 반복 중 (02:09 ~ 02:17, 총 23건)

orderId가 undefined. 그 한 줄을 보는 순간 무슨 문제인지 바로 떠올랐다. 결제 완료 웹훅 핸들러에서 요청 바디를 파싱하는 방식을 바꿨는데, 특정 결제 수단에서 오는 응답 구조가 달랐다. 그걸 확인하지 않았다. 로컬에서는 카드 결제만 테스트했고, 카카오페이로 결제한 사람들의 요청이 전부 터지고 있었다.

23건. 23명의 사람이 결제를 완료했는데 시스템이 처리하지 못했다는 뜻이었다. 돈은 나갔는데 주문이 안 잡혔을 수도 있었다.

장애 대응 — 새벽 2시 17분부터 4시 40분까지

02:17 — 장애 인지
슬랙 알림 확인, 로그 파악 시작

에러 로그 확인. 카카오페이 웹훅 응답 구조가 내가 예상한 것과 달랐음을 즉시 파악. 원인 특정까지 약 8분 소요.

02:25 — 임시 조치 1
이전 버전으로 롤백 시도

Vercel 대시보드에서 이전 배포로 롤백. 그런데 이전 버전에도 다른 버그가 있어서 롤백이 완전한 해결책이 아님을 확인. 일단 에러 발생은 멈춤.

02:41 — 피해 범위 파악
Supabase에서 누락 주문 확인

결제는 됐는데 주문 레코드가 없는 케이스 쿼리 실행. 23건 중 실제 금전 피해가 발생한 건 17건. 나머지 6건은 결제 자체가 실패 처리돼 환불됨.

03:10 — 수동 복구 시작
누락 주문 17건 직접 처리

카카오페이 결제 내역과 대조해서 누락된 주문을 DB에 수동 삽입. CTO와 함께 작업. 각 주문 확인 후 사용자에게 카카오페이 주문 확인 문자 발송 예정 처리.

03:55 — 핫픽스 배포
웹훅 핸들러 수정 후 재배포

카카오페이·토스·카드 등 결제 수단별 응답 구조를 모두 처리하는 코드로 수정. 로컬에서 모든 결제 수단 케이스 확인 후 배포. 이후 에러 0건.

04:40 — 안정화 확인
30분간 로그 모니터링 후 종료

새로운 에러 없음 확인. 사용자 CS 대응 방침 CTO와 공유 후 임시 종료. 자려고 누웠는데 잠이 안 왔다.

23건 총 에러 발생
17건 누락 주문
68분 장애 지속 시간
0건 핫픽스 후 추가 에러

원인 코드 — 무엇이 문제였나

코드를 보면 명확하다. 지금 보면 왜 이걸 못 잡았나 싶지만, 당시에는 카드 결제만 생각하고 짰다.

❌ 문제가 된 코드 — /app/api/payment/complete/route.ts
// 수정 전 — 카카오페이 응답 구조를 고려하지 않음 export async function POST(req: Request) { const body = await req.json() // 카드 결제는 body.data.orderId로 오는데 // 카카오페이는 body.orderId로 바로 옴 → undefined const { orderId, amount } = body.data const order = await findOrder(orderId) // orderId가 undefined이므로 order도 null → 이후 전부 에러 ... }
✅ 핫픽스 코드 — 결제 수단별 응답 구조 분기 처리
// 수정 후 — 결제 수단별 응답 구조를 모두 처리 type PaymentWebhookBody = { provider: 'card' | 'kakaopay' | 'toss' data?: { orderId: string; amount: number } orderId?: string amount?: number } function extractOrderInfo(body: PaymentWebhookBody) { // 카드: body.data.orderId / 카카오페이·토스: body.orderId const orderId = body.data?.orderId ?? body.orderId const amount = body.data?.amount ?? body.amount if (!orderId || !amount) { throw new Error(`Invalid webhook payload: orderId=${orderId}, amount=${amount}`) } return { orderId, amount } } export async function POST(req: Request) { const body: PaymentWebhookBody = await req.json() const { orderId, amount } = extractOrderInfo(body) ... }
🔴 근본 원인 — 내가 간과한 두 가지

첫째, 결제 수단이 여러 개인데 하나만 테스트했다. 카드 결제만 로컬에서 확인하고 배포했다. 둘째, 외부 서비스의 응답 구조가 수단마다 다를 수 있다는 걸 문서로 확인하지 않고 짐작으로 짰다. API 문서 한 페이지만 더 읽었어도 막을 수 있었던 버그였다.

그 다음 날 아침 — 감정이 더 힘들었다

기술적인 문제는 새벽에 다 정리됐다. 더 힘든 건 다음 날 아침이었다.

😶
사후 감정
팀원들한테 어떻게 말하지

오전 팀 스탠드업에서 어제 일을 설명해야 했다. 내가 배포한 코드가 결제를 터뜨렸고, 17건의 주문이 누락됐고, 새벽에 수동으로 복구했다고. CTO는 이미 알고 있었다. 다른 팀원들은 아침에 출근해서 처음 듣는 거였다.

솔직히 말하는 게 제일 힘들었다. 잘못한 게 있으면 빨리 말하는 게 맞다는 걸 머리로는 알면서도, 입이 잘 안 떨어졌다. 그냥 "결제 쪽 버그가 있었어요, 정리했어요"로 얼버무리고 싶었다.

그렇게 하지 않았다. 원인을 구체적으로 설명했다. 내 실수였다는 것도. CTO는 "빠르게 잡아줘서 다행이었다, 앞으로 결제 관련 배포는 테스트 케이스 추가하자"고 했다. 나를 다그치지 않았다. 오히려 그게 더 부담스러웠다.

코드가 터지는 것보다, 그 코드가 내 것이라는 걸 팀 앞에서 인정하는 게 더 어려웠다. 그런데 그걸 제대로 하고 나서야 진짜 배운 것 같은 기분이 들었다.

💬
CS 대응
결제한 사용자 17명에게 연락이 갔다

고객 서비스 담당자가 결제 완료됐는데 주문 확인이 안 된다는 문의를 이미 5건 받은 상태였다. 누락된 17건 모두에게 "주문이 정상 처리됐습니다, 불편을 드려 죄송합니다"는 문자를 발송하고, 쿠폰을 추가로 발급했다.

사용자들은 대부분 이해해줬다. 그런데 그 5명의 문의가 눈에 밟혔다. 밤에 결제하고 주문 확인이 안 돼서 걱정했을 사람들. 내 코드 한 줄이 그 사람들한테 실제로 불편을 줬다는 게 추상적인 게 아니라 구체적으로 느껴진 건 처음이었다.

사후 처리 — 팀이 바꾼 것들

이 일이 있고 나서 팀 차원에서 몇 가지가 바뀌었다. 나 혼자 반성하는 것보다 구조가 바뀌는 게 재발 방지에 효과적이라는 걸 CTO가 잘 알고 있었다.

결제 관련 API 통합 테스트 추가

카드, 카카오페이, 토스 세 가지 결제 수단의 웹훅 응답을 mocking한 테스트 케이스를 작성했다. 이후 결제 관련 코드 변경 시 CI에서 자동 실행된다.

결제 웹훅 에러 알림 즉시 발송

웹훅 핸들러에서 에러가 발생하면 Slack #alert 채널에 즉시 알림이 가도록 설정했다. 이번엔 CTO가 사용자 CS를 통해 알게 됐는데, 앞으로는 에러 발생 즉시 팀이 인지할 수 있다.

외부 API 응답 구조 타입 명시

결제사별 웹훅 응답 타입을 TypeScript로 명시적으로 정의하고, unknown 타입에서 파싱하는 유틸 함수를 만들었다. 타입이 없으면 빌드에서 에러가 나도록 구성했다.

스테이징 환경에 실결제 테스트 추가 (논의 중)

로컬 mock만으로는 실제 결제사 응답과 차이가 날 수 있다. 스테이징에서 소액 실결제 테스트를 배포 전에 의무화하는 방안을 논의 중이다. 아직 확정은 안 됐다.

결제 관련 PR은 셀프 리뷰 체크리스트 추가

결제 관련 코드 변경 PR에는 "모든 결제 수단 테스트 완료" 체크박스가 자동으로 생성된다. 체크 안 하면 머지 불가.

이 경험에서 실제로 배운 것들

01
🔍
외부 API는 문서를 먼저 읽는다

결제사 API 문서에 수단별 응답 구조 차이가 명시돼 있었다. 읽지 않고 짐작으로 짠 게 문제였다. 외부 서비스를 쓸 때는 "아마 이럴 것이다"가 아니라 문서로 확인하는 게 기본이다.

02
🧪
테스트는 내가 아는 케이스가 아니라 모르는 케이스를 위해 쓴다

카드 결제 테스트는 해봤다. 그건 내가 이미 아는 케이스다. 테스트의 진짜 가치는 내가 미처 생각 못 한 케이스를 커버하는 것이었다. 다중 결제 수단이 있다는 걸 알면서도 하나만 테스트한 건 변명이 안 된다.

03
🚨
에러 모니터링은 배포 전에 설정한다

CTO가 사용자 CS를 통해 알게 됐다는 건 에러 감지가 8분이나 늦었다는 뜻이다. 에러 알림이 있었다면 첫 번째 에러 발생 시점에 인지했을 것이다. 모니터링은 있으면 좋은 게 아니라 있어야 하는 것이다.

04
🗣️
빠르게, 구체적으로 보고한다

에러를 인지한 즉시 CTO에게 "이 부분 문제 있고, 원인은 이것이고, 지금 이렇게 대응 중입니다"를 보내는 게 맞다. 고치고 나서 보고하는 건 늦다. 진행 상황을 공유하는 게 신뢰를 만든다.

05
📋
사후 회고를 글로 남긴다

이 일이 있고 나서 노션에 포스트모텀을 썼다. 타임라인, 원인, 영향 범위, 개선 사항. 팀원들이 볼 수 있게 공유했다. 같은 실수를 다른 사람이 반복하지 않도록 하는 게 가장 생산적인 반성이다.

06
💪
코드는 언젠가 터진다 — 그게 끝이 아니다

어떤 개발자도 버그 없는 코드만 쓰지 않는다. 중요한 건 터졌을 때 얼마나 빠르게 감지하고, 얼마나 침착하게 대응하고, 같은 일이 반복되지 않도록 구조를 바꾸느냐다.

2년이 지난 지금 — 그 경험이 남긴 것

그날 이후 결제 관련 코드를 짤 때 습관이 생겼다. 외부 API를 쓸 때 응답 타입을 먼저 정의하고, 모든 케이스를 문서에서 확인하고, 테스트 케이스를 먼저 쓴다. 그 새벽이 훈련이 됐다.

그리고 에러 로그를 볼 때 예전과 다른 감각이 생겼다. 숫자가 아니라 사람이 보인다. 에러 23건은 23번의 실패한 요청이 아니라, 23명이 뭔가를 하려다가 안 됐다는 뜻이다. 그게 추상적이지 않게 느껴지기 시작한 건 그 날 새벽 이후부터였다.

코드가 터지는 게 두렵지 않냐는 질문을 가끔 받는다. 두렵다. 지금도. 그런데 그 두려움이 배포 전에 한 번 더 확인하게 만들고, 테스트를 더 꼼꼼하게 쓰게 만들고, 모니터링 설정을 미리 하게 만든다. 두려움이 나쁜 게 아니라 그걸 어디에 쓰느냐가 중요하다는 걸, 새벽 2시 17분의 나는 아직 몰랐다.

✅ 지금의 나에게 그때 해주고 싶은 말

괜찮다. 터지면 고치면 된다. 중요한 건 빠르게 인정하고, 빠르게 고치고, 같은 일이 반복되지 않게 구조를 바꾸는 것이다. 그리고 팀한테 솔직하게 말하는 게 결국 가장 빠른 길이다. 숨기려는 에너지를 고치는 데 쓰는 게 훨씬 낫다.


이 글을 쓰면서 그 날 새벽 로그 화면이 다시 눈앞에 떠올랐다. 빨간 글씨로 가득한 터미널, 손이 떨리던 기억, CTO한테 문자를 쓰던 새벽 2시. 그게 지금의 내 코드 리뷰 습관과 테스트 문화를 만들었다고 생각하면, 비싼 수업이었지만 나쁘지 않은 수업이었다.

프로덕션에서 코드가 터진 적 없는 개발자는 아직 충분히 많이 배포하지 않은 것이다. 언젠가 그 날이 오면 — 침착하게, 빠르게, 그리고 솔직하게. 그게 전부다.

💬 프로덕션에서 코드가 터진 경험이 있으신가요?

누구나 겪는 일이지만 아무도 먼저 꺼내지 않는 이야기. 어떤 버그였는지, 어떻게 대응했는지, 그 경험에서 뭘 배웠는지 댓글로 나눠주세요. 비슷한 경험을 가진 분들끼리 서로에게 위로가 되었으면 합니다.


소개 및 문의 · 개인정보처리방침 · 면책조항

© 2026 나무핀