리액트

FormTube - 헬스 자세 코칭 동영상 사이트 개발 과정 정리 - 1 - 기본 인증 구현 과 레이아웃 setting

코딩질문자 2025. 6. 3. 21:06
728x90

아래 부터 개발 과정을 개략적으로 복습차 정리한 글입니다.


1. Clerk 통합하기 (Integrate Clerk)

  1. 패키지 설치
    Next.js 프로젝트 루트에서 Clerk SDK를 설치합니다.
     
    npm install @clerk/nextjs @clerk/nextjs/server # 또는 yarn add @clerk/nextjs @clerk/nextjs/server
  2. 환경 변수 등록
    Clerk 대시보드(https://dashboard.clerk.com → API Keys)에서 “Publishable Key”와 “Secret Key”를 복사한 뒤, 로컬 개발 환경에서는 프로젝트 최상위에 .env.local 파일을 만들어 다음과 같이 추가합니다:
    • 주의:
      • 브라우저(클라이언트)에서 사용하려면 반드시 NEXT_PUBLIC_ 접두사가 붙어야 합니다.
      • .env.local을 수정한 뒤에는 npm run dev 또는 yarn dev로 서버를 재시작해야 변경 내용이 반영됩니다.
  3. env
     
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_여기에_복사한_값 
CLERK_API_KEY=sk_live_여기에_복사한_값 
CLERK_JWT_KEY=jwt_live_여기에_복사한_값 # (선택사항: JWT 사용 시)

 

  1. 최상위 레이아웃에 <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)

  1. 표준 로그인 모달 띄우기 (<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)

  1. 로그인된 사용자를 나타낼 프로필 버튼
    • 사용자가 정상적으로 로그인된 상태에서는, 화면 우측 상단이나 사이드바 등 원하는 위치에 <UserButton /> 컴포넌트를 추가합니다.
    • <UserButton />을 누르면 드롭다운이 뜨면서 “My profile”, “Studio”, “Manage account”, “Sign out” 같은 메뉴를 자동으로 제공합니다.
  2. 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)

  1. middleware.ts 파일 생성
    Next.js 앱 루트(일반적으로 app/ 폴더 바로 위)에 middleware.ts(또는 middleware.js) 파일을 만듭니다.
  2. 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)

  1. 로그인 상태 기반 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)

  1. 미들웨어에서 인증 강제하기 (이미 4번에서 설정)
    • /protected 경로(또는 /upload 등)로 들어오는 모든 요청은 middleware.ts에서 isProtectedRoute(req)가 true로 판단하며,
      → await auth.protect() 호출
      → 미인증 시 Clerk 로그인 화면(또는 /sign-in)으로 자동 리다이렉트
      → 인증 후 다시 원래 요청하던 페이지를 로드
  2. 페이지 레벨에서 인증 검사하기
    • 때로는 미들웨어가 아니라 “해당 페이지 컴포넌트 자체에서 인증된 유저만 접근할 수 있도록” 제어하고 싶을 수 있습니다.
    • 이럴 때는 페이지 내부에서 Clerk 훅(hook)을 사용해 인증 여부를 검사하고, 없으면 다른 페이지로 리다이렉트하거나 “Loading / Unauthorized” 화면을 띄웁니다.
    예시: app/protected/page.tsx
"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)

위에서 다룬 모든 단계를 순서대로 정리하면 다음과 같습니다:

  1. Clerk 설치 & 환경 변수 등록
    • npm install @clerk/nextjs @clerk/nextjs/server
    • .env.local에 NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_API_KEY 등 추가
    • Clerk 대시보드에서 키를 복사해 정확히 붙여넣기
  2. <ClerkProvider>로 전역 초기화
    • app/layout.tsx 또는 pages/_app.tsx에 <ClerkProvider publishableKey={…}> 감싸기
  3. 로그인 버튼 & 화면 구성 (Sign In Screens)
    • <SignInButton mode="modal">…</SignInButton>을 활용해 모달 로그인 추가
    • 또는 app/sign-in/page.tsx에 <SignIn /> 컴포넌트를 직접 렌더링
  4. 로그인 후 프로필 버튼 (UserButton) 추가
    • <SignedIn><UserButton /></SignedIn>
    • <SignedOut><SignInButton mode="modal">…</SignInButton></SignedOut>
    • 사이드바/헤더 등 원하는 위치에 배치하여 로그인 상태에 따른 UI 분기 처리
  5. 미들웨어 설정 (Add middleware)
    • middleware.ts 생성
    • createRouteMatcher(["/protected", "/upload", …]) 같은 보호할 경로 정의
    • export default clerkMiddleware(async (auth, req) => { if (isProtectedRoute(req)) await auth.protect(); });
    • config.matcher에 Next.js 내부 파일/정적 리소스 제외 및 API 경로 포함
  6. 클라이언트 측 & API 측 보호 로직 추가 (Protect routes)
    • (이미 미들웨어에서 /protected 경로를 보호하므로 이 단계만으로 충분)
    • 필요시 페이지 컴포넌트 내부(useAuth 사용)에서 직접 로그인 여부 체크 후 리다이렉트
    • API 핸들러(getAuth(req))에서 userId가 없으면 401 처리

 

 


저장소 url: https://github.com/goodsosbva/FormTube

728x90