
6버전 기준으로 작성되었습니다.
Clerk 란?
인증과 사용자 관리를 제공하는 SaaS 서비스이다.
구글, 깃허브, 페이스북, X, 노션 등..
OAuth, 이메일/비밀번호, Magic Link, Web3 Wallet 등 다양한 로그인 방식을 지원한다.
Next.js, Remix, React Native 등과 쉽게 통합을 할 수 있어서 스타트업, 1인 개발자 등 프로토타입 개발 환경에서 개발 시간을 단축시키는 데 도움을 줄 수 있다.
물론, 각 Provider 마다 어느정도 설정은 해줘야한다. 이건 Clerk 에서 못한다.
Clerk 설정



이 설정을 기본으로 하고 시작
Personal Information 의 Name 은 하는 순간 귀찮아지니 생략
저게 뭐냐면 Google 로그인 시 구글의 First Name, Last Name 을 사용하지 않고 사용자가 직접 입력하게 끔 Name 을 받는 거다.

Google 로 등록
Google Console OAuth 등록 법은 다른 블로그에 친절히 나와 있음

Client ID, Client Secret 은 구글 콘솔에 있는 것을 등록하고
미리 적혀져 있는 Authorized Redirect URI 는 구글 콘솔에 등록한다.
Authorized JavaScript origins
Authorized redirect URIs
여기에 슬래시 붙은 거랑 안 붙은 거 둘 다 붙여 넣어주자
예시
https://example.acounts.dev/
https://example.acounts.dev
localhost 도 마찬가지
NextJs 에서의 Clerk 사용법
Clerk 는 사실 되게 불친절하다. 얘네 Docs 진짜 불친절함
근데 사실 나도 되게 불친절하게 적는다. 그래서 설명이 될 수 있나 모르겠다.
필요한 것은 다섯개?다?
1. 로그인 버튼 창
2. 콜백 프론트 창
3. 콜백 백엔드 API
4. 인가에서 사용할 미들웨어
5. user 정보 불러오는 API
로그인 버튼 넣는 창 (/sign-in)
'use client';
import { useSignIn, useUser } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
export default function SignIn() {
const { SignIn } = useSignIn();
const { isLoaded, user } = useUser();
const router = useRouter();
const url = process.env.NEXT_PUBLIC_URL; // http://localhost:port or https://example (개발용, 배포용)
// isLoaded와 user 상태를 확인하여 리다이렉트
useEffect(() => {
if (isLoaded && user) {
router.push('/');
return;
}
}, [isLoaded, user, redirect, router]);
const handleGoogleSignIn = useCallback(async () => {
if (isLoaded) {
router.push('/');
}
try {
signIn?.authenticateWithRedirect({
strategy: 'oauth_google',
redirectUrl: url + '/auth/google/callback', // sign-up 하면 여기로 감
redirectUrlComplete: url + '/api/auth/google/callback', // sign-in 하면 여기로 감
});
} catch (error) {
console.error('로그인 중 에러 발생:', error);
}
}, [isLoaded, isProcessing, signIn]);
return (
<Button
onClick={handleGoogleSignIn}
>
);
}
저거 redirectUrl 들 구글에 또 추가 한다.
redirectUrl 이 프론트 콜백
redirectUrlComplete 가 백엔드 콜백이다
내가 맞게 이해한지는 모르겠는데 일단 나 따라오면 될 거다
프론트 콜백 창 (/auth/google/callback)
'use client';
import { AuthenticateWithRedirectCallback } from '@clerk/nextjs';
export default function GoogleCallback() {
return (
<AuthenticateWithRedirectCallback />
);
}
이거 하나면 됨
백엔드 콜백 창 (api/auth/google/callback)
import { getAuth } from '@clerk/nextjs/server';
import { clerkClient } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(request: NextRequest) {
try {
// 1. Clerk 에 등록 안된 사용자는 돌리기
const { userId, sessionId } = getAuth(request);
if (!userId || !sessionId) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
// 2. Clerk 에서 UserId 기반으로 User 불러오기
const client = clerkClient();
const user = await (await client).users.getUser(userId);
// 3. 등록
// 먼저 exist 확인
if (
!(await prisma.user.findUnique({
where: {
oauthId: user.id,
},
}))
) {
// Prisma로 사용자 정보 저장/업데이트
await prisma.user.create({
data: {
oauthId: user.id,
imageUrl: user.imageUrl,
oauthName: 'google',
email: user?.emailAddresses.pop()?.emailAddress || '',
firstName: user?.firstName || '',
lastName: user?.lastName || '',
},
});
}
return NextResponse.redirect(new URL('/', request.url));
} catch (error) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
}
이거 까지만 하면 안될 거다.
왜냐면 Clerk 는 ClerkMiddleware 를 강제하기 때문이다.
왜???????????????
ClerkMiddleware 안 쓰면 Jwt 해독등을 직접 구현해야한다. 근데 이게 좀 웃기다
직접 구현 시 Jwt 만 해독하고 Clerk 내에서 삭제된 유전지는 또 Clerk API 쏴서 알 수 있는 부분이라 걍 ClerkMiddleware 쓰는 게 낫다. 애초에 직접 구현 한다면 그냥 Clerk 를 안 쓰는 게 나음
밑에가 예시 코든데 걍 따라하지 마라
카카오는 Clerk 에서 지원안해줘서 카카오까지 Jwt 해독하는 코드 넣는다면 해독 코드가 엄청 길어짐
import Cookies from 'cookies'
import jwt from 'jsonwebtoken'
export default async function (req: Request, res: Response) {
// Your public key should be set as an environment variable
const publicKey = process.env.CLERK_PEM_PUBLIC_KEY
// Retrieve session token from either `__session` cookie for a same-origin request
// or from the `Authorization` header for cross-origin requests
const cookies = new Cookies(req, res)
const tokenSameOrigin = cookies.get('__session')
const tokenCrossOrigin = req.headers.authorization
if (!tokenSameOrigin && !tokenCrossOrigin) {
res.status(401).json({ error: 'Not signed in' })
return
}
try {
let decoded
const options = { algorithms: ['RS256'] } // The algorithm used to sign the token. Optional.
const permittedOrigins = ['http://localhost:3000', 'https://example.com'] // Replace with your permitted origins
if (tokenSameOrigin) {
decoded = jwt.verify(tokenSameOrigin, publicKey, options)
} else {
decoded = jwt.verify(tokenCrossOrigin, publicKey, options)
}
// Validate the token's expiration (exp) and not before (nbf) claims
const currentTime = Math.floor(Date.now() / 1000)
if (decoded.exp < currentTime || decoded.nbf > currentTime) {
throw new Error('Token is expired or not yet valid')
}
// Validate the token's authorized party (azp) claim
if (decoded.azp && !permittedOrigins.includes(decoded.azp)) {
throw new Error("Invalid 'azp' claim")
}
res.status(200).json({ sessionToken: decoded })
} catch (error) {
res.status(400).json({
error: error.message,
})
}
}
미들웨어
import { NextRequest, NextResponse } from 'next/server';
import {
clerkMiddleware,
ClerkMiddlewareAuth,
currentUser,
} from '@clerk/nextjs/server';
import logger from './lib/logger';
// 인증 필요 없는 곳 정의
const PUBLIC_PATHS = [
'/', '/sign-in'
];
// 인증 필요 한 경로인지 확인하는 함수
function isPublicRoute(pathname: string): boolean {
return PUBLIC_PATHS.some((path) => {
return (
pathname === path ||
pathname.startsWith(`${path}/`) ||
pathname.match(new RegExp(`^${path.replace(/\(.*\)/, '.*')}$`))
);
});
}
// 내부 API를 호출하여 사용자 정보 얻기
// 이거 함수 직접 불러서 못함, clerkmiddleware 자체에서 prisma 사용 불가
async function getUserInfo(oauthId: string, baseUrl: string) {
try {
// 현재 도메인의 내부 API 호출 (절대 URL 구성)
const apiUrl = new URL(
'/api/v1/userinfo/oauth/' + oauthId,
baseUrl,
).toString();
const response = await fetch(apiUrl, {
method: 'GET',
});
if (!response.ok) {
return null;
}
return await response.json();
} catch (error) {
console.error('사용자 조회 API 호출 오류:', error);
return null;
}
}
// X-User-Id 헤더를 설정하는 함수
function setUserIdHeader(req: NextRequest, userId: string) {
const requestHeaders = new Headers(req.headers);
requestHeaders.set('X-User-Id', userId.toString());
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
// 오류 시 로그인 페이지로 리다이렉트하는 함수
function redirectToSignIn(url: string, error: string) {
return NextResponse.redirect(new URL(`/sign-in?error=${error}`, url));
}
// Clerk 미들웨어
export default clerkMiddleware(async (auth, request) => {
const { userId } = await auth();
const pathname = new URL(request.url).pathname;
const originUrl = request.url;
// 1. 인증 필요없으면 넘김
if (isPublicRoute(pathname)) {
return NextResponse.next();
}
try {
// 2. Clerk 인증 처리
if (userId) {
// 내부 API를 통해 사용자 정보 조회
const baseUrl = new URL(originUrl).origin;
const user = await getUserInfo(userId.toString(), baseUrl);
if (user && user.id) {
return setUserIdHeader(request, user.id.toString());
}
}
// Kakao 가 추가 된다면 여쪽에 추가
} catch (error) {
console.error('인증 처리 오류:', error);
return redirectToSignIn(originUrl, 'auth-error');
}
});
export const config = {
matcher: [
// Next.js 내부 및 모든 정적 파일 제외
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// API 경로에 항상 실행
'/(api|trpc)(.*)',
],
};
이걸로 X-User-Id 설정하는 것 까지 다 마쳤다.
카카오 추가하면 카카오 인증 따로 주석 단 곳에 추가하면 된다
User 불러오는 거
import { NextRequest, NextResponse } from 'next/server';
import logger from '@/lib/logger';
import { getUserInfoByOauthId } from '../../service/userInfo.service';
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } },
) {
try {
if (!params.id) {
return NextResponse.json(
{ error: 'Missing X-User-Oauth-Id header' },
{ status: 400 },
);
}
return NextResponse.json(await getUserInfoByOauthId(params.id));
} catch (error) {
logger(error, 'Error getting user info');
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 },
);
}
}
저 getUserInfoByOauthId 는 간단하게 Prisma로 User 보조키(OauthId)를 이용해 User 불러오는 거다.
이러면 된다.
진짜 많이 삽질했는데 플로우가 이런 방식이다.

