AI와 함께 코딩할 때 먼저 배운 것: 만든 것과 만들려던 것을 구분하기
AI와 함께 코딩하면 계획, 프롬프트, 문서, 실제 코드가 쉽게 섞입니다. 이번 TASK-013에서는 공개 상태 페이지를 만들려 했지만, 실제로 ship된 것은 실시간 대시보드, 알림 테스트 상태 추적, 디자인 정합 작업이었습니다. 이번 글에서는 TASK 이름보다 머지된 코드를 기준으로 사실을 검증한 과정과, Realtime 아키텍처·비동기 상태 모델·공통 계약 관리 등 AI와 함께 개발할 때 놓치기 쉬운 기준들을 정리합니다. Build in Public 관점에서 ‘만들려던 것’과 ‘실제로 만든 것’을 구분하는 방법을 공유합니다.
TASK-013을 정리하면서 가장 먼저 확인한 것은 코드가 아니라 사실 관계였습니다.
이번 TASK의 이름은 “공개 상태 페이지 + 디자인 정합”이었습니다. 로드맵에도 /status/[slug] 공개 상태 페이지가 들어 있었고, TASK 문서에도 공개 상태 API, 공개/비공개 토글, 90일 uptime bar 같은 항목이 있었습니다.
그런데 머지된 코드를 기준으로 다시 확인해보니, 실제로는 /status/[slug] 페이지와 공개 상태 API가 아직 들어가 있지 않았습니다. DB 스키마에는 projects.slug, projects.is_public_status가 준비되어 있었지만, 라우트와 API는 없었습니다.
여기서 잠깐 멈췄습니다.
AI와 함께 코딩하다 보면 “하려던 것”, “프롬프트에 쓴 것”, “문서에 적은 것”, “실제로 머지된 것”이 쉽게 섞입니다. 특히 빠르게 작업할수록 더 그렇습니다. 이번 글은 TASK-013에서 구현한 기능 자체보다, AI와 함께 개발할 때 놓치기 쉬운 기준을 중심으로 정리했습니다.
이름이 사실을 대신하면 안 됩니다
TASK-013의 큰 흐름은 분명했습니다.
공개 상태 페이지를 향한 기반을 만들고, 사용자가 보는 표면을 정리하고, 실시간 대시보드와 알림 UX를 다듬는 작업이었습니다. 하지만 블로그 제목을 그대로 “공개 상태 페이지를 만들었습니다”라고 쓰면 사실과 맞지 않았습니다.
실제로 머지된 범위는 다음에 가까웠습니다.
|
구분 |
계획 |
이번 ship 결과 |
블로그에서의 표현 |
|---|---|---|---|
|
공개 |
포함 |
아직 없음 |
후속 슬라이스 |
|
공개 상태 API |
포함 |
아직 없음 |
미구현 명시 |
|
DB 스키마 |
필요 |
준비됨 |
기반 준비 |
|
Realtime dashboard |
포함 |
구현됨 |
이번 핵심 |
|
알림 테스트 UX |
포함 |
구현됨 |
이번 핵심 |
|
DESIGN.md 정합 |
포함 |
구현됨 |
이번 핵심 |
이 표를 만들고 나서 글의 방향이 바뀌었습니다.
처음에는 “공개 상태 페이지 작업기”처럼 쓸 수 있다고 생각했습니다. 하지만 실제 코드를 기준으로 보면 더 중요한 이야기는 따로 있었습니다. 작업의 이름보다 머지된 코드가 더 강한 사실이라는 점이었습니다.
TASK 이름은 의도이고, 머지된 코드는 결과입니다.
이 둘을 구분하지 않으면 회고도, 문서도, 다음 작업도 쉽게 흔들립니다.
이번에 실제로 ship한 것
TASK-013에서 실제로 들어간 핵심은 크게 세 가지였습니다.
첫째, 실시간 대시보드였습니다. 기존처럼 일정 주기로 데이터를 다시 불러오거나 router.refresh()에 기대는 방식이 아니라, Worker에서 이벤트를 발행하고 API가 socket.io로 브라우저에 전달한 뒤 TanStack Query를 invalidate하는 구조로 바꿨습니다.
둘째, 알림 테스트의 비동기 처리였습니다. 테스트 알림을 보냈을 때 HTTP 응답 하나로 “성공”을 단정하지 않고, testRequestId를 발급한 뒤 Redis에 pending, sent, failed, suppressed 상태를 기록하는 방식으로 정리했습니다.
셋째, 디자인 정합이었습니다. DESIGN.md의 motion, overlay, 채널 폼, 알림 이력, 모니터 상세 UI를 실제 화면과 맞춰갔습니다. 여기서도 중요한 점은 “예쁘게 다듬었다”가 아니라, UI의 규칙을 문서와 코드 사이에서 맞췄다는 점이었습니다.
작업 흐름을 단순화하면 이렇습니다.
TASK-013
├─ 실제 구현
│ ├─ realtime dashboard
│ ├─ notification test status
│ ├─ notification history UX
│ ├─ dashboard / monitor UI polish
│ └─ DESIGN.md 정합
├─ 준비만 된 것
│ └─ public status page용 DB schema
└─ 후속으로 남은 것
├─ /status/[slug] page
├─ public status API
└─ public/private toggle UI저는 이 구조를 보면서 한 가지 기준을 다시 잡았습니다.
AI가 어디까지 만들었는지보다, 사람이 어디까지 확인했는지가 더 중요했습니다.
AI는 빠르게 많은 파일을 만들 수 있습니다. 이번 PR도 181개 파일이 바뀌었고, 9,576줄이 추가됐습니다. 숫자만 보면 큰 기능이 모두 끝난 것처럼 보입니다. 하지만 파일 수와 코드 줄 수는 완료의 증거가 아닙니다. 완료의 증거는 라우트, API, 테스트, 문서, 실제 동작이 서로 맞는지 확인한 기록에 가깝습니다.
polling을 지우는 순간 기준이 올라갑니다
이번 ship에서 가장 기술적으로 의미 있었던 부분은 realtime 구조였습니다.
대시보드에서 상태가 바뀌었을 때 매번 새로고침하거나, 일정 시간마다 다시 요청하는 방식은 구현이 쉽습니다. 빠르게 결과를 만들다 보면 이 방식이 특히 유혹적입니다. “일단 되게 만들기”에 좋기 때문입니다.
하지만 모니터링 서비스에서는 조금 다릅니다.
상태 변화는 사용자가 바로 알아야 하고, 동시에 불필요한 요청은 줄여야 합니다. 그래서 이번에는 polling fallback을 제거하고, 이벤트 기반으로 화면을 갱신하는 방향을 선택했습니다.

