Back to list
yejune

do-platform-clerk

by yejune

0🍴 0📅 Jan 15, 2026

SKILL.md


Clerk 현대 인증 플랫폼 전문가

WebAuthn, 패스키, 비밀번호 없는 인증 플로우, 사전 구축된 UI 컴포넌트, 멀티테넌트 조직 지원을 제공하는 현대 인증 플랫폼.

SDK 버전 (2025년 12월 기준):

  • @clerk/nextjs: 6.x (Core 2, Next.js 13.0.4+, React 18+ 필수)
  • @clerk/clerk-react: 5.x (Core 2, React 18+ 필수)
  • @clerk/express: 1.x
  • Node.js: 18.17.0+ 필수

빠른 참조

환경 변수:

# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

ClerkProvider 설정 (app/layout.tsx):

import { ClerkProvider } from '@clerk/nextjs'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  )
}

기본 미들웨어 (middleware.ts):

import { clerkMiddleware } from '@clerk/nextjs/server'

export default clerkMiddleware()

export const config = {
  matcher: [
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    '/(api|trpc)(.*)',
  ],
}

Context7 접근:

  • 라이브러리: /clerk/clerk-docs
  • 해결: resolve-library-id에 "clerk" 사용 후 get-library-docs 호출

구현 가이드

인증 컴포넌트를 포함한 ClerkProvider

로그인/로그아웃 컨트롤을 포함한 전체 레이아웃:

// app/layout.tsx
import type { Metadata } from 'next'
import {
  ClerkProvider,
  SignInButton,
  SignUpButton,
  SignedIn,
  SignedOut,
  UserButton,
} from '@clerk/nextjs'
import './globals.css'

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <header className="flex justify-end items-center p-4 gap-4 h-16">
            <SignedOut>
              <SignInButton />
              <SignUpButton />
            </SignedOut>
            <SignedIn>
              <UserButton />
            </SignedIn>
          </header>
          {children}
        </body>
      </html>
    </ClerkProvider>
  )
}

미들웨어로 라우트 보호

createRouteMatcher를 사용한 라우트 보호:

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isProtectedRoute = createRouteMatcher([
  '/dashboard(.*)',
  '/forum(.*)',
  '/api/private(.*)',
])

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect()
  }
})

export const config = {
  matcher: [
    '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
    '/(api|trpc)(.*)',
  ],
}

공개 라우트를 제외한 모든 라우트 보호:

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isPublicRoute = createRouteMatcher([
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/',
  '/about',
])

export default clerkMiddleware(async (auth, req) => {
  if (!isPublicRoute(req)) {
    await auth.protect()
  }
})

useAuth 훅

인증 상태 및 토큰 접근:

'use client'
import { useAuth } from '@clerk/nextjs'

export default function ExternalDataPage() {
  const { userId, sessionId, getToken, isLoaded, isSignedIn } = useAuth()

  const fetchExternalData = async () => {
    const token = await getToken()
    const response = await fetch('https://api.example.com/data', {
      headers: { Authorization: `Bearer ${token}` },
    })
    return response.json()
  }

  if (!isLoaded) return <div>Loading...</div>
  if (!isSignedIn) return <div>Sign in to view this page</div>

  return (
    <div>
      <p>User ID: {userId}</p>
      <p>Session ID: {sessionId}</p>
      <button onClick={fetchExternalData}>Fetch Data</button>
    </div>
  )
}

useUser 훅

사용자 프로필 데이터 접근:

'use client'
import { useUser } from '@clerk/nextjs'

export default function ProfilePage() {
  const { isSignedIn, user, isLoaded } = useUser()

  if (!isLoaded) return <div>Loading...</div>
  if (!isSignedIn) return <div>Sign in to view your profile</div>

  return (
    <div>
      <h1>Welcome, {user.firstName}!</h1>
      <p>Email: {user.primaryEmailAddress?.emailAddress}</p>
      <img src={user.imageUrl} alt="Profile" width={100} height={100} />
    </div>
  )
}

로그인 및 회원가입 페이지

전용 인증 페이지:

// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs'

export default function SignInPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignIn />
    </div>
  )
}
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from '@clerk/nextjs'

export default function SignUpPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignUp />
    </div>
  )
}

서버 측 인증

App Router 서버 컴포넌트:

// app/dashboard/page.tsx
import { auth, currentUser } from '@clerk/nextjs/server'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const { userId } = await auth()
  if (!userId) redirect('/sign-in')

  const user = await currentUser()

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {user?.firstName}!</p>
    </div>
  )
}

Route Handler 인증:

// app/api/user/route.ts
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'

export async function GET() {
  const { userId } = await auth()
  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
  return NextResponse.json({ userId })
}

조직 관리

OrganizationSwitcher 컴포넌트:

// app/dashboard/layout.tsx
import { OrganizationSwitcher } from '@clerk/nextjs'

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div>
      <nav className="flex items-center gap-4 p-4">
        <OrganizationSwitcher />
      </nav>
      {children}
    </div>
  )
}

useOrganizationList를 사용한 커스텀 조직 전환:

'use client'
import { useOrganizationList } from '@clerk/nextjs'

export function CustomOrganizationSwitcher() {
  const { isLoaded, setActive, userMemberships } = useOrganizationList({
    userMemberships: { infinite: true },
  })

  if (!isLoaded) return <p>Loading...</p>

  return (
    <div>
      <h2>Your Organizations</h2>
      <ul>
        {userMemberships.data?.map((membership) => (
          <li key={membership.id}>
            <span>{membership.organization.name}</span>
            <button
              onClick={() => setActive({ organization: membership.organization.id })}
            >
              Select
            </button>
          </li>
        ))}
      </ul>
      {userMemberships.hasNextPage && (
        <button onClick={() => userMemberships.fetchNext()}>Load more</button>
      )}
    </div>
  )
}