구글 로그인 창에서 바로 클러크에 등록하는 게 아니라 우리 사이트에서 기다려야지 등록이 된다.
쨋든 이러면 된다

6버전 기준으로 작성되었습니다.
Clerk 란?
인증과 사용자 관리를 제공하는 SaaS 서비스이다.
구글, 깃허브, 페이스북, X, 노션 등..
OAuth, 이메일/비밀번호, Magic Link, Web3 Wallet 등 다양한 로그인 방식을 지원한다.
Next.js, Remix, React Native 등과 쉽게 통합을 할 수 있어서 스타트업, 1인 개발자 등 프로토타입 개발 환경에서 개발 시간을 단축시키는 데 도움을 줄 수 있다.
물론, 각 Provider 마다 어느정도 설정은 해줘야한다. 이건 Clerk 에서 못한다.
Clerk 설정



이 설정을 기본으로 하고 시작
Personal Information 의 Name 은 하는 순간 귀찮아지니 생략
저게 뭐냐면 Google 로그인 시 구글의 First Name, Last Name 을 사용하지 않고 사용자가 직접 입력하게 끔 Name 을 받는 거다.

Google 로 등록
Google Console OAuth 등록 법은 다른 블로그에 친절히 나와 있음

Client ID, Client Secret 은 구글 콘솔에 있는 것을 등록하고
미리 적혀져 있는 Authorized Redirect URI 는 구글 콘솔에 등록한다.
Authorized JavaScript origins
Authorized redirect URIs
여기에 슬래시 붙은 거랑 안 붙은 거 둘 다 붙여 넣어주자
예시
https://example.acounts.dev/
https://example.acounts.dev
localhost 도 마찬가지
NextJs 에서의 Clerk 사용법
Clerk 는 사실 되게 불친절하다. 얘네 Docs 진짜 불친절함
근데 사실 나도 되게 불친절하게 적는다. 그래서 설명이 될 수 있나 모르겠다.
필요한 것은 다섯개?다?
1. 로그인 버튼 창
2. 콜백 프론트 창
3. 콜백 백엔드 API
4. 인가에서 사용할 미들웨어
5. user 정보 불러오는 API
로그인 버튼 넣는 창 (/sign-in)
'use client';
import { useSignIn, useUser } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
export default function SignIn() {
const { SignIn } = useSignIn();
const { isLoaded, user } = useUser();
const router = useRouter();
const url = process.env.NEXT_PUBLIC_URL; // http://localhost:port or https://example (개발용, 배포용)
// isLoaded와 user 상태를 확인하여 리다이렉트
useEffect(() => {
if (isLoaded && user) {
router.push('/');
return;
}
}, [isLoaded, user, redirect, router]);
const handleGoogleSignIn = useCallback(async () => {
if (isLoaded) {
router.push('/');
}
try {
signIn?.authenticateWithRedirect({
strategy: 'oauth_google',
redirectUrl: url + '/auth/google/callback', // sign-up 하면 여기로 감
redirectUrlComplete: url + '/api/auth/google/callback', // sign-in 하면 여기로 감
});
} catch (error) {
console.error('로그인 중 에러 발생:', error);
}
}, [isLoaded, isProcessing, signIn]);
return (
<Button
onClick={handleGoogleSignIn}
>
);
}
저거 redirectUrl 들 구글에 또 추가 한다.
redirectUrl 이 프론트 콜백
redirectUrlComplete 가 백엔드 콜백이다
내가 맞게 이해한지는 모르겠는데 일단 나 따라오면 될 거다
프론트 콜백 창 (/auth/google/callback)
'use client';
import { AuthenticateWithRedirectCallback } from '@clerk/nextjs';
export default function GoogleCallback() {
return (
<AuthenticateWithRedirectCallback />
);
}
이거 하나면 됨
백엔드 콜백 창 (api/auth/google/callback)
import { getAuth } from '@clerk/nextjs/server';
import { clerkClient } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(request: NextRequest) {
try {
// 1. Clerk 에 등록 안된 사용자는 돌리기
const { userId, sessionId } = getAuth(request);
if (!userId || !sessionId) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
// 2. Clerk 에서 UserId 기반으로 User 불러오기
const client = clerkClient();
const user = await (await client).users.getUser(userId);
// 3. 등록
// 먼저 exist 확인
if (
!(await prisma.user.findUnique({
where: {
oauthId: user.id,
},
}))
) {
// Prisma로 사용자 정보 저장/업데이트
await prisma.user.create({
data: {
oauthId: user.id,
imageUrl: user.imageUrl,
oauthName: 'google',
email: user?.emailAddresses.pop()?.emailAddress || '',
firstName: user?.firstName || '',
lastName: user?.lastName || '',
},
});
}
return NextResponse.redirect(new URL('/', request.url));
} catch (error) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
}
이거 까지만 하면 안될 거다.
왜냐면 Clerk 는 ClerkMiddleware 를 강제하기 때문이다.
왜???????????????
ClerkMiddleware 안 쓰면 Jwt 해독등을 직접 구현해야한다. 근데 이게 좀 웃기다
직접 구현 시 Jwt 만 해독하고 Clerk 내에서 삭제된 유전지는 또 Clerk API 쏴서 알 수 있는 부분이라 걍 ClerkMiddleware 쓰는 게 낫다. 애초에 직접 구현 한다면 그냥 Clerk 를 안 쓰는 게 나음
밑에가 예시 코든데 걍 따라하지 마라
카카오는 Clerk 에서 지원안해줘서 카카오까지 Jwt 해독하는 코드 넣는다면 해독 코드가 엄청 길어짐
import Cookies from 'cookies'
import jwt from 'jsonwebtoken'
export default async function (req: Request, res: Response) {
// Your public key should be set as an environment variable
const publicKey = process.env.CLERK_PEM_PUBLIC_KEY
// Retrieve session token from either `__session` cookie for a same-origin request
// or from the `Authorization` header for cross-origin requests
const cookies = new Cookies(req, res)
const tokenSameOrigin = cookies.get('__session')
const tokenCrossOrigin = req.headers.authorization
if (!tokenSameOrigin && !tokenCrossOrigin) {
res.status(401).json({ error: 'Not signed in' })
return
}
try {
let decoded
const options = { algorithms: ['RS256'] } // The algorithm used to sign the token. Optional.
const permittedOrigins = ['http://localhost:3000', 'https://example.com'] // Replace with your permitted origins
if (tokenSameOrigin) {
decoded = jwt.verify(tokenSameOrigin, publicKey, options)
} else {
decoded = jwt.verify(tokenCrossOrigin, publicKey, options)
}
// Validate the token's expiration (exp) and not before (nbf) claims
const currentTime = Math.floor(Date.now() / 1000)
if (decoded.exp < currentTime || decoded.nbf > currentTime) {
throw new Error('Token is expired or not yet valid')
}
// Validate the token's authorized party (azp) claim
if (decoded.azp && !permittedOrigins.includes(decoded.azp)) {
throw new Error("Invalid 'azp' claim")
}
res.status(200).json({ sessionToken: decoded })
} catch (error) {
res.status(400).json({
error: error.message,
})
}
}
미들웨어
import { NextRequest, NextResponse } from 'next/server';
import {
clerkMiddleware,
ClerkMiddlewareAuth,
currentUser,
} from '@clerk/nextjs/server';
import logger from './lib/logger';
// 인증 필요 없는 곳 정의
const PUBLIC_PATHS = [
'/', '/sign-in'
];
// 인증 필요 한 경로인지 확인하는 함수
function isPublicRoute(pathname: string): boolean {
return PUBLIC_PATHS.some((path) => {
return (
pathname === path ||
pathname.startsWith(`${path}/`) ||
pathname.match(new RegExp(`^${path.replace(/\(.*\)/, '.*')}$`))
);
});
}
// 내부 API를 호출하여 사용자 정보 얻기
// 이거 함수 직접 불러서 못함, clerkmiddleware 자체에서 prisma 사용 불가
async function getUserInfo(oauthId: string, baseUrl: string) {
try {
// 현재 도메인의 내부 API 호출 (절대 URL 구성)
const apiUrl = new URL(
'/api/v1/userinfo/oauth/' + oauthId,
baseUrl,
).toString();
const response = await fetch(apiUrl, {
method: 'GET',
});
if (!response.ok) {
return null;
}
return await response.json();
} catch (error) {
console.error('사용자 조회 API 호출 오류:', error);
return null;
}
}
// X-User-Id 헤더를 설정하는 함수
function setUserIdHeader(req: NextRequest, userId: string) {
const requestHeaders = new Headers(req.headers);
requestHeaders.set('X-User-Id', userId.toString());
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
// 오류 시 로그인 페이지로 리다이렉트하는 함수
function redirectToSignIn(url: string, error: string) {
return NextResponse.redirect(new URL(`/sign-in?error=${error}`, url));
}
// Clerk 미들웨어
export default clerkMiddleware(async (auth, request) => {
const { userId } = await auth();
const pathname = new URL(request.url).pathname;
const originUrl = request.url;
// 1. 인증 필요없으면 넘김
if (isPublicRoute(pathname)) {
return NextResponse.next();
}
try {
// 2. Clerk 인증 처리
if (userId) {
// 내부 API를 통해 사용자 정보 조회
const baseUrl = new URL(originUrl).origin;
const user = await getUserInfo(userId.toString(), baseUrl);
if (user && user.id) {
return setUserIdHeader(request, user.id.toString());
}
}
// Kakao 가 추가 된다면 여쪽에 추가
} catch (error) {
console.error('인증 처리 오류:', error);
return redirectToSignIn(originUrl, 'auth-error');
}
});
export const config = {
matcher: [
// Next.js 내부 및 모든 정적 파일 제외
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// API 경로에 항상 실행
'/(api|trpc)(.*)',
],
};
이걸로 X-User-Id 설정하는 것 까지 다 마쳤다.
카카오 추가하면 카카오 인증 따로 주석 단 곳에 추가하면 된다
User 불러오는 거
import { NextRequest, NextResponse } from 'next/server';
import logger from '@/lib/logger';
import { getUserInfoByOauthId } from '../../service/userInfo.service';
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } },
) {
try {
if (!params.id) {
return NextResponse.json(
{ error: 'Missing X-User-Oauth-Id header' },
{ status: 400 },
);
}
return NextResponse.json(await getUserInfoByOauthId(params.id));
} catch (error) {
logger(error, 'Error getting user info');
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 },
);
}
}
저 getUserInfoByOauthId 는 간단하게 Prisma로 User 보조키(OauthId)를 이용해 User 불러오는 거다.
이러면 된다.
진짜 많이 삽질했는데 플로우가 이런 방식이다.

구글 로그인 창에서 바로 클러크에 등록하는 게 아니라 우리 사이트에서 기다려야지 등록이 된다.
쨋든 이러면 된다