홈시리즈멘토링

© 2026 정기창. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

☕후원하기소개JSON Formatter러닝 대기질개인정보처리방침이용약관

© 2026 정기창. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

☕후원하기
소개|JSON Formatter|러닝 대기질|개인정보처리방침|이용약관

한국 SaaS 인증 스택 고르기 — 비교표에 숨은 함정 4가지와 3축 기준

정기창·2026년 6월 3일

한국에서 멀티테넌트 SaaS를 하나 만든다고 가정해보겠습니다. 회원가입 화면을 그리다 보면 어김없이 인증 스택을 무엇으로 할지 고민하게 됩니다. Supabase Auth, Clerk, NextAuth, 아니면 자체 JWT. 비교 글도 많고, 표도 잘 정리돼 있어서 처음에는 어느 쪽이 더 나은지를 고르는 단순한 문제처럼 보입니다.

그런데 그 비교표를 따라 읽다 보면 어딘가 자꾸 미끄러진다는 생각이 들었습니다. 표에는 "소셜로그인 ✅"라고 한 칸이 깔끔하게 채워져 있는데, 막상 한국에서 필수인 카카오와 네이버를 떠올리면 그 체크 표시가 그대로 믿어지지 않습니다. 본인인증은 또 어떤가요. 표의 칸 하나가 영미권의 기본값을 전제로 그려졌다는 사실은, 한국에서 실제로 붙여보기 전까지는 잘 드러나지 않습니다.

이 글은 인증 스택을 설정하는 튜토리얼이 아닙니다. 한국에서 인증 솔루션을 고를 때 비교표만 보고 넘어가면 빠지기 쉬운 함정 네 가지와, 그 함정들을 피해 결정을 내리게 해주는 세 가지 축을 정리해보려 합니다. 단정하기보다는, 어느 칸이 어떤 전제 위에 그려졌는지를 차분히 들여다보는 글에 가깝습니다.

먼저, 의사결정의 세 축

함정들을 하나씩 보기 전에, 한국에서 인증 스택을 고를 때 결정을 가르는 세 축을 먼저 세워두면 좋겠습니다. 이 세 가지를 손에 쥐고 있으면, 각 함정이 왜 함정인지가 한결 또렷하게 보입니다.

  • ① 카카오·네이버 소셜로그인이 필수인가. 국내 B2C 서비스라면 사실상 둘 다 필요한 경우가 많습니다.
  • ② 자체 백엔드(NestJS 등)를 가지고 있는가. 프론트가 인증 서비스에 직결하는 구조인지, 백엔드가 가운데에 끼는 구조인지에 따라 같은 솔루션도 전혀 다르게 동작합니다.
  • ③ 휴대폰 본인인증(PASS/NICE)이 필요한가. 실명·연령 확인이 사업 요건이라면 이 축이 선택지를 크게 좁힙니다.

아래 네 가지 함정은 모두 이 세 축 중 어딘가에서, 비교표의 칸과 한국의 현실이 어긋나며 생깁니다.

함정 1 — Supabase RLS는 자체 백엔드에서 조용히 우회된다

가장 먼저 짚고 싶은 것은 인가(authorization) 쪽입니다. Supabase를 검토하면 거의 항상 RLS(Row Level Security) 이야기가 따라옵니다. 행 단위로 접근 권한을 데이터베이스가 직접 강제해주니, 보안의 상당 부분을 DB에 맡길 수 있다는 매력적인 그림입니다.

흔한 오해는, RLS만 제대로 걸어두면 백엔드 인가는 자연히 따라온다고 여기는 것입니다. 정책을 한 번 써두면 어느 경로로 들어오든 DB가 막아주리라는 기대입니다.

