
페스티봄의 기능이 어느 정도 갖춰진 뒤에는 화면이 잘 열리는 것과 함께 점검할 것이 늘었습니다. 제 컴퓨터에서는 멀쩡했지만 다른 브라우저나 휴대전화에서 CSS가 깨졌고, 어떤 기능은 아예 작동하지 않았습니다. 데이터를 입력하고 다시 확인하는 과정에서 값이 예상대로 남지 않는 경우도 있었습니다.
보안은 더 막막했습니다. 로그인 화면을 만들었다고 관리 기능과 데이터까지 안전해지는 것은 아니었습니다. 누가 관리자 화면에 들어갈 수 있는지, 로그인한 사람이 다른 사용자의 데이터를 읽지는 못하는지, 브라우저에 노출되면 안 되는 키가 섞이지 않았는지를 따로 확인해야 했습니다.
저는 전문적인 보안 감사를 할 수 있는 개발자가 아닙니다. 대신 실제로 사용하면서 생긴 문제를 기록하고, AI에게 테스트를 맡긴 뒤 결과를 문서에 남기는 방식으로 범위를 넓혔습니다. 이번 글에서는 Next.js와 Supabase로 만든 서비스를 어떤 순서로 테스트했고, RLS와 MFA를 왜 적용했는지 제 경험을 바탕으로 정리합니다.
Next.js Supabase 보안, 화면 확인만으로 부족했던 이유
기능 테스트와 보안 확인은 질문부터 달랐습니다. 기능 테스트에서는 버튼을 눌렀을 때 원하는 화면이 열리는지, 입력한 데이터가 제대로 저장되는지를 봅니다. 보안 확인에서는 권한이 없는 사람이 같은 주소에 접근했을 때 실제로 차단되는지를 확인합니다.
화면에서 관리자 메뉴를 숨기는 것만으로는 충분하지 않습니다. 주소를 직접 입력하거나 데이터베이스에 요청을 보내도 막혀야 합니다. 그래서 페스티봄에서는 관리자 로그인, Supabase 데이터 권한, 환경변수를 서로 다른 항목으로 나눠 확인했습니다.
처음에는 이런 구분을 잘 몰랐습니다. 문제가 생길 때마다 하나씩 고치다 보니 로그인 여부를 확인하는 코드와 데이터 접근을 막는 설정이 별개라는 점을 알게 됐어요. 이후에는 “화면에서 안 보인다”가 아니라 “권한이 없는 요청이 차단되는가”를 기준으로 테스트했습니다.
다른 브라우저·기기·시크릿 모드로 기능을 확인한 방법
Next.js 공식 문서에서는 실제 사용자 흐름을 브라우저에서 재현하는 E2E 테스트를 설명합니다.[1] 저는 자동 테스트만으로 끝내지 않고 다른 브라우저와 기기에서 직접 서비스를 열었습니다. 로그인 정보나 캐시가 남아 있지 않은 시크릿 모드도 함께 사용했습니다.
데이터·기능·CSS를 따로 확인했습니다
확인할 항목은 데이터, 기능, 화면 세 가지였습니다. 데이터는 입력한 내용이 저장되고 다시 불러와지는지 살폈고, 기능은 로그인과 관리자 작업, 달력 이동을 실제 사용 순서대로 눌러봤습니다. 화면에서는 PC와 모바일의 글자, 버튼, 여백이 깨지지 않는지 점검했어요.
한 환경에서만 보면 놓치는 부분이 많았습니다. 평소 사용하던 브라우저에는 로그인 상태와 저장된 데이터가 남아 있어서 문제가 가려지기도 했어요. 시크릿 모드에서는 처음 방문한 사람과 비슷한 조건으로 로그인 전후의 흐름을 볼 수 있었습니다.
모바일은 화면 크기까지 달라서 별도로 확인해야 했습니다. PC에서 한 줄로 보이던 내용이 좁은 화면에서 겹치거나, CSS가 깨져 버튼을 누르기 어려운 문제가 생길 수 있었기 때문입니다. 자동 테스트가 통과해도 눈으로 확인해야 찾을 수 있는 문제였어요.
자동 테스트와 직접 확인을 함께 사용했습니다
코드를 수정할 때마다 제가 모든 기능을 처음부터 다시 누르는 것은 현실적으로 어려웠습니다. 그래서 AI에게 기존 기능이 깨지지 않았는지 자동 테스트, 타입 검사, ESLint, 프로덕션 빌드를 실행하게 했습니다. 보안 설정을 정리하던 시점에는 전체 자동 테스트 335개가 통과했고, 타입 검사와 빌드 결과도 함께 기록했습니다.
여기서 335개는 보안만 검사한 숫자가 아니라 프로젝트 전체의 회귀 테스트 수입니다. 자동 테스트는 이전 기능이 그대로 작동하는지 빠르게 확인하는 데 도움이 됐습니다. 반면 기기별 화면과 실제 로그인 흐름은 제가 직접 확인했습니다. 두 방법은 어느 하나로 대체하기보다 함께 쓰는 편이 나았어요.
관리자 로그인과 데이터 권한을 이중으로 막은 방법
페스티봄의 관리자 기능에는 축제와 아티스트 정보를 추가하거나 수정하는 작업이 들어갑니다. 일반 사용자에게는 필요하지 않은 권한입니다. 저는 관리자 화면에 들어오는 단계와 데이터베이스에서 실제 변경을 허용하는 단계를 나눠 막았습니다.
로그인 사용자와 관리자를 다시 구분했습니다
로그인은 사용자가 누구인지 확인하는 과정입니다. 로그인했다고 모두 관리자가 되는 것은 아닙니다. 페스티봄에서는 Next.js 관리자 영역에 들어올 때 Supabase의 getUser()로 사용자를 확인하고, 데이터베이스의 is_admin() 함수로 관리자 역할인지 다시 검사하도록 구성했습니다.
일반 회원이 관리자 주소를 직접 입력하면 홈으로 돌아가도록 했습니다. 다만 이 처리는 화면 접근을 편리하게 막는 장치입니다. 최종적으로 데이터를 보호하는 역할은 뒤에서 설명할 RLS가 맡도록 했습니다.
TOTP MFA와 AAL2를 관리자 조건에 넣었습니다
관리자 계정에는 인증 앱의 일회용 번호를 사용하는 TOTP 방식의 다중 인증도 적용했어요. Supabase는 비밀번호 등 첫 인증만 마친 상태를 AAL1, 두 번째 인증 수단까지 통과한 상태를 AAL2로 구분합니다.[2]
관리자 역할만 가지고 있거나 MFA만 통과해서는 부족하게 만들었습니다. 관리자 역할과 AAL2를 모두 만족해야 is_admin()이 통과하도록 했습니다. 실제 검증에서는 관리자 AAL1 요청은 거부되고, 관리자 AAL2 요청은 허용됐습니다. 일반 회원은 AAL2 상태여도 관리자 권한을 얻지 못했습니다.
Supabase RLS로 로그인 뒤의 데이터까지 막기
RLS는 Row Level Security의 약자로, 데이터베이스의 각 행을 누가 읽고 바꿀 수 있는지 정하는 기능입니다. Supabase는 브라우저에서 데이터 API를 사용할 때 노출된 스키마의 테이블에 RLS를 적용하도록 안내합니다.[3]
페스티봄에서는 공개 방문자, 일반 회원, 관리자, 자동 입력에 사용하는 봇의 역할을 나눴어요. 공개 승인이 끝난 축제와 아티스트 정보는 누구나 읽을 수 있지만, 축제·아티스트·티켓 정보를 추가하거나 수정하는 작업은 관리자만 가능하게 했습니다.
다른 회원의 데이터가 정말 막히는지 확인했습니다
RLS 정책을 작성했다고 끝내지 않고 서로 다른 일반 회원 A와 B의 인증 상태를 모의해 상대방 데이터에 접근해 봤습니다. 관심 정보, 개인 일정, 기록처럼 사용자에게 귀속되는 9개 테이블과 관련 RPC 두 개를 대상으로 조회·추가·수정·삭제를 양방향으로 확인했습니다.
결과는 다른 계정의 데이터에 대한 요청이 모두 차단되는 것이었습니다. 테스트용 데이터는 트랜잭션 안에서 만들고 마지막에 되돌려 실제 데이터베이스에 남지 않게 했습니다. 설정 화면만 보는 것보다 권한 없는 동작을 일부러 시도해 보는 편이 훨씬 분명했어요.
환경변수와 비밀키를 브라우저에 두지 않기
환경변수라는 이름만 붙이면 모든 값이 숨겨진다고 생각하기 쉽습니다. 하지만 Next.js에서 NEXT_PUBLIC_ 접두사가 붙은 값은 빌드할 때 브라우저용 코드에 포함됩니다.[4] 따라서 이 접두사가 붙은 변수는 사용자가 볼 수 있다는 전제로 다뤄야 합니다.
Supabase의 publishable 키나 기존 anon 키는 브라우저에서 사용할 수 있지만, 이것만 믿고 데이터를 열어두면 안 됩니다.[5] 앞에서 설명한 RLS가 반드시 함께 작동해야 합니다. 반대로 RLS를 우회할 수 있는 service_role 키는 NEXT_PUBLIC_ 변수에 넣지 않고 서버에서만 사용하도록 확인했습니다.
저장소에 환경변수 파일이 올라가지 않는지도 점검했습니다. 화면에 필요한 공개 설정과 서버에서만 써야 하는 비밀 값을 구분하니, 어떤 키를 어디에서 사용해도 되는지 판단하기 쉬워졌어요.
문서로 확인해 보니 아직 남은 보안 작업
여기까지 했다고 페스티봄이 완벽하게 안전해진 것은 아닙니다. 작업 문서를 다시 살펴보니 로그인 시도 제한과 CAPTCHA, 운영 환경의 방화벽 규칙과 이상 로그 알림은 더 보완할 항목으로 남아 있었어요.
CSP는 우선 Report-Only 상태로 관찰한 뒤 실제 차단 모드로 바꾸는 과정이 필요했습니다. 최신 DB 백업을 격리 환경에서 복구하는 시험은 마쳤지만, Supabase 제공 백업과 Storage까지 포함한 전체 복구 훈련은 남아 있습니다. 로그인 과정에서 시간이 약 한 시간 어긋났다는 경고도 기록해 뒀어요.
처음에는 어디까지 해야 보안 설정이 끝나는지 알 수 없었습니다. 지금도 그 답을 완전히 안다고 말하기는 어렵습니다. 다만 완료한 항목과 남은 항목을 같은 문서에 적어두니, 막연히 안전하다고 생각하는 대신 다음에 확인할 일을 찾을 수 있게 됐어요.
비개발자가 공개 전 확인할 체크리스트
- 평소 쓰는 브라우저뿐 아니라 다른 브라우저와 시크릿 모드에서도 핵심 기능을 실행합니다.
- PC와 모바일에서 데이터, 기능, CSS를 각각 확인합니다.
- 로그인하지 않은 사용자, 일반 회원, 관리자의 접근 결과를 구분해 봅니다.
- 일반 계정 두 개로 서로의 개인 데이터를 읽거나 바꿀 수 없는지 검사합니다.
NEXT_PUBLIC_변수와 서버 전용 비밀키가 섞이지 않았는지 확인합니다.- 코드 변경 뒤 자동 테스트, 타입 검사, 린트, 프로덕션 빌드를 다시 실행합니다.
- 끝내지 못한 보안 항목도 숨기지 않고 다음 작업으로 기록합니다.
비개발자인 제게 테스트는 “잘 된다”를 확인하는 작업보다 “어디에서 실패해야 하는가”를 정하는 과정에 가까웠어요. 일반 회원의 관리자 요청이 실패하고, 다른 계정의 데이터 조회가 실패하는 것도 정상적인 결과였습니다.
바이브 코딩으로 기능을 빠르게 만들 수는 있었지만, 테스트와 보안까지 자동으로 완성되지는 않았습니다. 브라우저와 기기를 바꿔 직접 확인하고, Supabase 권한을 실제 요청으로 검증하고, 남은 작업을 문서에 남겨야 비로소 공개할 수 있는 서비스에 가까워졌습니다. 다음에 기능을 추가할 때도 저는 성공 화면보다 먼저 실패해야 할 조건부터 적어볼 생각입니다.
참고 자료
- Next.js, Testing: 브라우저에서 실제 사용자 흐름을 확인하는 E2E 테스트의 역할을 참고했습니다.
- Supabase, Multi-Factor Authentication: TOTP MFA의 등록·인증 흐름과 AAL1·AAL2 구분을 확인했습니다.
- Supabase, Row Level Security: 데이터 행 단위 접근 정책과 RLS 적용 원칙을 확인했습니다.
- Next.js, Environment Variables:
NEXT_PUBLIC_변수가 브라우저 코드에 포함되는 방식과 환경변수 관리 기준을 확인했습니다. - Supabase, API keys: 브라우저용 publishable·anon 키와 서버 전용 secret·service_role 키의 차이를 확인했습니다.
함께 읽는 시리즈
비개발자의 바이브 코딩 도전기
-
1편
비개발자 바이브 코딩 후기: 어려웠던 이유 5가지
-
2편
LLM 코드 생성 원리: 2026 Codex는 코드를 어떻게 수정할까?
-
3편
2026 비전공자의 첫 바이브 코딩 MVP 만들기: 페스티봄 핵심 기능과 범위 정하기
-
4편
컴포넌트란? 2026 바이브 코딩 컴포넌트 비개발자가 부딪히며 배운 코드 재사용 방법
-
5편
데이터베이스 설계 방법: 2026 비개발자가 축제와 아티스트를 연결한 과정
-
6편
2026 Next.js·Supabase 관리자 페이지 만들기: JSON 등록과 승인 기능 구현
-
7편
2026 비개발자 Next.js 캘린더 만들기: 라이브러리 없이 페스티벌 일정 구현하기
-
8편
리팩토링이란? 2026 비개발자가 바이브 코딩 코드 구조를 나눈 방법
-
9편 · 현재 글
Next.js Supabase 보안 설정: 2026 비개발자가 RLS·MFA·서비스 테스트까지 한 과정