리액트
FormTube - 헬스 자세 코칭 동영상 사이트 개발 과정 정리 - 1 - 기본 인증 구현 과 레이아웃 setting
코딩질문자
2025. 6. 3. 21:06
728x90
아래 부터 개발 과정을 개략적으로 복습차 정리한 글입니다.
1. Clerk 통합하기 (Integrate Clerk)
- 패키지 설치
Next.js 프로젝트 루트에서 Clerk SDK를 설치합니다.npm install @clerk/nextjs @clerk/nextjs/server # 또는 yarn add @clerk/nextjs @clerk/nextjs/server
- 환경 변수 등록
Clerk 대시보드(https://dashboard.clerk.com → API Keys)에서 “Publishable Key”와 “Secret Key”를 복사한 뒤, 로컬 개발 환경에서는 프로젝트 최상위에 .env.local 파일을 만들어 다음과 같이 추가합니다:- 주의:
- 브라우저(클라이언트)에서 사용하려면 반드시 NEXT_PUBLIC_ 접두사가 붙어야 합니다.
- .env.local을 수정한 뒤에는 npm run dev 또는 yarn dev로 서버를 재시작해야 변경 내용이 반영됩니다.
- 주의:
-
env
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_여기에_복사한_값
CLERK_API_KEY=sk_live_여기에_복사한_값
CLERK_JWT_KEY=jwt_live_여기에_복사한_값 # (선택사항: JWT 사용 시)
- 최상위 레이아웃에 <ClerkProvider> 추가
Next.js App Router (app/ 디렉터리)를 사용하는 경우, app/layout.tsx(또는 App Router가 아닌 Pages Router 환경에서는 pages/_app.tsx)에 ClerkProvider를 감싸서 전역적으로 Clerk를 초기화- publishableKey 프로퍼티에 반드시 process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY 값을 넘겨야 합니다.
- TypeScript 환경에서는 !를 붙여 “절대 undefined가 아니다”라고 선언할 수 있지만, 실제로 환경 변수가 제대로 설정되지 않으면 런타임에 에러가 발생하므로 반드시 .env.local을 확인해야 합니다.
// app/layout.tsx
"use client";
import { ClerkProvider } from "@clerk/nextjs";
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<ClerkProvider publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY!}>
{children}
</ClerkProvider>
</body>
</html>
);
}
2. Sign-in 화면 추가하기 (Add Sign in screens)
- 표준 로그인 모달 띄우기 (<SignInButton> 활용)
- 로그인되지 않은 상태에서 버튼을 클릭하면 Clerk가 제공하는 모달형 로그인 화면이 나타나도록 할 수 있습니다.
- 예를 들어, 화면 아무 곳에나 다음 코드를 배
"use client";
import { SignInButton } from "@clerk/nextjs";
import { Button } from "@/components/ui/button";
import { UserCircleIcon } from "lucide-react";
export function SignInScreenButton() {
return (
<SignInButton mode="modal">
<Button variant="outline" className="px-4 py-2 text-sm font-medium text-blue-600 border-blue-500/20 rounded-full shadow-none">
<UserCircleIcon className="mr-1" />
Sign in
</Button>
</SignInButton>
);
}
- 중요
- mode="modal" 속성을 반드시 mode라는 이름으로 넘겨야 합니다(modal="modal"이 아님).
- 버튼을 클릭하면 자동으로 “Sign in to YourApp” 모달이 뜨고, Google OAuth나 이메일 로그인 등을 제공해 줍니다.
2. 커스텀 로그인 페이지 만들기 (<SignIn> 컴포넌트 단독 사용)
- Clerk가 기본으로 제공하는 컴포넌트를 직접 렌더링해서 자체 페이지/레이아웃을 꾸밀 수도 있습니다.
- 예시: app/sign-in/page.tsx에 다음 코드를 추가하면 “/sign-in” 경로에서 커스텀 로그인 화면을 렌더링합니다
// app/sign-in/page.tsx
"use client";
import { SignIn } from "@clerk/nextjs";
export default function SignInPage() {
return (
<div className="flex justify-center items-center h-screen bg-gray-50">
<div className="w-full max-w-md p-6 bg-white rounded-lg shadow-md">
<SignIn path="/sign-in" routing="path" signUpUrl="/sign-up" />
</div>
</div>
);
}
3. UserButton 추가하기 (Add UserButton)
- 로그인된 사용자를 나타낼 프로필 버튼
- 사용자가 정상적으로 로그인된 상태에서는, 화면 우측 상단이나 사이드바 등 원하는 위치에 <UserButton /> 컴포넌트를 추가합니다.
- <UserButton />을 누르면 드롭다운이 뜨면서 “My profile”, “Studio”, “Manage account”, “Sign out” 같은 메뉴를 자동으로 제공합니다.
- SignedIn / SignedOut 컴포넌트로 분기 처리
- Clerk가 제공하는 <SignedIn>과 <SignedOut> 래퍼를 이용해 “로그인 상태”와 “로그아웃 상태”를 나누어 렌더링할 수 있습니다.
- 예시: 사이드바(Component 이름: SidebarAuthSection.tsx)
"use client";
import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/nextjs";
import { Button } from "@/components/ui/button";
export function SidebarAuthSection() {
return (
<div className="flex items-center space-x-2">
{/* 로그아웃 상태: Sign In 버튼 노출 */}
<SignedOut>
<SignInButton mode="modal">
<Button variant="outline" size="sm">
Sign In
</Button>
</SignInButton>
</SignedOut>
{/* 로그인 상태: UserButton (프로필 드롭다운) 노출 */}
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
</div>
);
}
4. 미들웨어 추가하기 (Add middleware)
- middleware.ts 파일 생성
Next.js 앱 루트(일반적으로 app/ 폴더 바로 위)에 middleware.ts(또는 middleware.js) 파일을 만듭니다. - Clerk 미들웨어와 라우트 매처 설정
미들웨어 안에서 특정 경로만 인증을 강제(protect)하려면 createRouteMatcher를 써서 보호할 경로 패턴을 정의하고, clerkMiddleware 콜백 안에서 auth.protect()를 호출합니다.
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
// 보호할 경로를 배열에 나열 ("/protected" 하위 모든 URL 포함)
const isProtectedRoute = createRouteMatcher(["/protected"]);
export default clerkMiddleware(async (auth, req) => {
// 요청 URL이 "/protected"로 시작하면 인증(protect) 처리
if (isProtectedRoute(req)) {
await auth.protect();
}
// 이외의 경로라면 (예: "/about", "/public-page" 등) 그냥 넘어감
});
export const config = {
matcher: [
// 1) Next.js 내부 파일과 정적 리소스는 미들웨어 실행 대상에서 제외
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
// 2) API 또는 TRPC 엔드포인트는 항상 Clerk 세션 파싱만 수행 (추가 보호는 따로 코드에서)
"/(api|trpc)(.*)",
],
};
- reateRouteMatcher(["/protected"])
- /protected, /protected/anything, /protected/foo/bar 등을 모두 true로 매칭.
- auth.protect()
- 현재 세션에 유효한 로그인 정보가 없으면 자동으로 Clerk 로그인 흐름 페이지로 리다이렉트.
- 이미 로그인된 상태라면 미들웨어가 다음 단계(페이지 렌더링 혹은 API 핸들러)로 진행.
- config.matcher 배열
- 첫 번째 정규식: _next 폴더 내부 파일이나 .css, .js, 이미지, 폰트 같은 정적 리소스를 제외하고 모든 일반 페이지에 미들웨어를 적용.
- 두 번째 항목: /api/… 또는 /trpc/…로 시작하는 경로에도 미들웨어가 적용되도록 설정.
5. 사이드바(또는 레이아웃)에서 인증 상태 사용하기 (Use auth state on sidebar sections)
- 로그인 상태 기반 UI 분기
- 이미 앞서 살펴본 <SignedIn> / <SignedOut> 컴포넌트를 통해 사이드바 메뉴나 헤더, 네비게이션 바에서 로그인 여부에 따라 보여줄 항목을 나눌 수 있습니다.
- 예를 들어, 사이드바에 “Upload” 메뉴를 넣고 싶다면, 업로드 페이지(/upload)를 보호(protected)하면서, 로그인 상태에서만 “Upload” 메뉴를 보여주도록 다음과 같이 구현할 수 있습니다
// components/Sidebar.tsx
"use client";
import Link from "next/link";
import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/nextjs";
import { createRouteMatcher } from "@clerk/nextjs/server";
import { usePathname } from "next/navigation";
export default function Sidebar() {
const pathname = usePathname();
const isUploadRoute = pathname?.startsWith("/upload");
return (
<aside className="w-60 h-screen bg-gray-800 text-white p-4">
<nav className="flex flex-col space-y-2">
<Link href="/">Home</Link>
<Link href="/about">About</Link>
{/* 로그인 상태: "Upload" 메뉴 노출 */}
<SignedIn>
<Link href="/upload">Upload</Link>
</SignedIn>
{/* 비로그인 상태: "Upload" 메뉴 대체 UI (예: 로그인 유도) */}
<SignedOut>
<SignInButton mode="modal">
<button className="text-sm text-blue-400 underline">Login to Upload</button>
</SignInButton>
</SignedOut>
</nav>
{/* 사이드바 하단에 사용자 정보 / 로그인 버튼 */}
<div className="mt-auto flex items-center space-x-2">
<SignedOut>
<SignInButton mode="modal">
<button className="px-3 py-1 border border-blue-400 rounded">Sign In</button>
</SignInButton>
</SignedOut>
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
</div>
</aside>
);
}
2. 클라이언트 측 인증 상태 조회 (추가 예시)
- 만약 더 세밀하게 “현재 유저가 로그인되어 있는지”를 코드 로직 상에서 직접 확인하고 싶다면, Clerk의 React 훅(hook)인 useAuth 등을 사용할 수 있습니다
"use client";
import { useAuth } from "@clerk/nextjs";
export function SomeComponent() {
const { isSignedIn, userId, user } = useAuth();
if (isSignedIn) {
return <p>환영합니다, {user?.fullName ?? "User"}님!</p>;
} else {
return <p>로그인이 필요합니다.</p>;
}
}
- 위처럼 useAuth를 쓰면 isSignedIn: boolean, userId: string | null, user: User | null 등을 받아와서 자유롭게 분기 로직 처리 가능.
6. 보호된 라우트 설정하기 (Protect routes)
- 미들웨어에서 인증 강제하기 (이미 4번에서 설정)
- /protected 경로(또는 /upload 등)로 들어오는 모든 요청은 middleware.ts에서 isProtectedRoute(req)가 true로 판단하며,
→ await auth.protect() 호출
→ 미인증 시 Clerk 로그인 화면(또는 /sign-in)으로 자동 리다이렉트
→ 인증 후 다시 원래 요청하던 페이지를 로드
- /protected 경로(또는 /upload 등)로 들어오는 모든 요청은 middleware.ts에서 isProtectedRoute(req)가 true로 판단하며,
- 페이지 레벨에서 인증 검사하기
- 때로는 미들웨어가 아니라 “해당 페이지 컴포넌트 자체에서 인증된 유저만 접근할 수 있도록” 제어하고 싶을 수 있습니다.
- 이럴 때는 페이지 내부에서 Clerk 훅(hook)을 사용해 인증 여부를 검사하고, 없으면 다른 페이지로 리다이렉트하거나 “Loading / Unauthorized” 화면을 띄웁니다.
"use client";
import { useAuth, UserButton } from "@clerk/nextjs";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function ProtectedPage() {
const { isSignedIn } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isSignedIn) {
// 로그인되지 않았다면 sign-in 페이지로 강제 이동
router.replace("/sign-in");
}
}, [isSignedIn, router]);
if (!isSignedIn) {
return <p className="text-center mt-20">접근 권한이 없습니다. 로그인 중...</p>;
}
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">보호된 페이지</h1>
<p>여기는 로그인된 사용자만 볼 수 있는 콘텐츠입니다.</p>
<UserButton afterSignOutUrl="/" />
</div>
);
}
3. API 엔드포인트에서 인증 검사하기
- API 라우트(또는 TRPC 핸들러)에서도 “로그인된 유저만 호출”하도록 인증을 체크해야 할 때가 있습니다.
- 예를 들어 pages/api/protected.ts에서
// pages/api/protected.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { getAuth } from "@clerk/nextjs/server";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { userId } = getAuth(req);
if (!userId) {
// 세션이 없으면 401 Unauthorized 반환
return res.status(401).json({ message: "로그인 필요" });
}
// 유효한 로그인 세션이 있을 때만 이하 로직 수행
res.status(200).json({ message: `안녕하세요 ${userId}님!` });
}
- TRPC를 사용할 경우에도 비슷하게 ctx.auth.userId 또는 ctx.auth.isSignedIn을 체크해서 호출을 제어할 수 있습니다.
전체 플로우 요약 (Checklist)
위에서 다룬 모든 단계를 순서대로 정리하면 다음과 같습니다:
- Clerk 설치 & 환경 변수 등록
- npm install @clerk/nextjs @clerk/nextjs/server
- .env.local에 NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_API_KEY 등 추가
- Clerk 대시보드에서 키를 복사해 정확히 붙여넣기
- <ClerkProvider>로 전역 초기화
- app/layout.tsx 또는 pages/_app.tsx에 <ClerkProvider publishableKey={…}> 감싸기
- 로그인 버튼 & 화면 구성 (Sign In Screens)
- <SignInButton mode="modal">…</SignInButton>을 활용해 모달 로그인 추가
- 또는 app/sign-in/page.tsx에 <SignIn /> 컴포넌트를 직접 렌더링
- 로그인 후 프로필 버튼 (UserButton) 추가
- <SignedIn><UserButton /></SignedIn>
- <SignedOut><SignInButton mode="modal">…</SignInButton></SignedOut>
- 사이드바/헤더 등 원하는 위치에 배치하여 로그인 상태에 따른 UI 분기 처리
- 미들웨어 설정 (Add middleware)
- middleware.ts 생성
- createRouteMatcher(["/protected", "/upload", …]) 같은 보호할 경로 정의
- export default clerkMiddleware(async (auth, req) => { if (isProtectedRoute(req)) await auth.protect(); });
- config.matcher에 Next.js 내부 파일/정적 리소스 제외 및 API 경로 포함
- 클라이언트 측 & API 측 보호 로직 추가 (Protect routes)
- (이미 미들웨어에서 /protected 경로를 보호하므로 이 단계만으로 충분)
- 필요시 페이지 컴포넌트 내부(useAuth 사용)에서 직접 로그인 여부 체크 후 리다이렉트
- API 핸들러(getAuth(req))에서 userId가 없으면 401 처리
728x90