실제로는 조금 다릅니다. RLS는 브라우저가 anon key로 Supabase에 직접 연결하는 경로를 1급 시민으로 가정하고 설계된 장치입니다. 그래서 NestJS 같은 자체 백엔드가 가운데 끼고, 그 백엔드가 service_role 키와 ORM(Drizzle, TypeORM 등)으로 DB에 붙으면 이야기가 달라집니다. Supabase 공식 문서가 분명히 적어두었듯, service_role로 들어온 연결에서는 RLS가 항상 우회됩니다. 정책을 아무리 촘촘히 짜두어도, 백엔드를 거친 쿼리에는 적용되지 않는 것입니다. 에러가 나는 것도 아니라서, 모르고 지나가면 "정책을 걸었으니 안전하다"는 착각만 남습니다.

RLS를 그래도 살리고 싶다면, 요청마다 트랜잭션을 열어 현재 사용자의 클레임을 주입하고 역할을 바꿔주는 래퍼 패턴이 필요합니다. Drizzle 공식 문서가 안내하는 형태도 이와 같습니다.

await db.transaction(async (tx) => {
  await tx.execute(sql`select set_config('request.jwt.claims', ${jwtClaims}, true)`);
  await tx.execute(sql`set local role authenticated`);
  // 이 트랜잭션 안의 쿼리에만 RLS가 사용자 컨텍스트로 적용됩니다
  return tx.select().from(posts);
});

여기에는 커넥션 풀 함정이 한 겹 더 붙습니다. set local 계열은 트랜잭션 스코프에서만 유효하기 때문에, "한 트랜잭션이 한 커넥션을 온전히 점유한다"는 보장이 있어야 안전합니다. 그래서 PgBouncer를 transaction 모드로 두고, prepared statement를 끄는(prepare: false) 설정이 함께 따라옵니다. 이 전제가 깨지면, 한 요청에 주입한 역할이 엉뚱한 요청에 새어 들어갈 수 있습니다.

그래서 이 상황은 "Supabase와 자체 백엔드의 궁합이 나쁘다"가 아니라, "둘이 전제하는 접근 경로가 다르다"고 보는 편이 정확합니다. RLS는 백엔드 인가의 대체재가 아니라 defense-in-depth의 한 겹입니다. service_role로 끄고 인가를 애플리케이션에 맡기든, 트랜잭션 래퍼로 RLS를 살리든, 둘 다 충분히 합리적인 선택입니다. 중요한 것은 "내가 지금 어느 경로로 DB에 붙고 있는지"를 알고 고르는 것입니다.

성능에 대한 이야기도 비슷한 결로 교정하는 편이 좋겠습니다. "RLS는 느리다"가 아니라 "잘못 짜면 느리다"가 맞습니다. 정책 안의 auth.uid()를 (select auth.uid())로 감싸 initPlan으로 캐싱하고 필요한 인덱스를 더해주면, 대형 테이블에서 100배 수준으로 빨라졌다는 보고도 있습니다. 도구의 한계라기보다, 쓰는 방식의 문제에 가깝습니다.

함정 2 — Supabase Auth는 카카오는 되는데 네이버는 (네이티브로) 안 된다

인가에서 인증으로 넘어와 봅니다. Supabase Auth는 소셜 로그인을 폭넓게 지원하고, 카카오도 그 목록에 들어 있습니다.

흔한 오해는, 카카오가 되니 네이버도 비슷하게 클릭 몇 번으로 붙겠지 하고 넘어가는 것입니다. 한국 소셜이라는 한 묶음으로 뭉뚱그려 생각하기 쉬운 지점입니다.

실제로는 둘의 위치가 다릅니다. 카카오는 Supabase Auth가 네이티브 프리셋으로 지원하지만, 네이버는 그 프리셋 목록에 없습니다. Supabase GitHub Discussions에 네이버 지원을 묻는 글이 여러 차례 올라온 것도 이 때문입니다. 카카오를 기준으로 네이버를 기대하고 들어가면, 막상 설정 화면에서 네이버 항목을 찾지 못해 당황하게 됩니다.