구조는 단순하게 보면 이렇습니다.
[Worker probe/heartbeat]
↓
Redis PUBLISH
↓
[API RealtimeGateway]
↓
socket.io room: org:{orgId}
↓
[Browser RealtimeProvider]
↓
TanStack Query invalidate핵심 계약은 shared package에 두었습니다.
export const REALTIME_MONITOR_CHANNEL = 'vibepulse:monitor-events';
export const MONITOR_EVENT = 'monitor.changed';
export function orgRoom(orgId: string): string {
return `org:${orgId}`;
}
export const monitorChangedEventSchema = z.object({
monitorId: z.string().uuid(),
orgId: z.string().uuid(),
reason: z.enum(['status.changed', 'incident.opened', 'incident.resolved']),
status: monitorStatusSchema.nullable(),
at: z.string(),
});이 코드는 길지 않지만 중요한 기준을 담고 있습니다.
이벤트 이름, Redis 채널, 조직별 room, payload schema가 한곳에 모여 있습니다. AI와 함께 작업할 때 이런 계약을 흩어놓으면, 다음 작업에서 비슷하지만 다른 이름이 새로 생길 가능성이 커집니다.
예를 들어 어떤 파일에서는 monitor.changed, 다른 파일에서는 monitor.status.changed, 또 다른 파일에서는 monitorChange를 쓰기 시작하면 디버깅이 어려워집니다. 동작하지 않는 이유가 socket 문제인지, Redis 문제인지, 이벤트 이름 문제인지 바로 보이지 않습니다.
AI와 함께 코딩할수록 “공통 계약 파일”을 먼저 만들어두는 습관이 중요했습니다.
HTTP 응답 하나로 성공을 믿지 않기
알림 테스트도 비슷했습니다.
처음에는 테스트 버튼을 누르고 API가 응답하면 성공처럼 보이게 만들 수 있습니다. 하지만 실제로 알림은 큐에 들어가고, Worker가 처리하고, 외부 채널로 전송되고, 실패하거나 suppressed 될 수도 있습니다.
즉, “요청을 받았다”와 “알림이 실제로 처리됐다”는 다른 사건입니다.
이번에는 testRequestId를 발급하고 Redis에 상태를 저장하는 방식으로 정리했습니다.
export const channelTestDeliveryStatusSchema = z.enum([
'pending',
'sent',
'failed',
'suppressed',
]);
export function buildChannelTestStatusRedisKey(testRequestId: string): string {
return `channel-test:${testRequestId}`;
}흐름은 이렇게 잡았습니다.
[Web] test send
↓
testRequestId 발급
↓
[API] queue enqueue + Redis pending
↓
[Worker] send
↓
Redis sent | failed | suppressed
↓
[Web] status polling 또는 realtime event여기서 배운 점은 분명했습니다.
비동기 작업을 동기 응답처럼 포장하면 UX가 빨리 무너집니다.
처음에는 화면이 단순해 보일 수 있지만, 실패 케이스가 생기는 순간 사용자는 “방금 보낸 테스트가 어떻게 됐는지” 알 수 없습니다.
AI와 함께 작업할 때도 이 지점은 쉽게 지나칠 수 있습니다. “버튼을 누르면 토스트가 뜬다”까지는 빠르게 만들 수 있습니다. 하지만 운영 도구에서는 그다음 질문이 더 중요했습니다.
- 큐에 들어갔는가
- 실제 Worker가 처리했는가
- 전송됐는가
- 실패했는가
- 요금제나 설정 때문에 suppressed 됐는가
- 사용자가 그 상태를 화면에서 이해할 수 있는가
이 질문을 건너뛰면 기능은 있어 보이지만, 운영할 수 있는 기능은 되지 않습니다.
AI에게 맡길 것과 사람이 확인할 것
이번 TASK는 VibeOps 흐름으로 진행했습니다. develop을 기준 브랜치로 잡고, TASK 브랜치에서 작업한 뒤 ship, merge, follow-up reship을 두 번 거쳤습니다.
PR 흐름은 다음과 같았습니다.
|
PR |
내용 |
결과 |
|---|---|---|
|
#27 |
realtime, 알림 UX, 디자인 정합 메인 ship |
merged |
|
#28 |
TASK-013 follow-up notes |
merged |
|
#33 |
follow-up task record 갱신 |
merged |
이 과정에서 좋았던 점은 작업 단위가 기록으로 남았다는 것입니다. 반대로 아쉬웠던 점도 있었습니다. follow-up reship 이후 재실행된 테스트 결과가 TASK 문서에 남아 있지 않았습니다.
테스트 기록은 이렇게 정리되어 있었습니다.
|
영역 |
결과 |
메모 |
|---|---|---|
|
shared test |
통과 |
15 files / 94 tests |
|
api typecheck |
통과 |
|
|
worker typecheck |
통과 |
|
|
web typecheck |
실패 |
기존 vitest 타입 해석 문제 |
|
follow-up reship 이후 테스트 |
기록 없음 |
문서상 공백 |
여기서 꼭 봐야 하는 지점이 있었습니다.
AI는 작업을 이어갈 수 있지만, 검증의 공백까지 알아서 책임지지는 않습니다.
특히 reship을 하면 “뭔가 다시 고쳤다”는 느낌이 강해집니다. 하지만 그 뒤에 어떤 테스트를 다시 돌렸는지 기록하지 않으면, 나중에 돌아왔을 때 신뢰할 기준이 없습니다.
저라면 다음부터 TASK를 닫기 전에 이 체크리스트를 더 강하게 보겠습니다.
- TASK 이름과 실제 머지 범위가 일치하는가
- 계획에 있었지만 빠진 항목을 명시했는가
- DB 스키마만 있는지, 실제 API와 UI도 있는지 확인했는가
- follow-up reship 후 테스트 결과를 다시 남겼는가
- 실패한 테스트를 “기존 이슈”라고만 쓰지 않고 파일과 원인을 적었는가
- 문서, 코드, PR 설명이 같은 이야기를 하고 있는가
이런 체크리스트는 느려 보입니다. 하지만 실제로는 다음 작업을 빠르게 만듭니다. AI와 다시 작업할 때도 “어디까지 했는지”를 설명하는 시간이 줄어듭니다.
DESIGN.md는 예쁜 문서가 아니라 안전장치입니다
이번 TASK에서 DESIGN.md도 많이 갱신했습니다.
처음에는 디자인 문서를 화면을 예쁘게 맞추기 위한 보조 자료처럼 보기 쉽습니다. 하지만 작업을 하다 보니 DESIGN.md는 UI의 SSOT에 가까웠습니다. 특히 motion, overlay, form dialog, notification detail sheet 같은 요소는 한 번 기준이 흔들리면 화면마다 다른 느낌이 나기 쉽습니다.
AI와 함께 개발할 때는 이 문제가 더 자주 생깁니다.
AI가 컴포넌트를 만들 때마다 “그럴듯한 UI”를 새로 제안할 수 있기 때문입니다. 처음에는 좋아 보이지만, 여러 화면을 지나고 나면 제품 전체가 조금씩 다른 규칙을 가진 화면들의 모음이 됩니다.
이번에 정리한 방향은 단순했습니다.
|
기준 |
위험한 방식 |
이번에 잡은 방향 |
|---|---|---|
|
overlay |
화면마다 다른 모달 |
DESIGN.md 기준 정렬 |
|
motion |
컴포넌트별 임의 적용 |
§4 규칙 정합 |
|
notification UX |
목록만 제공 |
skeleton + detail sheet |
|
channel form |
기능 중심 입력창 |
설명, 브랜드 아이콘, plan gating |
|
dashboard |
데이터 나열 |
live 상태와 변화 중심 |
디자인 정합은 polish가 아니라 제품의 신뢰 문제였습니다.
모니터링 서비스에서 사용자는 화면의 작은 신호를 보고 상태를 판단합니다. 연결이 끊겼는지, 알림이 전송됐는지, 모니터가 down인지, incident가 진행 중인지 빠르게 이해해야 합니다. 이때 UI 규칙이 흔들리면 기능이 있어도 불안하게 느껴집니다.
이번 TASK에서 남긴 가장 중요한 미완성
이 글에서 가장 분명히 남기고 싶은 부분은 이것입니다.
공개 /status/[slug]는 아직 구현되지 않았습니다.
TASK 이름과 로드맵에는 있었지만, 이번 merge 코드 기준으로는 해당 페이지도 API도 없습니다.
대신 이번 ship은 다음 단계로 가기 위한 기반을 정리했습니다.
- 실시간 이벤트 파이프라인
- TanStack Query 기반 갱신 구조
- 알림 테스트 상태 추적
- 알림 이력 UX
- 채널 UX polish
- DESIGN.md 정합
- 공개 상태 페이지를 위한 DB 필드 준비
이렇게 쓰는 것이 조금 덜 멋있어 보일 수 있습니다. “공개 상태 페이지를 완성했습니다”라고 말하는 편이 더 강한 제목이 됩니다.
하지만 Build in Public에서는 멋진 제목보다 정확한 경계가 더 중요하다고 느꼈습니다. 어디까지 만들었고, 어디서 멈췄고, 다음에 무엇을 해야 하는지 남겨야 다음 빌드가 이어집니다.

