회원가입을 쉽게 만들어주는 라이브러리 Clerk

  1. 설치
npm install @clerk/nextjs
  1. env key발급 및 설정
    • clerk.com을 회원가입하고 프로젝트용 key를 발급받자
    • root경로에 `.env’파일 생성
    • gitignore에 .env 추가 잊지 말자 - env key 정보 업데이트
  2. 프로젝트에 Import
    • root/layout.tsx에 import할 수도 있지만, MSA를 위해서 별도의 파일에 설정해보자
//{root}/(platform)/layout.tsx

import { ClerkProvider } from "@clerk/nextjs";

const PlatformLayout = ({
    children
}: {
    children: React.ReactNode;
}) => {
    return (

        <ClerkProvider>
            {children}
        </ClerkProvider>
    )
})

비고. Clerk은 현지화 옵션도 제공한다.

import { ClerkProvider } from "@clerk/nextjs";

//npm install @clerk/localizations 이후
import { koKR } from "@clerk/localizations";

const PlatformLayout = ({ children }: { children: React.ReactNode }) => {
  return <ClerkProvider localization={koKR}>{children}</ClerkProvider>;
};

export default PlatformLayout;

  1. middleware 설정
// 사용자 인증 및 관리를 위한 미들웨어 설정
import { authMiddleware } from "@clerk/nextjs";

// This example protects all routes including api/trpc routes
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
export default authMiddleware({});

export const config = {
  matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};

비고. 정규표현식의 해석

  • .: 어떤 문자든지 일치시킵니다(줄바꿈 문자 제외).
  • .+: 하나 이상의 어떤 문자를 일치시킵니다.
  • \\.: 실제 점(.) 문자를 일치시킵니다.
  • [\\w]+: 하나 이상의 단어 문자(알파벳, 숫자, 밑줄)를 일치시킵니다.
  • $: 문자열의 끝을 나타냅니다.
  • |: OR 연산자로, 이전 패턴 또는 다음 패턴을 일치시킵니다.
  • _next: 실제 _next 문자열을 일치시킵니다.
  • (?!...): 부정 전방 탐색으로, 괄호 안의 패턴을 만족하지 않는 위치를 찾습니다.
  • .*: 어떤 문자든지 0번 이상 반복되는 패턴을 일치시킵니다.

=> ({하나이상의문자}.{하나이상의단어문자} [즉, 파일 확장자] 또는 _next`로 시작하는 문자를 제외한 단어, “/”, “api or trpc 로 시작하는 모든 라우트에 미들웨어를 적용”

  1. 버튼 생성
  2. Custom sign-up sign-in page setup

    https://clerk.com/docs/references/nextjs/custom-signup-signin-pages

// app/(platform)/(clerk)/sign-in/[[...sign-in]]/page.tsx

import { SignIn } from "@clerk/nextjs";
 
export default function Page() {
  return <SignIn />;
}

// sign-up도 동일하게 설정하면된다.