다만 여기서 "네이버는 아예 안 된다"고 단정하면 그것대로 시대착오가 됩니다. 2025년 Custom OAuth/OIDC Provider가 정식 출시되면서, 네이버의 authorization·token·userinfo 엔드포인트를 직접 넣어 설정 수준에서 연결할 수 있게 되었기 때문입니다. 그러니 정확한 표현은 "네이버는 안 된다"가 아니라, "네이티브 프리셋이 없어 커스텀 OIDC로 한 단계를 더 들인다"입니다. 가능하지만, 카카오만큼 매끄럽지는 않다는 정도의 무게로 받아들이면 되겠습니다.

함정 3 — Clerk의 "소셜로그인 지원"은 한국에선 절반만 사실

Clerk은 개발자 경험(DX)이 좋기로 이름난 인증 서비스입니다. 컴포넌트를 붙이면 로그인 화면이 거의 즉시 완성되고, MFA와 조직(Organizations) 기능도 잘 갖춰져 있습니다. 그래서 "이걸 쓰면 한국 소셜과 본인인증까지 한 번에 해결되지 않을까" 하는 기대를 품게 됩니다.

흔한 오해는, Clerk의 매끄러운 소셜 로그인 지원이 카카오·네이버·본인인증까지 그대로 덮어줄 것이라 여기는 것입니다. 그런데 한국 맥락에서는 이 기대가 절반쯤만 맞습니다.

먼저 소셜 쪽입니다. Clerk은 카카오와 네이버를 둘 다 네이티브로는 지원하지 않습니다(흥미롭게도 LINE은 네이티브로 지원합니다). 물론 커스텀 OAuth로 우회해 붙일 수는 있지만, 비교표의 "소셜로그인 ✅"만 보고 한국 소셜이 기본 제공된다고 읽으면 어긋납니다.

본인인증 쪽은 한 번 더 조심해서 읽어야 합니다. 휴대폰 본인인증(PASS/NICE)은 Clerk이 구조적으로 제공하지 않습니다. 반면 단순 SMS OTP, 즉 전화번호 소유 여부를 확인하는 기능은 유료로 제공됩니다. 이 둘을 한 줄로 뭉쳐 "Clerk으로 본인인증 된다/안 된다"고 말하면 틀립니다. 전화번호를 가지고 있다는 확인과, 그 번호의 주인이 실명·연령까지 검증된 본인이라는 확인은 전혀 다른 일이기 때문입니다.

몇 가지 더 갱신해두면 좋을 사실들이 있습니다.

  • 한국어 UI: ko-KR 로케일이 존재하긴 합니다. 다만 공식 README가 "en-US만 공식적으로 유지보수하고 나머지는 커뮤니티 기여"라고 밝혀두었기 때문에, 신규 기능에서 번역이 빠지거나 구버전에 머물 가능성을 염두에 둬야 합니다.
  • 무료 티어: 예전 자료에 보이는 "10K MAU"는 갱신되어, 현재는 50,000 MRU까지 무료입니다. 다만 초과분은 MRU 단위로 과금되므로, 사용자가 누적되는 멀티테넌트에서는 비용이 함께 쌓입니다.
  • 데이터 국외이전(PIPA): 이건 Clerk만의 문제가 아닙니다. 2023년 개정으로 동의 외의 근거(계약 이행에 필요한 경우 등)도 넓어졌기 때문에 "해외 SaaS를 쓰면 곧 위법"이라는 말은 사실이 아닙니다. 다만 국외이전 사실의 고지와 근거 확보는 여전히 사업자의 책임입니다. Clerk이 한국 리전에 데이터를 저장하는지는 공식적으로 확인된 바가 없어, 이 부분은 "미확인"으로 남겨두는 편이 정직합니다.