AI와 함께 코딩할 때 놓치기 쉬운 기준
이번 TASK를 정리하면서, AI와 함께 코딩할 때 쉽게 놓치지만 꼭 남겨야 할 기준이 몇 가지 보였습니다.
첫째, 프롬프트에 쓴 내용과 실제 머지된 코드를 구분해야 합니다.
AI가 “구현했다”고 말해도 파일이 없으면 없는 것입니다. 라우트가 없고 API가 없으면 아직 기능은 없는 것입니다.
둘째, 비동기 작업은 상태 모델을 먼저 잡아야 합니다.
알림, 결제, 배포, 모니터링처럼 시간이 걸리는 작업은 HTTP 응답 하나로 성공을 단정하면 안 됩니다. pending, sent, failed, suppressed처럼 상태를 분리해야 UX도 운영도 편해집니다.
셋째, 실시간 기능에서 polling은 쉬운 출발점이지만 최종 답은 아닐 수 있습니다.
특히 모니터링 대시보드처럼 변화가 중요한 화면에서는 이벤트 기반 구조를 고민해야 합니다. Redis pub/sub, socket.io, query invalidation 같은 구조가 처음에는 복잡해 보여도 나중의 혼란을 줄여줍니다.
넷째, 공통 계약을 shared에 두는 습관이 필요합니다.
이벤트 이름, Redis key, room name, schema가 흩어지면 AI가 비슷한 이름을 계속 만들어낼 수 있습니다. 작은 문자열 하나가 디버깅 시간을 크게 늘릴 수 있습니다.
다섯째, 문서도 코드처럼 검증 대상입니다.
TASK 문서에 적힌 목표와 실제 결과가 다르면, 문서를 고쳐야 합니다. 문서가 낡으면 AI도 낡은 문서를 기준으로 다음 코드를 만들 수 있습니다.
결국 이번 TASK에서 남은 건 한 문장이었습니다.
AI와 함께 코딩하는 일은 빨리 만드는 일이기도 하지만, 더 정확히 말하면 빨리 만들어진 결과를 사람이 다시 확인하고 경계 짓는 일이었습니다.
● goodtek은 vibePulse를 만들면서 이런 경계를 계속 기록하고 있습니다. 완성된 기능만 보여주기보다, 중간에 헷갈렸던 지점과 다시 기준을 세운 순간도 함께 남기려고 합니다.
비슷하게 AI와 SaaS를 만들고 있다면, 이 기록이 “나도 여기서 한번 확인해봐야겠다”는 작은 신호가 되면 좋겠습니다.