비고2. [[…sign-in ]] 폴더 명에 대해서

  1. […] 을포함한 폴더명은 모든 하위 경로를 포함하는 라우트를 지정하는 폴더 명이다.

  2. …는 catch all라우트 를 뜻하는 의미로, 모든 하위경로를 포함한다는 의미이다.
  3. [[…]] 은 Optional Catch-all Segement로 […] 와 동일한 기능을 가지지만 하위경로가 없어도 된다 (하위 경로가 없다면 router.query.id값은 undefined로 정의된다.

getServerSideProps, getStaticProps, getInitialProps 또는 페이지 컴포넌트 자체에서 router.query를 통해 하위경로 값을 가져올 수 있다.

  • 예를 들어, /app/org/1/2/3 경로로 접근하면, router.query.organization['1', '2', '3']이 출력된다.
  1. .env 업데이트
...env keys
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/
  1. 접속 시도
//middleware.ts
export default authMiddleware({
  publicRoutes: ["/"],
});

auth, CurrentUser사용해보기

// app/(Platform)/protected
import { auth, currentUser } from "@clerk/nextjs";

const ProtectedPage = async () => {
  const user = await currentUser(); // 사용자 정보를 반환
  const { userId } = auth(); //사용자의 ID키를 반환

  return (
    <div>
      User: {user?.firstName}
      userId: {userId}
    </div>
  );
};

export default ProtectedPage;

// ver.2
"use client";

import { useAuth, useUser } from "@clerk/nextjs";

const ProtectedPage = () => {
  const { userId } = useAuth();
  const { user } = useUser();
  return (
    <div>
      User: {user?.firstName}
      userId: {userId}
    </div>
  );
};

export default ProtectedPage;

User 버튼 만들기 예제

"use client";

import { UserButton } from "@clerk/nextjs";

const ProtectedPage = () => {
  return (
    <div>
      <UserButton afterSignOutUrl="/" />
    </div>
  );
};

export default ProtectedPage;

Organization 만들기

//app/(platform)/(clerk)/select-org/[[...select-org]]

import { OrganizationList } from "@clerk/nextjs";

export default function CreateOrganizationPage() {
  return (
    <OrganizationList
      hidePersonal // 개인 설정 버튼 제거
      afterSelectOrganizationUrl="/organization/:id" // 선택후 이동
      afterCreateOrganizationUrl="/organization/:id" // 생성후 이동
    />
  );
}


// (platform)/(dashboard)/organization/[organizationId]
import { OrganizationSwitcher, auth } from "@clerk/nextjs";

const OrganizationIdPage = () => {
  //const { userId, orgId } = auth();을통해 orgId도 확인 가능하다.
  return (
    <div>
      // 대시보드 내용
      <OrganizationSwitcher hidePersonal />
    </div>
  );
};

export default OrganizationIdPage;

메인페이지에 접속했을때 Orgpage로 Redirect 해줘야하는 경우 (Clerk 사용시)

//middleware.ts
import { authMiddleware } from "@clerk/nextjs";

export default authMiddleware({
  publicRoutes: ["/"],
  afterAuth(auth, req) { //clerk에서 기본으로 제공해주는 함수
    // 예외 1. 로그인한상태로 Public root에 접속 했을때
    if (auth,userId && auth.isPublicRoute) {
      let path = "/select-org";

      if (auth.orgId) { // 그리고 사용자가 orgID가 존재하면 (회사 그룹에 속해있으면)
        path= `/organization/${auth.orgId}` //경로 수정
      }

      const orgSelection = new URL(path, req.url //새로운 path로 url을 변경해서 
      return NextResponse.redirect(orgSelection); // redirect시킨다.
    }
    // 예외2. 로그인 하지 않은 상태로 다른 경로에 접속했을때 (이를 설정하지않으면 404error 페이지로 이동된다.)
    if (!auth.userId && !auth.isPublicRoute) {
      return redirectToSignIn({ returnBackUrl: req.url });
    }
	// 예외3. 로그인 했지만 그룹에 속해있지 않으면서, 
    if (auth.userId && !auth.orgId && req.nextUrl.pathname !== "/select-org") {
      const orgSelection = new URL("/select-org", req.url);
      return NextResponse.redirect(orgSelection);
    }
      
  }
});

URL을 통한 회사 변경 기능 구현

// org상세 페이지를 정의한 app/(platform)/(dashboard)/organization/[organizationId]/layout.tsx

import { OrgControl } from "./_components/org-control";

const OrganizationIdLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <>
      <OrgControl /> // 회사변경 로직을 정의한 useEffect를 정의한 컴포넌트 파일
      {children}
    </>
  );
};

export default OrganizationIdLayout;

// _components/org-control.tsx

// CSR을 위한 컴포넌트 정의 지시어
"use client";

import { useEffect } from "react";
import { useParams } from "next/navigation";
import { useOrganizationList } from "@clerk/nextjs";

export const OrgControl = () => {
  const params = useParams(); //파라미터의 회사Id 추출
  const { setActive } = useOrganizationList(); // clerk의 활성화 정의 함수

  useEffect(() => {
    if (!setActive) return; //정의된 회사가 없으면 pass

    setActive({
      organization: params.organizationId as string, //활성 회사를 현 Url회사로 변경
    });
  }, [setActive, params.organizationId]);
};

비고."use client"파일에 정의하면 하위 구성 요소를 포함하여 모든요소를 클라이언트 컴포넌트로 간주한다.