정리하면, Clerk의 강점은 진짜 강점입니다. 다만 그 강점은 글로벌 기준에서 빛나는 것이지, 한국 소셜과 본인인증의 빈칸까지 채워주지는 않습니다. "본인인증"과 "SMS OTP"를 같은 칸으로 읽지 않는 것, 이것 하나만 지켜도 Clerk을 한국에서 잘못 기대하는 일은 크게 줄어듭니다.

함정 4 — 반대 방향의 함정: NextAuth/Auth.js는 카카오·네이버를 둘 다 빌트인으로 가진다

지금까지의 함정이 "되는 줄 알았는데 한 단계 더 들더라"였다면, 마지막 함정은 방향이 반대입니다.

흔한 오해는, 해외에서 만든 라이브러리이니 한국 소셜은 오히려 더 번거로울 것이라 지레짐작하는 것입니다. 그런데 NextAuth(현재의 Auth.js)는 의외로 정반대입니다.

Auth.js는 Kakao와 Naver를 둘 다 빌트인 provider로 제공합니다. 공식 문서에 두 provider가 나란히 들어 있어서, 한국 소셜이 필수인 환경에서 마찰이 가장 적은 축에 듭니다. 앞서 본 Supabase의 네이버나 Clerk의 카카오·네이버처럼 커스텀 OAuth를 한 단계 더 얹을 필요가, 적어도 소셜 연결에서는 없습니다.

여기에 자체 백엔드(NestJS 등)를 이미 가지고 있다면 선택지가 하나 더 열립니다. 굳이 라이브러리에 묶이지 않고, 자체 JWT를 발급하면서 각 provider의 OAuth를 직접 구현하는 것도 충분히 현실적인 길입니다. 토큰의 수명과 갱신, 세션 저장 위치까지 내 손으로 쥐게 되니, 백엔드가 인증의 중심이 되는 구조에서는 오히려 깔끔할 때가 있습니다.

한 장으로 보는 비교표

네 가지 함정을 한 표로 모으면, 각 솔루션이 한국에서 어디에 서 있는지가 한눈에 들어옵니다.

항목 Supabase Auth Clerk NextAuth / Auth.js 자체 JWT
한국 소셜(카카오/네이버) 카카오 네이티브 / 네이버 커스텀 OIDC 둘 다 커스텀 OAuth 둘 다 빌트인 직접 구현
본인인증(PASS/NICE) 미제공(별도 연동) 미제공 / SMS OTP는 유료 미제공(별도 연동) 직접 연동
자체 백엔드 궁합 RLS는 우회 — 트랜잭션 래퍼 필요 백엔드와 분리 운용 백엔드 중심에 적합 백엔드가 곧 중심
과금 성격 사용량 기반 50,000 MRU 무료 후 MRU 과금 오픈소스(무료) 인프라 비용만

표를 읽을 때 한 가지만 당부하고 싶습니다. "소셜로그인 ✅" 같은 칸을 그대로 받아들이지 말고, 항상 두 경계로 쪼개어 보시기 바랍니다. 하나는 네이티브 프리셋인가, 커스텀으로 한 단계 더 드는가이고, 다른 하나는 본인인증인가, 단순 SMS OTP인가입니다. 같은 ✅라도 그 안의 무게가 다릅니다.

그래서 어떻게 고르나 — 의사결정 트리

세 축을 따라 내려가면, 대부분의 경우 선택지가 자연스럽게 좁혀집니다. 아래는 제가 머릿속으로 따라가는 순서입니다.

휴대폰 본인인증(PASS/NICE)이 필수인가?
├─ 예 → 어떤 인증 솔루션을 쓰든 본인인증은 별도 연동(NICE/PASS 등).
│        인증 솔루션은 "본인인증을 끼우기 쉬운가"로만 보고 아래로 진행.
└─ 아니오 → 아래로 진행.

