인증을 직접 구현할까 고민하다가 Clerk을 써보기로 했다. NextAuth도 생각해봤는데, 요즘 Next.js App Router랑 궁합이 애매하다는 이야기를 몇 번 들어서 패스. Firebase Auth는 Google 의존성이 더 깊어지는 게 좀 꺼림칙했고.
Clerk은 무료 플랜에서 MAU 10,000명까지 된다. 개인 프로젝트 수준에서 이걸 넘길 일은 없을 것 같아서 일단 붙여봤다.
설치와 기본 세팅
npm install @clerk/nextjs
.env.local에 키 두 개 넣으면 시작은 된다.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
App Router 기준으로 app/layout.tsx 에 ClerkProvider를 최상단에 감싸고, middleware.ts를 만들어서 어떤 라우트를 보호할지 설정한다. 공식 문서에 이 두 가지가 기본 흐름으로 잘 나와 있다.
한 가지 챙길 게 있는데, 한국 서비스라면 한국어 로컬라이제이션을 꼭 넣어줘야 한다. 기본값이 영어라서 로그인 모달 UI가 전부 영어로 나온다.
import { koKR } from '@clerk/localizations';
<ClerkProvider localization={koKR}>
이것만 추가하면 Clerk에서 제공하는 UI 전부 한국어로 바뀐다.
미들웨어 설정
처음엔 미들웨어를 너무 타이트하게 잡았다가 낭패를 봤다. 모든 라우트에 인증을 강제하면 로그인 페이지 자체가 리다이렉트 루프에 빠진다.
공개 라우트는 명시적으로 풀어줘야 한다.
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher(['/api/webhooks/clerk']);
export default clerkMiddleware(async (auth, req) => {
if (isPublicRoute(req)) return;
});
내 경우엔 플레이그라운드를 로그인 없이 둘러볼 수 있게 하고, 실제 AI 기능을 쓸 때만 로그인을 요구하는 구조로 갔다. 미들웨어에서 라우트를 막는 대신 API에서 auth()로 유저를 확인하고 401을 내려주는 식으로.
프로덕션에서 막힌 것들
로컬에서는 잘 됐는데 배포하고 나서 두 가지에서 막혔다.
DNS 설정. Clerk 프로덕션 인스턴스를 쓰려면 도메인에 CNAME 레코드를 5개 추가해야 한다. Clerk 대시보드에서 "Configure domain" 들어가면 다 나오는데, 처음엔 이게 있는지도 몰랐다. DNS 변경은 전파되는 데 좀 걸리니까 배포 일정에 여유를 두는 게 좋다.
Google OAuth 클라이언트. Clerk 개발 환경에서는 Clerk이 공유 OAuth 앱을 써서 Google 로그인이 그냥 된다. 근데 프로덕션으로 올리면 직접 Google Cloud Console에서 OAuth 클라이언트를 만들어서 Clerk에 연결해야 한다. 이걸 모르고 배포했다가 "Missing required parameter: client_id" 에러를 만났다.
Google Cloud Console에서 OAuth 2.0 클라이언트 ID 만들고, 승인된 리디렉션 URI에 Clerk이 알려주는 URL 넣어주면 된다. Clerk 대시보드 → Social Connections → Google → "Use custom credentials" 켜면 입력란이 나온다.
웹훅으로 신규 가입 처리
로그인만 하면 끝이 아니라, 신규 가입 시점에 DB에 유저를 만들어야 했다. Clerk에서 그 역할을 해주는 게 웹훅이다.
Clerk 대시보드에서 웹훅 엔드포인트를 등록하면 user.created 같은 이벤트를 받을 수 있다. svix 라이브러리로 서명 검증하고, 이벤트 타입 보고 처리하는 구조.
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
export async function POST(req: Request) {
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
const payload = await req.text();
const headers = Object.fromEntries(req.headers);
const event = wh.verify(payload, headers) as WebhookEvent;
if (event.type === 'user.created') {
await grantWelcomeTokens(event.data.id, event.data.email_addresses[0]?.email_address);
}
return new Response('ok');
}
여기서 또 삽질이 있었는데, Clerk 대시보드 기본 웹훅 URL을 promtend.com으로 넣었더니 308 Permanent Redirect가 떴다. Vercel이 promtend.com을 www.promtend.com으로 리다이렉트하는 설정이 있어서였다. www. 붙인 URL로 바꾸니까 해결됐다.
유저 상태 가져오기
클라이언트에선 useUser 훅, 서버에선 auth()를 쓴다.
// 클라이언트
const { isSignedIn, user } = useUser();
// 서버 (API Route, Server Component)
import { auth } from '@clerk/nextjs/server';
const { userId } = await auth();
UserButton은 기본 제공 UI인데, 로그아웃, 프로필 관리까지 다 들어있다. 디자인도 꽤 깔끔하게 나온다.
전반적으로 문서가 잘 돼 있어서 기본 기능은 빠르게 붙일 수 있었다. 로컬이랑 프로덕션 환경 차이에서 오는 설정 이슈가 있었는데, 알고 나면 별거 아닌 것들이라 미리 알고 시작하면 훨씬 수월할 것 같다.