8회 · 로그인과 보안: 내 글은 나만 고치게
회원가입·로그인을 붙이고, 행 수준 보안(RLS)과 환경변수로 내가 만든 모임은 나만 고칠 수 있게 지킵니다.
“로그인은 문, 보안 규칙(RLS)은 자물쇠, 키는 금고에.”
학습 목표
- 인증(누구인가)과 권한(무엇을 할 수 있나)의 차이를 안다
- Supabase 로그인과 행 수준 보안(RLS)을 설정한다
- 환경변수와 공개 키·비밀 키를 올바르게 다룬다
오늘 만드는 것: 회원가입·로그인, 내가 만든 모임만 고칠 수 있는 웹앱
실습 진도
—
실습 단계를 체크하면 표시돼요
오늘의 흐름
| 시간 | 내용 | 방식 |
|---|---|---|
| 0:00–0:10 | 지난 시간 복습: 화면에서 모임 추가해 보기 | 함께 |
| 0:10–0:35 | 누구인가, 무엇을 할 수 있나: 인증과 권한 | 설명 |
| 0:35–0:55 | 자물쇠와 금고: RLS, 환경변수, 공개 키와 비밀 키 | 설명·체험 |
| 0:55–1:05 | 요즘 업계에서는: 170개 앱의 문이 열려 있던 사건 | 설명 |
| 1:05–1:15 | 쉬는 시간 | — |
| 1:15–1:35 | 실습 8-1: 회원가입·로그인 붙이기 | 실습 |
| 1:35–1:55 | 실습 8-2, 8-3: 보안 규칙 만들고 두 계정으로 시험 | 실습 |
| 1:55–2:00 | 정리와 퀴즈 (8-4 키 점검은 선택·과제) | 정리 |
오늘이 이 과정에서 가장 중요한 시간입니다
기능은 나중에 고쳐도 되지만, 보안 구멍은 한 번 새면 되돌릴 수 없습니다. 오늘 배우는 세 가지(로그인, RLS, 키 관리)는 이후 모든 회차, 그리고 휴대폰 앱에서도 똑같이 쓰입니다.
지금 웹앱의 문제
7회까지 만든 웹앱은 이런 상태입니다.
- 누구나 모임을 만들 수 있습니다. 장난으로 모임을 100개 만들어도 막을 방법이 없습니다.
- 누구나 남의 모임을 고치거나 지울 수 있습니다.
- 공개 키는 화면 코드에 실려 있어 누구나 볼 수 있습니다. 마음만 먹으면 웹앱을 거치지 않고 데이터베이스에 직접 요청을 보낼 수도 있습니다.
열쇠가 꽂혀 있는 공용 사물함
지금은 동네 사물함마다 열쇠가 꽂혀 있는 상태입니다. 착한 이웃만 있다면 괜찮지만, 주소가 공개된 웹에서는 그렇게 기대할 수 없습니다. 오늘은 이름표(로그인) 를 달고, 내 이름표가 붙은 칸만 열리는 자물쇠(RLS) 를 채웁니다.
인증과 권한은 다릅니다
보안 이야기에서 가장 먼저 구분해야 할 두 단어가 있습니다.
| (Authentication) | (Authorization) | |
|---|---|---|
| 묻는 질문 | 당신은 누구인가요? | 당신은 무엇을 할 수 있나요? |
| 일상의 예 | 회사 출입증 찍기 | 출입증으로 들어갈 수 있는 층 |
| 우리 웹앱 | 이메일과 비밀번호로 로그인 | 내가 만든 모임만 고치기 |
| 담당 | Supabase Auth | RLS 보안 규칙 |
로그인만 붙이고 권한을 정하지 않으면 반쪽짜리 보안입니다. "누구인지는 아는데 아무거나 다 할 수 있는" 상태가 되기 때문입니다. 실제 사고는 대부분 이 두 번째를 빠뜨려서 생깁니다.
Supabase Auth: 로그인을 빌려 쓰기
로그인을 직접 만드는 것은 전문가에게도 어렵고 위험합니다. 비밀번호를 안전하게 저장하고, 비밀번호 찾기 메일을 보내고, 로그인 상태를 유지하는 일이 모두 까다롭습니다. 그래서 요즘은 검증된 서비스의 로그인을 빌려 쓰는 것이 상식입니다.
Supabase Auth가 해 주는 일:
- 이메일·비밀번호 회원가입, 로그인, 로그아웃
- 비밀번호는 원래 글자가 아니라 되돌릴 수 없게 바꾼 값(해시) 으로 저장
- 이메일 확인 메일, 비밀번호 재설정 메일
- 원하면 구글·카카오 같은 소셜 로그인도 연결 가능
놀이공원 손목밴드
매표소(로그인)에서 신분을 확인하면 손목밴드(로그인 토큰)를 채워 줍니다. 그다음부터는 놀이기구마다 신분증을 다시 보여 줄 필요 없이 손목밴드만 보여 주면 됩니다. 밴드 색(권한)에 따라 탈 수 있는 기구가 다릅니다.
RLS: 줄마다 채우는 자물쇠
(Row Level Security, 행 수준 보안)는 데이터베이스의 한 줄 한 줄마다 "누가 읽고, 누가 만들고, 누가 고칠 수 있는지"를 정하는 규칙입니다.
7회의 엑셀 비유로 말하면, 모임 시트를 모두에게 공유하되 각 줄에 "이 줄은 하늘 님만 고칠 수 있음"이라는 자물쇠를 다는 것과 같습니다.
역할을 바꿔 가며 [수정]·[삭제]를 눌러 보세요. RLS를 끄면 무슨 일이 생기는지 비교해 보세요.
RLS가 꺼져 있으면 공개 키만 있으면 누구나 모든 행을 고치고 지울 수 있습니다.
| 모임 | 등록자 | 해 보기 |
|---|---|---|
| 11월 정기 모임 | 하늘 | |
| 북토크 번개 | 바다 | |
| 연말 모임 | 하늘 |
우리 모임의 규칙
| 하고 싶은 일 | 누가 할 수 있나 | SQL에서는 |
|---|---|---|
| 모임 목록 보기 | 누구나 (로그인 안 해도) | select 정책: true |
| 모임 만들기 | 로그인한 사람 | insert 정책: 로그인 확인 |
| 모임 고치기 | 그 모임을 만든 사람만 | update 정책: auth.uid() = owner_id |
| 모임 지우기 | 그 모임을 만든 사람만 | delete 정책: auth.uid() = owner_id |
auth.uid()는 "지금 요청한 사람의 회원 번호", owner_id는 "이 줄을 만든 사람의 회원 번호"입니다. 둘이 같을 때만 허락한다는 뜻입니다. 이 문장 하나만 읽을 수 있어도 오늘의 목표는 달성입니다.
자세히RLS를 켜기만 하고 규칙을 안 만들면 어떻게 되나요펼치기
RLS를 켜면 기본값은 "아무도 안 됨" 입니다. 그래서 규칙을 하나도 안 만들면 목록이 텅 비어 보입니다. 7회에 "목록이 비어 있어요"라는 문제를 겪었다면 바로 이 상태였습니다.
반대로 RLS를 아예 끄면 "누구나 다 됨" 입니다. 공개 키만 있으면 누구나 읽고, 고치고, 지울 수 있습니다. 가장 위험한 상태입니다.
| 상태 | 결과 | 위험 |
|---|---|---|
| RLS 꺼짐 | 누구나 무엇이든 | 매우 위험 |
| RLS 켜짐, 규칙 없음 | 아무도 아무것도 못 함 | 안전하지만 안 쓰임 |
| RLS 켜짐, 규칙 있음 | 규칙대로 | 올바른 상태 |
열쇠는 금고에: 환경변수와 두 가지 키
환경변수
는 비밀번호나 키처럼 코드에 직접 적으면 안 되는 값을 따로 보관하는 칸입니다.
- 내 컴퓨터:
.env.local파일 (.gitignore덕분에 GitHub에 안 올라감) - 배포 서버: Vercel의 Settings → Environment Variables
현관 비밀번호를 쪽지에 써서 문에 붙이지 않는 것
코드는 GitHub에 올라가고, 공개 저장소라면 전 세계가 봅니다. 키를 코드에 적는 것은 현관 비밀번호를 쪽지에 써서 문에 붙여 두는 것과 같습니다.
공개 키와 비밀 키
열쇠를 하나 누른 뒤, 둘 곳(브라우저 코드 / 서버 환경변수)을 누르세요.
| (anon / publishable) | (service_role / secret) | |
|---|---|---|
| 비유 | 건물 출입 카드 | 건물 관리인의 만능 열쇠 |
| 할 수 있는 일 | RLS 규칙이 허락한 것만 | RLS를 무시하고 무엇이든 |
| 어디에 두나 | 브라우저 코드에 들어가도 됨 | 서버에만. 절대 화면 코드에 넣지 않음 |
| Next.js 이름 | NEXT_PUBLIC_SUPABASE_… | NEXT_PUBLIC_을 붙이지 않음 |
NEXT_PUBLIC_ 이라는 이름표의 뜻
Next.js에서 이름이 NEXT_PUBLIC_으로 시작하는 환경변수는 브라우저로 내려가는 코드에 그대로 실립니다. 누구나 볼 수 있다는 뜻입니다. 공개 키에는 붙여도 되지만, 비밀 키나 10회의 AI 키에는 절대 붙이면 안 됩니다.
키가 새어 나갔다면
실수로 키를 GitHub에 올렸거나 채팅창에 붙여 넣었다면, 파일을 지우는 것만으로는 부족합니다. Git은 모든 기록을 남기기 때문에 예전 커밋에 키가 그대로 남아 있습니다.
- 해당 서비스 대시보드에서 키를 즉시 다시 발급(rotate) 합니다. 예전 키는 쓸모없어집니다.
- 새 키를
.env.local과 Vercel에 넣습니다. - 파일을 저장소에서 빼고
.gitignore에 추가합니다.
.env 파일이 GitHub에 올라간 것 같아. 저장소에서 지우고 .gitignore에 추가해줘. 그리고 다시 발급해야 할 키 목록과 방법을 알려줘.
요즘 업계에서는
170개 앱의 문이 열려 있었다
2025년 봄, 한 보안 연구자가 AI 앱 빌더 Lovable로 만든 공개 앱 1,645개를 조사했더니 170개(약 10%) 에서 로그인하지 않은 누구나 개인정보를 읽을 수 있는 문제가 발견됐습니다. 원인은 오늘 배운 바로 그것, Supabase 표에 RLS가 제대로 걸려 있지 않아 공개 키만으로 데이터에 접근할 수 있었던 것입니다. 이 문제는 CVE-2025-48757이라는 번호로 등록됐습니다. (위키백과 · Vibe coding, CVE-2025-48757)
이 사건이 알려 주는 것:
- AI가 만든 앱도 동작은 완벽해 보였습니다. 화면에서는 아무 문제가 보이지 않습니다. 보안 구멍은 눈에 안 보입니다.
- 플랫폼 회사는 "앱 데이터를 지키는 책임은 앱을 만든 사람에게 있다"는 입장이었습니다. 즉, 만든 사람이 여러분이면 책임도 여러분에게 있습니다.
- 해결책은 어렵지 않았습니다. 규칙 몇 줄과 직접 막히는지 시험해 보는 습관이면 충분했습니다.
개발자들도 AI를 다 믿지는 않는다
2025년 Stack Overflow 설문에서 개발자의 46%가 AI 도구의 정확성을 믿지 않는다고 답했습니다. 믿는다는 응답(33%)보다 많습니다. (Stack Overflow 2025 설문 · AI) 전문가들도 AI가 만든 결과를 검증하는 것을 당연한 일로 여긴다는 뜻입니다. 보안은 그중에서도 가장 먼저 검증할 부분입니다.
시연
강사가 다음을 보여 줍니다.
- RLS 없이 공개 키만으로 데이터를 읽고 지우는 모습을 시험용 프로젝트에서 보여 줍니다. (실제 서비스에 해 보면 안 됩니다.)
- Claude Code에게 RLS 규칙을 만들게 하고, 같은 시도가 막히는 것을 보여 줍니다.
- 브라우저 두 개(일반 창과 시크릿 창)에 각각 다른 계정으로 로그인해, B 계정으로는 A의 모임에 수정 버튼이 안 보이고, 억지로 시도해도 거절되는 것을 확인합니다.
실습
오늘의 주제: "책 읽는 저녁" 웹앱에 회원가입·로그인을 붙이고, 모임마다 "만든 사람" 칸을 추가해 만든 사람만 고치고 지울 수 있게 합니다. 시험용 계정은 실제로 쓰는 이메일 대신 별칭을 써도 됩니다.
실습 8-1 · 회원가입·로그인 붙이기
약 20분Claude Code에서 웹앱 프로젝트를 열고 플랜 모드로 요청합니다.
PROMPT회원가입·로그인 붙이기 플랜 모드 권장Supabase 로그인으로 이메일 회원가입·로그인·로그아웃을 붙여줘. 로그인한 사람만 모임을 만들 수 있게 하고, 계획부터 보여줘.
이렇게 되면 성공계획1. Supabase 로그인 도구 설치 (@supabase/ssr)2. /login 화면: 이메일·비밀번호로 회원가입과 로그인3. 상단에 로그인 상태 표시와 로그아웃 버튼4. meetups 표에 owner_id(만든 사람) 칸 추가5. 로그인하지 않으면 "모임 만들기" 대신 "로그인하고 모임 만들기" 안내이대로 진행할까요?계획을 읽고 궁금한 점을 물어본 뒤 진행을 허락합니다.
Supabase 대시보드 → Authentication → Sign In / Providers에서 Email이 켜져 있는지 확인합니다. 실습 중에는 확인 메일을 기다리기 번거로우니 Confirm email을 잠시 꺼 둬도 됩니다. 실제로 운영할 때는 다시 켜세요.
localhost:3000/login에서 시험용 계정 A를 만들고 로그인합니다. Supabase Authentication → Users에 계정이 생겼는지 확인하세요. 비밀번호가 보이지 않는 것도 확인해 보세요. 원래 글자가 저장되지 않기 때문입니다.PROMPT로그인 흐름 쉽게 설명 듣기방금 만든 회원가입·로그인이 어떤 순서로 동작하는지, 놀이공원 입장권에 빗대어 처음 배우는 사람이 알아듣게 설명해줘. 비밀번호는 어디에 저장되는지도.
막혔을 때
실습 8-2 · 보안 규칙(RLS) 만들기
약 15분규칙을 표로 읽고 이해한 뒤 실행합니다.
- PROMPT보안 규칙(RLS) 만들기
모임 테이블에 RLS 정책을 만들어줘. 누구나 읽기 가능, 로그인한 사람만 만들기, 만든 사람만 고치고 지우기. 각 정책이 무슨 뜻인지 표로 설명해주고 SQL을 줘.
이렇게 되면 성공| 정책 이름 | 할 일 | 누구에게 || 누구나 모임 보기 | select | 모두 || 로그인하면 모임 만들기 | insert | 로그인한 사람 || 내 모임만 고치기 | update | auth.uid() = owner_id || 내 모임만 지우기 | delete | auth.uid() = owner_id |alter table meetups enable row level security;create policy "누구나 모임 보기" on meetups for select using (true);… - PROMPTSQL 한 줄씩 설명 듣기
이 SQL을 한 줄씩 엑셀에 빗대어 설명해줘. 데이터를 지우거나 바꾸는 줄이 있으면 따로 표시해줘.
지우는 문장이 없는지 확인한 뒤 Supabase SQL Editor에서 실행합니다. Authentication → Policies(또는 Table Editor의 표 옆 방패 아이콘)에서 정책 4개가 보이면 성공입니다.
막혔을 때
실습 8-3 · 두 계정으로 막히는지 시험
약 15분"만들었다"가 아니라 "막히는 것을 봤다"가 완료 기준입니다.
- PROMPT두 계정으로 보안 시험하기
보안 규칙이 제대로 동작하는지 시험하고 싶어. 계정 A와 계정 B로 각각 해 볼 일을 순서대로 적어줘. 특히 B가 A의 모임을 고치거나 지우려 할 때 막히는지, 로그아웃한 상태에서는 무엇이 보이는지 확인하는 단계를 넣어줘.
- Windows 기준
크롬·엣지에서 Ctrl + Shift + N으로 시크릿(InPrivate) 창을 엽니다. 일반 창은 계정 A, 시크릿 창은 계정 B로 로그인합니다.
받은 순서대로 해 보고 결과를 기록합니다.
이렇게 되면 성공[계정 A] 모임 만들기 → 성공, 내 모임에 "수정" 버튼 보임[계정 B] A의 모임 → "수정" 버튼 안 보임[계정 B] 주소창에 /meetups/1/edit 직접 입력 → "권한이 없어요"[로그아웃] 목록은 보이고, "모임 만들기"는 로그인 안내로 바뀜막혔을 때 - PROMPT바뀐 내용 커밋하기
지금까지 바뀐 내용을 한글 메시지로 커밋해줘.
푸시 후 배포 주소에서도 로그인이 되는지 확인합니다. 배포 주소에서 로그인 후 이상한 주소로 돌아간다면 Supabase Authentication → URL Configuration에 배포 주소를 추가해야 합니다. "배포 주소에서 로그인하려면 Supabase에 무엇을 설정해야 해?"라고 물어보세요.
실습 8-4 · 키가 제자리에 있는지 점검
약 10분- PROMPT키가 제자리에 있는지 점검
이 프로젝트에서 쓰는 키와 비밀값을 모두 찾아서 표로 보여줘. 각각 어디에 저장돼 있는지(.env.local, 코드, 브라우저로 내려가는 코드), 브라우저에 노출돼도 되는 값인지, .gitignore에 들어 있는지 확인해줘. 값 자체는 화면에 보여 주지 마.
- PROMPT보안 점검
이 프로젝트의 보안을 점검해줘. 키가 코드에 노출돼 있거나, 보안 규칙(RLS) 없이 누구나 쓸 수 있는 테이블, 앱 안에 들어간 비밀 키가 있는지 확인해서 목록으로 보여줘.
점검 결과에서 "위험"으로 나온 것이 있으면 하나씩 고치고, 고친 뒤 다시 점검합니다. 판단이 어려우면 다음 수업에 가져와 함께 봅니다.
정리
핵심 정리
- 1인증은 "누구인가", 권한은 "무엇을 할 수 있나". 로그인만으로는 반쪽 보안이다.
- 2로그인은 직접 만들지 않고 Supabase Auth 같은 검증된 서비스를 빌려 쓴다.
- 3RLS는 줄마다 채우는 자물쇠. auth.uid() = owner_id 는 "만든 사람만"이라는 뜻이다.
- 4공개 키는 RLS가 허락한 것만, 비밀 키는 무엇이든. 비밀 키에는 NEXT_PUBLIC_ 을 붙이지 않는다.
- 5보안은 두 계정으로 막히는 것을 직접 봐야 끝난다. 키가 새면 지우는 게 아니라 다시 발급한다.
다음 시간 예고: 이제 안전한 뼈대가 생겼습니다. 9회에는 참석 신청, 사진, 검색을 붙여 실제로 쓸 만한 서비스로 완성합니다.
복습
Q1."인증"과 "권한"의 차이로 맞는 것은?
Q2.RLS 정책의 auth.uid() = owner_id 는 무슨 뜻일까요?
Q3.RLS를 켜고 정책을 하나도 만들지 않으면, 누구나 모든 데이터를 읽고 쓸 수 있다.
Q4.Next.js에서 이름이 NEXT_PUBLIC_으로 시작하는 환경변수는?
Q5.실수로 키를 GitHub에 올렸을 때 할 일을 순서대로 맞춰 보세요.(순서대로 정렬)
- 1새 키를 .env.local과 Vercel에 넣는다
- 2서비스 대시보드에서 키를 즉시 다시 발급한다
- 3파일을 저장소에서 빼고 .gitignore에 추가한다
Q6.다른 사람 모임의 "수정" 버튼을 화면에서 숨겼다면 보안은 충분하다.
Q7.2025년 Lovable로 만든 공개 앱 1,645개 중 170개에서 개인정보가 노출될 수 있었던 주된 원인은?
과제
과제 · 두 계정 보안 시험 결과
실습 8-3의 시험 결과를 적어 오세요. 문제가 없었다면 "B 계정으로 A의 모임을 지우려 했을 때 어떤 안내가 나왔는지"를 적습니다. 가족에게 시크릿 창으로 계정 B 역할을 부탁해 봐도 좋습니다.