카카오 + 네이버가 둘 다 필수인가?
├─ 예 ─→ 자체 백엔드(NestJS 등)가 있는가?
│         ├─ 예  → NextAuth(빌트인) 또는 자체 JWT + provider OAuth 직접 구현
│         └─ 아니오 → NextAuth(Next.js 단독) — 빌트인 provider로 마찰 최소
│
└─ 아니오(카카오만 필수, 빠른 MVP, 백엔드 코드 최소화)
          → Supabase Auth — 카카오 네이티브 + DB·RLS 통합의 이점

한국 소셜이 비필수이고 글로벌/B2B가 중심인가?
└─ 예 → Clerk — DX·MFA·조직(Organizations) 기능이 강점

요지를 세 갈래로 정리하면 이렇습니다.

  • 카카오·네이버 둘 다 필수 + 자체 백엔드 보유 → NextAuth, 또는 자체 JWT에 provider OAuth를 직접 붙이는 쪽이 마찰이 가장 적습니다.
  • 카카오만 필수 + 빠른 MVP + 백엔드 코드 최소화 → Supabase Auth가 잘 맞습니다. 카카오 네이티브에 더해 DB와 RLS를 함께 가져갈 수 있습니다.
  • 글로벌·B2B 중심 + 한국 소셜 비필수 → Clerk의 DX와 MFA, 조직 기능이 매력적입니다.

정리

돌이켜 생각해보면 이 네 가지 함정은 따로 떨어진 별개의 문제라기보다, 한 가지 사실에서 갈라져 나온 것들이었습니다. 인증 SaaS 비교표의 "✅"는 대체로 영미권의 기본값을 전제로 그려진 칸이라는 사실입니다. 카카오와 네이버, 그리고 PASS/NICE 본인인증은 그 전제의 바깥에 있는 요건이라, 한국에서 실제로 붙이려 하면 어디선가 한 칸씩 더 들게 됩니다.

그래서 비교표를 볼 때 저는 두 개의 경계를 항상 다시 확인합니다. 하나는 네이티브 프리셋이냐, 커스텀으로 한 단계 더 드느냐. 다른 하나는 실명·연령을 검증하는 본인인증이냐, 전화번호 소유만 확인하는 SMS OTP냐. 이 둘만 분리해서 읽어도 큰 오판은 대부분 피할 수 있다는 생각이 들었습니다.

결국 도구를 고를 때 먼저 물어야 할 것은 "어느 게 더 좋은가"가 아니라 "나는 지금 무엇이 필수인가"인지도 모르겠습니다. 카카오만 필요한지 네이버까지 필요한지, 본인인증이 사업 요건인지 아닌지, 백엔드가 인증의 중심인지 아닌지. 이 세 질문에 먼저 답하고 나면, 비교표의 칸들이 비로소 제 무게로 읽히기 시작합니다.

SupabaseClerkNextAuth인증소셜로그인카카오로그인네이버로그인RLS

관련 글

SaaS 블로그 플랫폼을 Coolify로 배포하며 마주친 실전 문제들

글력(SaaS 블로그 플랫폼)을 Coolify에 처음 배포하면서 겪은 메모리 최적화, OG 이미지 문제, 봇 스캔 방어, Redis 연결, CORS까지. 실전에서 하나씩 해결한 과정을 정리했습니다.

관련도 88%

NestJS에서 Drizzle ORM을 선택한 이유: TypeORM, Prisma와의 비교

새로운 SaaS 모듈에 MySQL을 도입하면서 TypeORM, Prisma, Drizzle ORM을 비교했습니다. 각 ORM의 장단점과 Drizzle을 선택한 이유를 실제 코드 예시와 함께 정리했습니다.

관련도 87%

개발도 결국은 상품을 만드는 일 — 기술 스택 선택이 납품과 유지보수를 결정한다

사이드 프로젝트의 모던 스택을 클라이언트에게 납품하려 하니, 기술 선택이 곧 유지보수 비용과 클라이언트 경험을 좌우한다는 걸 깨달았습니다. 개발자가 시장과 목적에 맞는 기술 스택을 선택해야 하는 이유를 정리했습니다.

관련도 87%