관리자 페이지에서 '카드뉴스 생성' 버튼을 누르면 다음 오류가 떴습니다.
기존 카드뉴스 초안 조회 실패: JSON object requested, multiple (or no) rows returned
에러 메시지만 보면 원인이 모호합니다. "row가 없거나 여러 개"라는 뜻인데, 어느 쪽인지 메시지만으로는 알 수 없었습니다. 이 시점에서 바로 코드를 고치기 시작했다면, 잘못된 가정 위에 수정을 쌓았을 가능성이 큽니다.
중복 데이터를 정리하고, 두 환경 모두에 post_id UNIQUE 제약을 맞춰 걸었습니다. 조회 후 분기하던 로직을 Supabase의 upsert(onConflict: 'post_id') 한 번의 호출로 리팩터링했습니다. 코드가 77줄 줄고 24줄만 추가되어, 로직이 더 단순해졌습니다. 신규 생성과 재생성 두 케이스 모두 dev에서 검증 후 prod에 배포했고, 배포 후에도 동일하게 재확인했습니다.
카드뉴스 생성 버튼을 눌렀는데, 이런 오류가 떴습니다.
기존 카드뉴스 초안 조회 실패: JSON object requested, multiple (or no) rows returned
에러 메시지만 보면 "row가 없거나, 여러 개"라는 뜻인데, 어느 쪽인지는 알 수 없었습니다. 이럴 때 가장 위험한 건 메시지를 대충 해석해서 바로 코드를 고치는 것이라고 생각합니다. 잘못된 추측 위에 수정을 쌓으면, 문제는 사라진 것처럼 보이다가 나중에 더 복잡한 형태로 다시 나타나기 때문입니다.
그래서 코드를 만지기 전에 Supabase Table Editor를 열어 실제 데이터부터 확인했습니다. 확인해보니 원인은 명확했습니다. 같은 게시글 하나에 카드뉴스 초안이 5건이나 중복 저장되어 있었습니다. 조회 로직은 .maybeSingle()을 쓰고 있었는데, 이 함수는 "결과가 0건 또는 1건일 것"이라는 전제 위에서만 정상 동작합니다. 실제로는 재생성 버튼을 여러 번 누르며 중복이 계속 쌓여온 상태였습니다.
여기서 판단이 하나 더 필요했습니다. 5건 중 무엇을 남길 것인가 하는 문제였습니다. 단순히 "가장 오래된 것" 또는 "가장 최근 것"으로 기계적으로 정하지 않고, 각 초안의 생성 시각과 실제 슬라이드 문구를 하나씩 대조해봤습니다. 다행히 내용이 점점 다듬어지는 흐름이 보였고, 별도로 보존해야 할 완성본은 없다는 걸 확인한 뒤에야 삭제를 진행했습니다. 삭제는 되돌릴 수 없는 작업이라, 이 판단만큼은 자동화에 맡기지 않았습니다.
작업을 진행하며 또 하나 발견한 게 있습니다. dev와 prod, 두 운영 환경이 사실은 서로 다른 상태였다는 점입니다. dev DB에는 이미 post_id에 대한 UNIQUE 제약이 걸려 있었는데, prod에는 없었습니다. 같은 스키마라고 믿고 있었던 두 환경이 실제로는 어긋나 있었던 겁니다. 이런 차이는 겉으로 잘 드러나지 않다가, 이번처럼 데이터가 쌓이고 나서야 문제로 드러납니다.
원인이 명확해진 뒤에는 수정 방향도 명확해졌습니다. 중복 데이터를 정리하고, 두 환경 모두에 UNIQUE 제약을 맞춰 걸었습니다. 그리고 "먼저 조회해서 있으면 업데이트, 없으면 삽입"하던 분기 로직을, Supabase의 upsert(onConflict: 'post_id') 호출 한 번으로 리팩터링했습니다. 결과적으로 코드는 77줄이 줄고 24줄만 늘어, 로직 자체가 더 단순해졌습니다.
수정하는 과정에서 AI(Antigravity)가 제시한 설명 하나도 그대로 넘기지 않았습니다. updated_at 필드를 upsert 페이로드에서 빼도 되는지 물었을 때, "일반적으로 DB 트리거가 자동으로 갱신해줄 것"이라는 답이 돌아왔습니다. 그럴듯해 보이지만 확인된 사실은 아니었습니다. 마이그레이션 파일을 직접 검색해보니 이 프로젝트에는 트리거가 전혀 없었고, 그 자리에서 계획을 수정해 updated_at을 코드에서 직접 채워 넣는 방향으로 바꿨습니다.
신규 생성과 재생성, 두 가지 시나리오를 모두 dev 환경에서 검증한 뒤 prod에 배포했고, 배포가 끝난 뒤에도 실제 사이트에서 다시 한번 같은 테스트를 반복했습니다.
이번 디버깅에서 남은 원칙은 세 가지입니다.
첫째, 에러 메시지가 아니라 데이터부터 봅니다. 모호한 메시지를 추측으로 메우지 않고, 실제 테이블을 열어 정답을 확인하는 편이 결국 더 빠릅니다.
둘째, AI의 설명도 검증 대상입니다. 그럴듯한 설명이라도 사실 확인 없이 그대로 받아들이지 않고, 근거가 있는지 직접 찾아봅니다.
셋째, 되돌릴 수 없는 작업은 사람이 마지막으로 확인합니다. 데이터 삭제나 스키마 변경처럼 되돌리기 어려운 단계는 AI에게 맡기지 않고, 실행 결과를 직접 눈으로 확인한 뒤에만 다음 단계로 넘어갑니다.
이 세 가지는 이번 버그 하나에 국한된 이야기가 아니라, 다른 자동화 기능을 디버깅할 때도 그대로 적용할 수 있는 절차라고 생각합니다.
코드를 고치기 전에 Supabase Table Editor로 실제 데이터부터 확인했습니다. 확인 결과, 동일한 게시글(post_id) 하나에 카드뉴스 초안이 5건 중복 저장되어 있었습니다. 조회 로직은 .maybeSingle()을 사용해 "정확히 0건 또는 1건"만 있을 거라 가정하고 있었는데, 실제로는 여러 번 재생성을 시도하며 중복이 쌓인 상태였습니다.
여기서 중요한 판단이 하나 더 필요했습니다. "이 5건 중 무엇을 남길 것인가?"입니다. 단순히 오래된 순으로 지우는 대신, 각 초안의 생성 시각과 실제 문구 내용을 직접 대조해 가장 정제된 최신 버전만 남기기로 결정했습니다. 삭제는 되돌릴 수 없는 작업이라, 자동화된 판단에 맡기지 않고 사람이 직접 내용을 확인한 뒤 진행했습니다.
또한 dev와 prod, 두 개의 운영 환경이 서로 다른 상태였다는 사실도 발견했습니다. dev DB에는 이미 post_id에 대한 UNIQUE 제약이 걸려 있었지만, prod에는 없었습니다. 같은 스키마라고 생각했던 두 환경이 실제로는 어긋나 있었던 것입니다.
이번 작업에서 지킨 원칙은 세 가지로 정리됩니다.
에러 메시지가 아니라 데이터부터 본다. "row가 없거나 여러 개"라는 모호한 메시지를 코드 추측으로 메우지 않고, 실제 테이블을 열어 정답을 확인했습니다. AI의 설명도 검증 대상이다. "일반적으로 트리거가 있을 것"이라는 AI(Antigravity)의 추정을 그대로 받아들이지 않고, 마이그레이션 파일을 직접 검색해 사실 여부를 확인했습니다. 트리거가 없다는 게 확인되자 계획을 그 자리에서 수정했습니다. 되돌릴 수 없는 작업은 사람이 마지막으로 확인한다. 데이터 삭제, 스키마 변경처럼 되돌리기 어려운 단계는 AI에게 맡기지 않고, 직접 SQL을 실행하고 결과를 눈으로 확인한 뒤에만 다음 단계로 넘어갔습니다.
이 원칙은 다른 자동화 기능을 디버깅할 때도 동일하게 적용할 수 있는 절차입니다.
관련 프로젝트
프로젝트 개요 기획자(PM)로서 AI를 활용한 업무 자동화(AX) 사례를 문서화하는 개인 포트폴리오 사이트입니다. 국내 기업의 AX 기획자/AI PM 직군으로의 커리어 전환을 목표로, 사이트 자체가 채용 지원용 스토리텔링 자료로 실사용되고 있습니다. 핵심 패턴: 업무 병목 발견 → AI로 워크플로우 재설계 → Before/After·근거·재현 가능성 문서화 전체 서사는 '실행하지 않은 아이디어 나열'이 아니라, 하나의 검증된 자동화 패턴과 도메인별 확장 블루프린트를 갖춘 재사용 가능한 엔진을 보유하는 방향으로 수렴하고 있습니다. 기술 스택 Next.js 16, Tailwind CSS v4, TypeScript Supabase (개발/운영 환경 분리), Vercel, Resend Claude API 기반 기술 자문 및 프롬프트 설계 보안 아키텍처 관리자 CRUD는 세션 기반 인증과 RLS(Row Level Security)로 보호 공개 읽기 전용 경로는 별도의 익명 클라이언트로 분리 서비스 롤 키는 애플리케이션 코드에서 완전히 배제 권한 검증은 마이그레이션 파일 및 대시보드 검토를 통해서만 진행 작업 방식 (에이전틱 IDE 협업 워크플로우) AI 에이전트 기반 IDE가 파일/버전관리 작업을 담당하고, LLM은 기술 자문 및 프롬프트 작성 역할을 수행하는 협업 구조를 운영합니다. AI 자문 → 에이전트용 프롬프트 작성 → 실행자가 붙여넣기 → 계획 확인 → 자문 검토 → 실행 승인 → 로그 검토 → 직접 클릭 테스트 → 명시적 지시 시에만 커밋 통제 규칙 실행 전 계획 검토 필수 요약 보고가 아닌 직접 클릭 테스트로 검증 개발 환경 우선 적용 후 명시적 승인 하에 운영 환경 반영 커밋은 명시적 지시가 있을 때만 수행 DB 자격증명 직접 연결 및 범위 외 삭제 금지 브랜치 전략: 개발 브랜치에서 작업 후 운영 브랜치는 배포 전용으로 직접 커밋 금지, 병합 전 변경 범위 검증 현재 진행 상황 고객사 미팅 브리핑 자동화 사례: 1단계 완료, 개발/운영 환경 배포 및 실데이터 테스트 완료 범위 변경 감지 자동화 사례: 상태 확인 진행 중 리스크 조기 경보 자동화 사례: 운영 환경 배포 완료 보안 사고 관련 포스트: 'AI 리스크 거버넌스 사례'로 재구성 예정 AI 활용 관련 콘텐츠 시리즈 초안 작성 완료, 게시 미확정 핵심 학습 원칙 보고가 아닌 검증: 버그·보안 이슈는 항상 직접 로그/브라우저 검토를 통해서만 발견되며, 에이전트의 자체 결과 요약을 그대로 신뢰하지 않음 보안 사고의 콘텐츠화: 실제 발생한 권한 관련 이슈 사례를 케이스 스터디로 재구성하여 콘텐츠화 커밋 의미 분리: 기능 단위/변경 유형별로 항상 분리 커밋 자동화하지 않는 것도 스토리: 저사용 빈도 기능을 의도적으로 고도화하지 않은 결정 자체를 AX 스토리텔링 소재로 활용