고급 패턴

Core 2 마이그레이션

환경 변수 변경:

# Core 1 (지원 종료)
CLERK_FRONTEND_API=clerk.xxx.lcl.dev
CLERK_API_KEY=sk_xxx

# Core 2 (현재)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx

authMiddleware에서 clerkMiddleware로 마이그레이션:

// Core 1 (지원 종료) - 사용 금지
import { authMiddleware } from '@clerk/nextjs'
export default authMiddleware()

// Core 2 (현재) - 이것을 사용
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)'])

export default clerkMiddleware(async (auth, request) => {
  if (!isPublicRoute(request)) {
    await auth.protect()
  }
})

서버 임포트 경로 변경:

// Core 1 (지원 종료)
import { auth } from '@clerk/nextjs'

// Core 2 (현재)
import { auth } from '@clerk/nextjs/server'

마이그레이션 도구:

npx @clerk/upgrade --from=core-1

역할 기반 접근 제어

권한으로 라우트 보호:

// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'

const isAdminRoute = createRouteMatcher(['/admin(.*)'])
const isMemberRoute = createRouteMatcher(['/dashboard(.*)'])

export default clerkMiddleware(async (auth, req) => {
  if (isAdminRoute(req)) {
    await auth.protect((has) => has({ permission: 'org:admin:access' }))
  }

  if (isMemberRoute(req)) {
    await auth.protect()
  }
})

컴포넌트에서 권한 확인:

'use client'
import { useAuth } from '@clerk/nextjs'

export function AdminPanel() {
  const { has, isLoaded } = useAuth()

  if (!isLoaded) {
    return <div>Loading...</div>
  }

  const isAdmin = has?.({ permission: 'org:admin:access' })

  if (!isAdmin) {
    return <div>Access denied</div>
  }

  return <div>Admin Panel Content</div>
}

웹훅 통합

웹훅 핸들러:

// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix'
import { headers } from 'next/headers'
import { WebhookEvent } from '@clerk/nextjs/server'

export async function POST(req: Request) {
  const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET

  if (!WEBHOOK_SECRET) {
    throw new Error('Missing CLERK_WEBHOOK_SECRET')
  }

  const headerPayload = await headers()
  const svix_id = headerPayload.get('svix-id')
  const svix_timestamp = headerPayload.get('svix-timestamp')
  const svix_signature = headerPayload.get('svix-signature')

  if (!svix_id || !svix_timestamp || !svix_signature) {
    return new Response('Missing svix headers', { status: 400 })
  }

  const payload = await req.json()
  const body = JSON.stringify(payload)

  const wh = new Webhook(WEBHOOK_SECRET)
  let evt: WebhookEvent

  try {
    evt = wh.verify(body, {
      'svix-id': svix_id,
      'svix-timestamp': svix_timestamp,
      'svix-signature': svix_signature,
    }) as WebhookEvent
  } catch (err) {
    return new Response('Invalid signature', { status: 400 })
  }

  const eventType = evt.type

  if (eventType === 'user.created') {
    const { id, email_addresses, first_name, last_name } = evt.data
    // 데이터베이스에 사용자 동기화
  }

  if (eventType === 'user.updated') {
    const { id, first_name, last_name } = evt.data
    // 데이터베이스의 사용자 업데이트
  }

  if (eventType === 'user.deleted') {
    const { id } = evt.data
    // 사용자 삭제 처리
  }

  return new Response('Webhook received', { status: 200 })
}

커스텀 인증 플로우

useSignIn을 사용한 커스텀 로그인:

'use client'
import { useSignIn } from '@clerk/nextjs'
import { useState } from 'react'

export function CustomSignIn() {
  const { signIn, isLoaded, setActive } = useSignIn()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState('')

  if (!isLoaded) {
    return <div>Loading...</div>
  }

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    setError('')

    try {
      const result = await signIn.create({
        identifier: email,
        password,
      })

      if (result.status === 'complete') {
        await setActive({ session: result.createdSessionId })
      }
    } catch (err: any) {
      setError(err.errors?.[0]?.message || 'Sign in failed')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      {error && <p className="text-red-500">{error}</p>}
      <button type="submit">Sign In</button>
    </form>
  )
}

외부 서비스 연동

Clerk와 Supabase 연동:

// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
import { auth } from '@clerk/nextjs/server'

export async function createClerkSupabaseClient() {
  const { getToken } = await auth()
  const supabaseToken = await getToken({ template: 'supabase' })

  return createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      global: {
        headers: {
          Authorization: `Bearer ${supabaseToken}`,
        },
      },
    }
  )
}

Clerk와 Convex 연동:

// app/providers.tsx
'use client'
import { ClerkProvider, useAuth } from '@clerk/nextjs'
import { ConvexProviderWithClerk } from 'convex/react-clerk'
import { ConvexReactClient } from 'convex/react'

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
        {children}
      </ConvexProviderWithClerk>
    </ClerkProvider>
  )
}

리소스

공식 문서:

연관 스킬:

  • do-platform-auth0: 엔터프라이즈 SSO 솔루션
  • do-platform-supabase: Supabase 인증 연동
  • do-platform-vercel: Vercel 배포와 Clerk 연동
  • do-lang-typescript: TypeScript 개발 패턴
  • do-domain-frontend: React 및 Next.js 연동

상태: 프로덕션 준비 완료 버전: 2.0.0 최종 업데이트: 2025-12-30 SDK 버전: @clerk/nextjs 6.x (Core 2)

Score

Total Score

50/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon