포괄적인 코드 리뷰가 생각보다 더 중요한 이유
요약
Next.js와 Supabase를 활용하여 보안이 강화된 인증 시스템 MVP를 구축한 사례를 소개합니다. AI 에이전트를 활용한 다각도 코드 리뷰와 테스트 우선 접근 방식을 통해 보안 취약점을 사전에 방지하는 체계적인 개발 프로세스를 다룹니다.
핵심 포인트
- 보안 우선 원칙: 규칙 정의, 테스트 우선 구축, 철저한 리뷰의 3단계 접근법
- AI 에이전트 활용: 다양한 관점의 특화된 에이전트로 고강도 코드 리뷰 수행
- 테스트 자동화: RLS(행 수준 보안) 검증을 위한 공격 시나리오 테스트 스크립트 작성
- 실질적 오류 발견: AI 리뷰를 통해 10가지 구체적인 보안 및 로직 결함 식별
프로덕션 수준의 인증 시스템 구축하기: 하루 만에 완전한 MVP 기반을 배포한 방법
오늘 저는 숙련된 기술직 여성들을 위한 마켓플레이스 앱인 HandyFEM의 인증 기반을 배포했습니다. 스캐폴딩(scaffolded)된 Next.js 프로젝트로 시작했던 것이 데이터베이스 마이그레이션(database migrations), 로그인/회원가입 흐름, 그리고 검증된 액세스 제어(access control) 레이어를 갖춘, 테스트가 완료되고 보안 감사를 마친 인증 시스템이 되었습니다. 제가 어떻게 이를 수행했는지(그리고 무엇을 다르게 했을지) 소개합니다.
솔직히 말해서, 이 작업을 시작할 때 Anthropic의 새로운 Claude 모델인 Fable 5가 출시되어 한동안 무료로 제공되었던 것이 운이 좋았습니다! 제가 가장 먼저 한 일은 제 프로젝트 전체를 대상으로 프롬프트(prompt)를 실행하여 리뷰를 수행하고 개선 사항을 찾는 것이었는데... 실제로 효과가 있었습니다!
시작점
저에게는 디자인 시스템과 빈 캔버스가 있었습니다. 서류상으로는 범위가 간단해 보였습니다: Supabase를 연결하고, 로그인/회원가입 화면을 만들고, 몇 개의 데이터베이스 테이블을 추가하는 것 말이죠. 하지만 현실적으로 "간단한" 인증은 대부분의 앱에서 보안 누출과 사용자 경험(UX) 재앙이 발생하는 지점입니다.
빠르게 만들 수도 있었습니다. 하지만 저는 대신 "제대로" 구축하는 것을 선택했고, 그 결정이 이후의 모든 과정을 결정지었습니다.
체계적인 접근 방식: 마지막이 아닌, 보안 우선
단 하나의 인증 컴포넌트를 작성하기 전에, 저는 세 가지를 수행했습니다:
1. 규칙 정의
이 프로젝트에서 "올바름"이 무엇을 의미하는지 문서화했습니다(코드 주석 및 CLAUDE.md에). 모든 입력값에 대한 Zod 검증(validation), 모든 테이블에 대한 행 수준 보안(Row-Level Security, RLS), 두 개의 분리된 Supabase 클라이언트(하나는 브라우저용, 하나는 관리자 전용), 그리고 공격자가 할 수 있는 것뿐만 아니라 할 수 없는 것을 "증명"하는 테스트 하네스(test harness)가 그것입니다.
지루하게 들릴 수도 있습니다. 하지만 사실 이것이 제가 단독으로는 발견하지 못했을 취약점을 배포하는 것으로부터 저를 구해준 핵심이었습니다.
2. 테스트 우선 구축
폼(forms)이 존재하기 전에, 저는 rls-test.mjs를 작성했습니다. 이는 두 명의 일회용 사용자를 생성하고, 9가지의 서로 다른 공격 시나리오(사용자 A가 사용자 B의 데이터를 읽을 수 있는가? 행을 위조할 수 있는가? 자신의 계정을 삭제할 수 있는가?)를 시도한 뒤, 어떤 시나리오가 예상대로 실패했는지 보고하는 스크립트입니다.
데이터베이스 마이그레이션(database migrations)이 라이브되었을 때, 테스트는 통과(green)되었습니다: 9/9 체크 통과. 그 숫자는 의미가 있었습니다. 보안 모델(security model)이 실제로 작동한다는 것을 의미했습니다.
3. 철저한 리뷰 요청
여기서 AI가 승수 효과(force multiplier)를 발휘했습니다. 저는 여러 개의 특화된 에이전트(agents)를 사용하여 고강도 코드 리뷰를 실행했습니다. 각 에이전트는 서로 다른 관점(라인별 버그, 보안 취약점, 성능 문제, 디자인 패턴 등)에서 코드에 접근했습니다. 리뷰 결과, 각각 재현 시나리오를 포함한 10가지 구체적인 발견 사항이 드러났습니다.
대부분의 팀은 이를 과잉 대응(overkill)이라고 부르겠지만, 저는 이를 필수적이라고 불렀습니다.
10가지 발견 사항: 내가 틀렸던 부분 (그리고 수정한 부분)
리뷰는 제가 그대로 배포했을 법한 문제들을 잡아냈습니다:
-
시스템 내에서 유실된 전문가 사용자: 회원가입 폼에는 부제목만 변경하는
?rol=profesional플래그가 있었습니다. 의도가 저장되지 않았던 것입니다. 이는 전문가로 가입한 여성이 조용히 고객(clients)으로 분류됨을 의미했습니다. 저는 나중에 복구할 수 없는 사용자 메타데이터(user metadata)에signup_role을 저장함으로써 이를 수정했습니다. -
기기 간에 깨지는 이메일 링크: 저는 PKCE (OAuth 코드 교환)를 사용했는데, 이는 동일한 브라우저에서 링크를 열 때만 작동합니다. 휴대폰 대신 노트북에서 링크를 열면? 링크가 실패합니다. 저는
token_hash를 사용하는 병렬 라우트(/auth/confirm)를 추가하여 기기 간 교차 작동이 가능하게 했습니다. -
Google 사용자의 이메일 주소 유출: 사용자가 Google로 로그인할 때, 데이터베이스 트리거(database trigger)가
display_name(Gmail은 전송하지 않음)을 찾다가 이메일의 로컬 파트(local-part)로 대체(fallback)되었고, 이로 인해 모든 전문가가 다른 여성의 이메일 주소를 보게 되었습니다. 저는 Google의 실제 필드(name,full_name)에 대한 대체 수단(fallbacks)을 추가했습니다.
오류 발생 후 당신을 당황하게 만드는 폼: React 19는 제출 실패 후 제어되지 않은(uncontrolled) 폼 입력을 초기화하지만, 저의 오류 메시지는 그대로 남아 이제 비어 있는 필드를 가리키고 있었습니다. 저는 상태 순환(state round-tripping)과 공유 유효성 검사 훅을 추가하여 수정할 때 오류가 사라지도록 했습니다.
- 유효성 검사를 위한 번들 크기 증가: 회원가입 페이지에서 단 5개 필드를 클라이언트 측에서 유효성 검사하기 위해 전체 Zod 라이브러리(~65KB gzipped)를 사용했습니다. 저는
zod/mini(~4KB)로 전환했고, API는 동일하지만 크기는 16배 작아졌습니다.
그리고 다섯 가지가 더 있었습니다. 각각이 중요했고, 모두 출시되었을 것입니다.
제가 AI를 코파일럿(Copilot)으로 사용한 방법 (의존 수단이 아닌)
숨기기 쉬운 것은 다음과 같습니다: 제가 작성한 모든 코드 조각을 저는 이해했습니다. 저는
- 브라우저 기본 유효성 검사가 서버 유효성 검사를 가로채지 않도록 함. 브라우저의 영어 에러 메시지가 제가 설정한 스페인어 Zod 에러를 방해하지 않도록 폼(form)에
noValidate를 추가했습니다. - 비밀번호 규칙을 숨기지 않고 유지함. 힌트가 입력하는 동안 계속 보이도록 유지했습니다 (사라져 버리는 플레이스홀더(placeholder)가 아닙니다).
- 에러 메시지가 이메일 존재 여부를 절대 드러내지 않음 (사용자 열거 공격(user enumeration attacks) 방지). "비밀번호가 틀림"과 "존재하지 않는 사용자"에 대해 동일한 메시지를 표시합니다.
- 150ms의 네트워크 지연 시간(latency) 제거. 매 페이지마다 네트워크 왕복(round-trip)이 발생하는
getUser()대신, 로컬 JWT 검증을 수행하고 실제 갱신 시에만 네트워크를 사용하는getClaims()로 전환했습니다. - 모든 보호된 페이지에 동일한
requireUser()가드(guard)를 사용함. 따라서 다음 개발자가 실수로 보호되지 않은 페이지를 배포하는 일을 방지할 수 있습니다.
이 중 화려한 것은 하나도 없습니다. 하지만 이 모든 것들이 "작동하는 것"과 "출시할 수 있는 것"의 차이를 만듭니다.
내가 배운 점
-
포괄적인 리뷰는 시간 비용을 들일 가치가 있다. 많은 노력이 들어가는 리뷰는 시간이 더 오래 걸렸지만, 출시 전에 10개의 문제를 잡아내는 것이 사용자가 영향을 받는 프로덕션(production) 환경에서 문제를 수정하는 것보다 훨씬 낫습니다. 저는 모든 인증 시스템(auth system)과 크리티컬 패스(critical path)에 대해 이 방식을 적용할 것입니다.
-
AI 보조 리뷰는 제약 조건이 있을 때 가장 효과적이다. "내 코드를 리뷰해줘"라고 하면 일반적인 피드백만 돌아옵니다. 반면 "이 코드를 7가지 다른 관점(보안, 성능, 패턴 등)에서 스캔하고, 재현 시나리오와 함께 구체적인 버그를 찾아줘"라고 하면 실제 문제들을 찾아냅니다.
-
보안을 테스트한다는 것은 _발생하지 않는 것_을 증명하는 것을 의미한다. RLS 하네스(harness)는 단순히 해피 패스(happy path)뿐만 아니라, 공격자가 할 수 없는 9가지 부정적 케이스(negative cases)를 검증합니다. 이러한 비대칭성이 중요합니다.
-
디테일이 복리로 작용한다. 작은 UX 수정(에러 발생 시 폼 값 유지)은 사용자가 비밀번호를 다시 입력해야 하는 수고를 덜어줍니다. 번들 최적화(
zod→zod/mini)는 모바일의 모든 가입 페이지에서 61KB를 절약합니다. 하나씩은 극적이지 않지만, 이들이 모이면 "작동하는 것"과 "즐거움을 주는 것"의 차이를 만듭니다.
다음 단계
인증 시스템은 다음 단계인 온보딩(signup_role이 효력을 발휘하기 시작하는 단계), 공개 디렉토리, 그리고 전문가 프로필을 맞이할 준비가 되었습니다. CI/CD는 회귀(Regression)를 자동으로 포착할 수 있도록 설정되었습니다.
세 가지 작은 잔여 항목(Google OAuth 프로바이더 설정, 교차 기기 링크를 위한 커스텀 이메일 템플릿, 약관/개인정보 보호 페이지)이 남아 있지만, 모두 추적 중이며 진행을 막는 요소(non-blocking)는 아닙니다.
하지만 기반(Foundation)—사용자는 결코 볼 수 없지만 다른 모든 것이 그 위에 구축되는 것—은 견고합니다. 저는 이를 증명할 수 있습니다.
다른 빌더들을 위하여
더 빠르게 제품을 출시하기 위해 AI를 사용하고 있다면, 다음과 같은 사항을 권장합니다:
- 빌드하기 전에 "정확함"을 정의하세요. 제약 사항(보안 모델, 성능 예산, 접근성 규칙)을 파악하고 이를 테스트로 인코딩하세요.
- 단순히 속도뿐만 아니라 폭을 넓히는 데 AI를 사용하세요. 단순히 더 빨리 쓰는 것이 아니라, 다양한 관점에서 리뷰하도록 요청하세요.
- 배포하는 코드를 이해하세요. AI를 덜 생각하기 위해서가 아니라, 더 깊이 생각하기 위해 사용하세요. 최고의 결과는 당신이 회의적일 때 나옵니다.
- 보이지 않는 것을 테스트하세요. 무엇이 발생하지 않는지(공격 실패, 승인되지 않은 접근 차단)를 증명하는 하네스(Harness)를 구축하세요. 그러한 확신은 그 어떤 UI 테스트보다 가치 있습니다.
HandyFEM의 인증 시스템은 최첨단 기술은 아닙니다. 하지만 신중하게 설계되었고, 테스트되었으며, 감사(Audit)를 거쳤고, 문서화되었습니다. 그것이 바로 자랑스러워할 만한 가치가 있는 기반입니다.
기술적 여정을 따라가고 싶다면 GitHub의 PR #11을 확인하세요. 여기에는 보안 리뷰 결과와 코드 수정 사항이 포함되어 있습니다. RLS 테스트 하네스는 scripts/rls-test.mjs에 있으며, 액세스 제어가 제대로 작동함을 증명하기 위해 언제든 실행할 수 있습니다.
Next.js 16, Supabase, React 19를 사용하여, 수많은 숙고 끝에 구축되었습니다. 타협은 없었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기