로그인 구현 방법 4가지, 직접 붙여보고 하나는 걷어냈습니다
이 글의 독자AI로 화면은 만들었는데 로그인을 어떤 방식으로 붙일지 못 정한 사람
한줄 요약: 로그인은 하나의 기능이 아니라 네 갈래입니다. 이메일만 받고 비밀번호를 없애는 매직링크, 남의 계정을 빌려 쓰는 소셜 로그인, 계정 없이 본인만 확인하는 별명과 핀번호, 사람이 아니라 서버가 들어올 때 쓰는 API 키입니다. 저는 네 가지를 다 붙여 봤고 그중 매직링크는 다 만들어 놓고 통째로 걷어냈습니다. 각각을 언제 고르는지, 그리고 제가 어디서 막혔는지를 적었습니다.
목차
- 로그인을 붙이기 전에, 정말 계정이 필요한가
- 네 가지 방법과 고르는 기준
- 매직링크, 이메일만 받고 비밀번호를 없앤다
- 소셜 로그인, 남의 계정을 빌린다
- 별명과 핀번호, 계정 없이 본인만 확인한다
- API 키, 사람이 아니라 서버가 들어올 때
- 다시 만든다면 어디로 가는가
1. 로그인을 붙이기 전에, 정말 계정이 필요한가
AI에게 서비스를 만들어 달라고 하면 로그인 화면이 딸려 옵니다. 이메일 칸, 비밀번호 칸, 회원가입 버튼까지 그럴듯하게 붙어 나옵니다. 그런데 그 화면을 실제로 돌아가게 만들려는 순간 질문이 하나 생깁니다. 내 서비스에 정말 계정이 필요한가.
저는 이 질문을 늦게 했습니다. 수강생용 제출 사이트에 매직링크 로그인을 다 붙여 놓고, 그 위에 얹을 것을 세어 보다가 통째로 걷어냈습니다. 어떻게 붙이느냐보다 붙일지 말지가 먼저였습니다.
계정이 하는 일은 두 가지입니다. 하나는 “지금 들어온 사람이 누구인지” 알아내는 것이고, 다른 하나는 “그 사람의 것을 다음에 다시 꺼내 주는” 일입니다. 앞의 것만 필요하다면 회원가입까지 안 가도 됩니다.
이 블로그의 댓글이 그렇습니다. 댓글에 필요한 건 신원 확인이 아니라 “내가 쓴 걸 내가 지울 수 있는” 권한뿐이었습니다. 그래서 계정을 만들지 않고 별명과 핀번호로 갔습니다.
2. 네 가지 방법과 고르는 기준
| 방법 | 사용자가 하는 일 | 내가 만들 것 | 언제 고르나 |
|---|---|---|---|
| 매직링크 | 이메일 입력, 메일함에서 링크 클릭 | 로그인 페이지, 메일 발송 경로 | 이메일이 이미 명단으로 있을 때 |
| 소셜 로그인 | 구글이나 카카오 버튼 한 번 클릭 | 제공자 앱 등록, 리디렉트 주소 등록 | 대상 사용자가 그 계정을 다 갖고 있을 때 |
| 별명과 핀번호 | 별명과 숫자 4~8자리 입력 | 입력 칸 2개, 핀 해시 저장 | 필요한 게 본인 확인뿐일 때 |
| API 키 | 없음 (사람이 아님) | 키 발급, 폐기, 사용 기록 | 서버나 자동화가 대신 들어올 때 |
고르는 순서는 위에서 아래가 아니라 아래에서 위입니다. 제일 싼 것부터 보고, 그걸로 안 되는 이유가 생겼을 때만 한 칸 올라갑니다.
고르는 순서: 아래 칸에서 시작해 이유가 생길 때만 올라간다
- 1단계 본인 확인만 필요한가 예 → 별명과 핀번호로 끝. 계정 없음
- 2단계 다음에 다시 꺼내 줘야 하나 예 → 계정 필요. 아래 둘 중 하나로
- 3단계 사용자가 다 가진 계정이 있나 예 → 소셜 로그인 / 아니오 → 매직링크
- 4단계 사람이 아니라 서버가 들어오나 예 → API 키를 따로 발급
이 블로그의 댓글은 1단계에서 종료
3. 매직링크, 이메일만 받고 비밀번호를 없앤다
매직링크는 비밀번호를 아예 만들지 않는 방식입니다. 이메일만 받아서 일회용 링크를 보내고, 사용자가 그 링크를 누르면 로그인이 끝납니다. 비밀번호를 저장하지 않으니 비밀번호가 새어 나갈 일도 없습니다.
Supabase를 쓰면 이건 함수 하나입니다.
supabase.auth.signInWithOtp({
email,
emailRedirectTo: 'https://내사이트/login.html'
})
문제는 이 한 줄 뒤에 붙는 것들입니다. 메일이 실제로 도착해야 하고, 링크를 누르고 돌아온 사용자를 받아 줄 페이지가 있어야 하고, 아무나 요청하는 걸 막아야 합니다.
매직링크 로그인이 실제로 지나가는 길
- 1. 사용자 이메일 입력 비밀번호 칸 없음
- 2. 서비스 일회용 링크 발송 여기서 메일 발송 경로가 필요
- 3. 사용자 메일함에서 링크 클릭 이 이탈 구간이 가장 큰 마찰
- 4. 서비스 세션 발급, 명단 대조 명단에 없으면 즉시 로그아웃
함수는 한 줄, 만들 것은 네 칸
제가 이 방식을 고른 이유는 사용자 이메일이 이미 명단으로 있었기 때문입니다. 수강생 명단이 이메일 기준이라 구글 계정으로 묶으면 네이버나 다음 메일을 쓰는 사람이 로그인을 못 할 것 같았습니다.
메일 발송에서 진짜로 막힙니다
여기가 이 글에서 가장 중요한 대목입니다. 매직링크를 붙이면 반드시 메일 발송 문제를 만납니다.
Supabase가 기본으로 주는 메일 발송기는 시간당 2통입니다. 공식 문서가 이 숫자를 표에 그대로 적어 두고, 바꾸려면 메일 발송 업체를 따로 연결해야 한다고 못 박고 있습니다. 이 연결을 커스텀 SMTP라고 부르는데, “메일은 내가 지정한 업체를 통해 나간다”고 설정에 적어 두는 일입니다.
시간당 2통이면 나 혼자 테스트하는 동안에도 막힙니다. 그래서 매직링크를 쓰기로 했다면 메일 발송 서비스를 함께 정해야 합니다. Supabase의 발송 설정 문서가 예시로 이름을 올려 둔 곳이 Resend, AWS SES, Postmark, SendGrid 등이고, 같은 문서가 기본 발송기는 운영용이 아니라고 적고 있습니다.
발송 서비스를 붙이는 일은 코드보다 도메인 설정 쪽이 큽니다. 도메인 주소록(DNS)에 “이 업체가 우리 도메인 이름으로 메일을 보내도 된다”는 표시를 세 줄 넣어야 합니다. 저는 발송 전용 하위 도메인을 하나 파서 기존 메일과 분리하는 쪽으로 설계했습니다. 메인 도메인이 발송 평판에 휘말리지 않게 하려는 목적이었는데, 이 설계는 실행 직전에 접었습니다. 왜 접었는지는 마지막 절에서 이야기하겠습니다.
4. 소셜 로그인, 남의 계정을 빌린다
소셜 로그인은 구글이나 카카오, 디스코드 같은 곳의 계정을 빌려 쓰는 방식입니다. 사용자는 버튼 한 번 누르고, 내 서비스는 비밀번호를 아예 갖지 않습니다. 비밀번호 분실 처리, 재설정 메일, 비밀번호 규칙 같은 것이 통째로 사라집니다.
| 항목 | 소셜 로그인 | 매직링크 |
|---|---|---|
| 사용자 마찰 | 버튼 한 번 | 메일함 왕복 |
| 메일 발송 필요 | 없음 | 필수 |
| 못 들어오는 사람 | 그 계정이 없는 사람 | 거의 없음 |
| 준비물 | 제공자 앱 등록, 리디렉트 주소 | 발송 서비스, 도메인 설정 |
저는 다른 프로젝트에서 이쪽을 골랐습니다. 콘텐츠 챌린지 사이트는 운영 자체를 디스코드 서버에서 했기 때문에 참가자가 이미 전원 디스코드 계정을 갖고 있었습니다. 그래서 계정 식별을 디스코드 로그인으로 통일했고, 사람이 들어오는 문은 그 버튼 하나였습니다. 3절에서 매직링크를 고른 것과 정반대인데 판단 기준은 같습니다. 대상 사용자가 그 계정을 이미 다 갖고 있는가. 강의 수강생은 아니었고 챌린지 참가자는 그랬습니다.
준비 과정에서 제가 가장 자주 막힌 곳은 리디렉트 주소입니다. 내 서비스가 “여기로 돌려보내 주세요”라고 보낸 주소와 제공자 화면에 등록해 둔 주소가 글자 하나라도 다르면 인증이 중간에 끊깁니다. 이 사고의 생김새는 n8n에서 구글 계정을 연결할 때 겪는 것과 똑같습니다. 도구가 달라도 원인이 같습니다.
5. 별명과 핀번호, 계정 없이 본인만 확인한다
계정을 만들지 않고도 “이거 내가 쓴 것”만 증명하게 하는 방법이 있습니다. 글을 쓸 때 별명과 숫자 몇 자리를 같이 받아 두고, 지울 때 그 숫자를 다시 물어보면 됩니다.
이 블로그의 댓글이 그렇게 돌아갑니다. 지금 이 글 아래에도 같은 칸 두 개가 있습니다.
만들 때 지켜야 할 것이 하나 있습니다. 핀번호를 그대로 저장하면 안 됩니다. 무작위 문자열(소금값)을 하나 붙여서 해시로 바꾼 값만 저장하고, 지울 때는 입력받은 숫자에 같은 소금값을 붙여 다시 해시해서 두 값이 같은지만 봅니다. 이렇게 하면 저장된 데이터를 열어 봐도 원래 숫자를 알 수 없습니다.
지운 댓글은 데이터에서 없애지 않고 “지워짐” 표시만 켰습니다. 실수로 지운 경우에 되돌릴 수 있고, 이상한 요청이 들어왔을 때 기록이 남기 때문입니다.
6. API 키, 사람이 아니라 서버가 들어올 때
지금까지는 사람이 들어오는 문이었습니다. 자동화 도구나 내가 짠 스크립트가 내 서비스를 부를 때는 다른 문이 필요합니다. 이때 쓰는 게 API 키인데, 사람 대신 프로그램이 들고 다니는 긴 비밀번호라고 보면 됩니다.
사람용 로그인과 섞으면 안 되는 이유가 있습니다. 브라우저에 있는 것은 누구나 볼 수 있어서, 오래 사는 키를 화면 쪽 코드에 넣으면 그대로 공개됩니다. 키를 어디에 두어야 하는지는 따로 정리해 뒀습니다.
| 규칙 | 이유 |
|---|---|
| 원문은 발급 때 한 번만 표시 | 저장소에는 해시만 보관, 다시 조회 불가 |
| 키마다 이름과 용도 부착 | 어느 키가 새어도 그것만 폐기 가능 |
| 폐기 시각, 마지막 사용 시각 기록 | 안 쓰는 키 정리와 사고 추적의 근거 |
| 사용자와 키를 묶어 검증 | 남의 이름으로 요청 보내는 것 차단 |
마지막 줄이 실무에서 제일 중요합니다. 키가 유효한지만 보고 통과시키면, 유효한 키를 가진 사람이 다른 사람 이름으로 데이터를 넣을 수 있습니다. 키로 사용자를 알아낸 다음 요청 안의 이름과 그 사용자가 같은지까지 봐야 합니다. 이 검증을 어디에 둘지는 인증과 데이터 잠금이 서로 다른 일을 한다는 이야기와 이어집니다.
7. 다시 만든다면 어디로 가는가
매직링크는 다 만들어 놓고 걷어냈습니다. 로그인 페이지, 명단 대조 함수, 세션 가드까지 돌아가는 상태였는데 그 위에 얹을 것들을 세어 보니 제출 페이지, 관리자 화면, 메일 발송까지 함수만 다섯 개였습니다. 그사이에 출석 확인의 기준이 제 사이트에서 외부 플랫폼으로 옮겨 갔고, 남은 건 피드백을 주고받는 일뿐이었습니다. 그건 이미 사람들이 모여 있는 채팅 서버에서 하는 게 나았습니다.
3절에서 미뤄 둔 이야기가 여기서 끝납니다. 발송 전용 하위 도메인을 파고 설정을 넣는 계획까지 세워 뒀지만, 로그인 자체를 걷어내면서 그 설정은 한 줄도 등록하지 않았습니다. 메일은 결국 제가 이미 쓰던 자동화 도구가 제 메일 계정으로 보내는 방식으로 갔습니다.
만들기 전에 한 번 더 물어보는 게 제일 크게 아낍니다. 이 서비스에 정말 계정이 필요한가. 저는 그 질문을 늦게 해서 함수 다섯 개를 지웠습니다.
댓글