Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ Feat ] 인증 시스템 초기 세팅 및 login api 적용 #178

Closed
wants to merge 7 commits into from

Conversation

ptyoiy
Copy link
Contributor

@ptyoiy ptyoiy commented Nov 2, 2024

#️⃣ Related Issue

Closes #175

✅ Done Task

  • NextAuth.js 세팅
  • 미들웨어 구성
  • 세션 데이터 구성
  • 로그인 api

☀️ New-insight

자세한 코드 설명은 PR Point에서 진행하고, New-insight는 개념 정리 위주로 작성합니다.

  1. NextAuth.js
    next 공식에서도 추천하는 인증/인가 라이브러리입니다. 대체제로는 lucia라는 라이브러리가 있습니다. lucia가 좀 더 경량으로 구성돼 있고 인증 커스터마이징 유연성이 더 높은 대신, 초기 설정은 nextAuth가 더 간결하다고 합니다.

    인증 로직을 클라이언트에서 react-query, local storage 등을 사용하여 처음부터 구현하는 대신 NextAuth를 사용하여 로그인, 회원가입, 세션 관리, OAuth 등을 쉽게 설정할 수 있습니다. 특히 인증/인가 관련 코드를 프론트에서 분리하여 server action으로 관리할 수 있어 손쉬운 구현, 보안, 유지 보수와 확장성에 용이하다는 장점이 있습니다.

    세션 데이터 관리 방식은 /src/auth.ts에서 session: { strategy: "jwt" | "database" } 로 정할 수 있는데,
    jwt를 선택하면 세션 데이터를 암호화된 jwt로 만들어서 클라이언트의 쿠키에 저장합니다. 클라이언트에서 세션 데이터를 얻으려면 이 쿠키에 저장된 세션 토큰을 서버로 보내 검증 및 해석하여 데이터를 반환 받는 방식입니다. 이 때 토큰의 암호화를 위해 .envAUTH_SECRET이 있어야 하는데 npx auth secret 명령어를 통해 자동으로 생성 가능합니다.

    database를 선택하면 세션 데이터를 데이터베이스에 저장하고 관리합니다. jwt때와는 달리 클라이언트의 쿠키에는 세션 ID만 저장합니다. 클라이언트에서 세션 데이터를 얻으려면 이 세션 ID를 next 서버로 보내고, DB에서 ID를 조회하여 데이터를 반환 받는 방식입니다.

    NextAuth 사용 시 인증 흐름도는 보편적으로 다음과 같습니다.

    1. 클라이언트의 로그인 요청
    2. 요청이 /api/auth/[...nextauth]라는 API 라우트로 전달
    3. server action 실행 (백엔드 서버와 통신)
    4. 인증 성공 및 세션 생성
      nextAuth가 세션 데이터를 jwt로 생성하고 클라이언트의 쿠키에 저장 (세션 데이터에는 로그인 정보, 유저 정보, 토큰 등등 아무거나 넣을 수 있으며, 아래 세션 데이터 얻는 방법을 통해 컴포넌트에서 사용할 수 있음)
    5. 클라이언트로 결과 반환
      정해진 경로로 리다이렉트 및 세션 데이터 확인 가능

    인가를 받을 때는 서버/클라이언트 컴포넌트에 따라 다른 방식으로 session 데이터를 얻어와 그 안의 accessToken을 추출하여 await kyInstance.get("api/user", { headers: { Authorization: Bearer ${accessToken}, }, })처럼 사용하면 됩니다.

    • 서버 컴포넌트 (await auth())
    export default async function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      const session = await auth(); // import { auth } from "@/auth"; 사용
      return (
        <html lang="ko">
        ...
    • 클라이언트 컴포넌트 (useSession)
      useSession을 사용하려면 RootLayout에서 SessionProvider로 감싸야 합니다.
    const UserDashboardPage = () => {
      const session = useSession({ required: true });
    
      return ( ... )
    • 현재 세션 데이터 구성 (login요청으로 응답받은 accessToken으로 user data를 얻어서 세션 데이터에 추가함)
      image
  2. 미들웨어(middleware.ts)
    클라이언트에서 Next.js서버로 보내는 모든 요청을 가로채서 권한에 따른 리다이렉트, 요청 헤더 수정 등의 역할을 하는 next.js 특수 파일입니다. 프로젝트 최상단에 위치시키거나 src 경로 사용 시에는 /src에 middleware.ts 파일명으로 작성합니다.

    NextAuth 사용 시 auth() 함수를 사용하여 미들웨어 함수를 구성하면 세션 데이터가 요청(request) 객체에 추가되는데 이걸로 로그인 여부에 따라 특정 라우트에 접근하지 못하도록 protected routes를 설정할 수 있고, 특정 페이지(로그인 페이지)로 리다이렉트 하도록 구현할 수 있습니다.

    이런 인증 여부나 역할 검사 뿐만 아니라 request 객체에 들어있는 여러 정보를 가지고 국가/지역별 접근 차단, 로그 분석, API 호출 횟수 제한 등도 구현 가능합니다.

    미들웨어는 node.js 환경을 경량화해서 만든 edge network 환경에서 실행되므로 node.js의 fs 모듈 같은 것은 사용할 수 없고, 요청을 가로채는 기능이므로 함수의 실행 시간이 길어지는 로직이 포함되지 않도록 조심해야 합니다.

  3. 세션 데이터
    auth.config.ts에 작성한 인증 로직이 반환하는 데이터를 auth.tscallbacks를 거쳐가며 데이터를 연결해주는 방식입니다. authorizejwtsession 함수 순서로 데이터가 이어지며 마지막 순서인 session함수의 반환값이 미들웨어나 컴포넌트에서 사용할 세션 데이터 입니다.

💎 PR Point

파일 변경 순서대로 정리하겠습니다. 코드와 함께 보시는 것을 추천 드립니다!

  • next-auth.d.ts
    src/auth.ts에서 정의한 session과 token 매개변수의 타입을 확장하는 용도의 type 파일입니다. 로그인 api 결과로 받는 accessToken을 추가했습니다. jwtnext-auth/jwt로 따로 써야 작동하길래 따로 작성 했습니다.

  • src/api/user/actions.ts
    NextAuth로 만든 signIn 함수를 사용하는 server action 함수입니다. 첫 인자에는 signIn 방식(OAuth 플랫폼 종류 혹은 사용자 지정 인증 방식인 credentials)을 넣고 두번째 인자에는 signIn에 필요한 데이터와 옵션을 넣어줍니다. server action이니 "use server";를 잊지 않도록 조심합시다.

    server action 함수는 각 폼의 handleSubmit 함수에서 호출하면 됩니다.

    import { getSession, useSession } from "next-auth/react";
    ...
    const session = useSession();
    ...
    const handleSubmit = (values: z.infer<typeof loginSchema>) => {
      startTransition(async () => {
        await loginAction(values);
        // login action으로 만들어진 세션 데이터를 `SessionProvider`에 업데이트 하기 위함
        // 페이지를 새로고침 하기 전 까지 `SessionProvider`의 데이터(`useSession`)는 업데이트 되지 않음
        await session.update(await getSession()); 
      });
    };

    여기서 startTransition은 비동기 작업이 진행되는 동안(특히 느린 네트워크 환경일 때)에도 UI가 반응성을 유지하고 사용자가 상호 작용할 수 있는 환경을 제공할 수 있습니다. 사용하지 않으면 비동기 작업이 진행될 때 UI가 잠시 멈출 수 있어 UX에 영향을 줄 수 있습니다. 여러 프로젝트를 확인해본 결과 폼의 handleSubmit에는 꼭 쓰는 것이 국룰인 것 같습니다.

  • src/app/api/auth/[...nextauth]/route.ts
    컴포넌트가 await auth()useSession으로 요청하는 세션 데이터는 기본적으로 이 API 라우트를 통해 가져옵니다. 그래서 파일 내용도 NextAuth가 export하는 handlers뿐입니다. 추상화가 잘 돼있어 간결한 코드로 구현이 가능한 것이 좋은 것 같습니다.

  • RootLayout
    react queryprovider와 클라이언트 컴포넌트에서 Session 데이터를 가져올 때 사용하는 SessionProvider가 추가됐습니다. 그리고 공통 헤더에는 로그인 여부를 알 수 있도록 session을 props로 받게 변경했습니다.

    주의할 점은 SessionProviderre-rendering 되지 않는 RootLayout에서 사용되기 때문에 session을 직접 업데이트 해 주지 않으면 새로고침 전 까지 useSession으로 얻어온 세션 데이터는 처음 세션 데이터를 사용하게 되니 세션 데이터 변경이 필요한 경우 직접 업데이트를 해 주어야 합니다. 이 상황의 예를 들면 login 후 다음 페이지인 /user로 갔을 때 useSession은 여전히 빈 세션을 계속 사용하게 됩니다. 그에 따라 위의 handleSubmit 예시처럼 login action 이후 SessionProvider를 명시적으로 업데이트하는 로직이 필요합니다.

  • src/auth.ts src/auth.config.ts
    server action으로 사용할 로그인, 로그아웃 함수 및 세션 데이터 핸들러 함수를 제공하는 부분입니다.

    • pages : 로그인, 로그아웃, 에러, 요청 검증, 회원 가입에 사용할 페이지를 지정하는 부분입니다. 디폴트 값은 JSDoc으로 작성돼 있습니다.
    • callbacks : signIn, redirect, session, jwt 콜백을 작성할 수 있는데, 각각의 과정이 이뤄진 후의 반환값을 조작할 수 있습니다.
      • signIn은 예외적으로 반환값이 boolean타입 인데, false는 로그인 실패 처리, true는 로그인 성공 처리를 의미합니다.
      • redirect, session, jwt : 각 작업이 실행될 때 호출됩니다.
    • session : 위에서 설명했던 세션 관리 방식을 정합니다.
    • ...authConfig의 내용인 providers : OAuth나 credential(사용자 지정 로그인) 로직을 설정하는 부분입니다. OAuth의 경우 해당 플랫폼의 clientId와 clientSecret을 얻어다가 env파일을 통해 넣어주면 작동합니다. auth를 두 파일로 나눈 이유는 관심사 분리의 일환입니다.
  • middleware.ts
    파일 맨 아래 configmatcher부터 설명하자면, 이 미들웨어를 호출할 route를 설정하는 부분입니다. Next.js의 내부 파일과 정적 파일은 미들웨어를 호출하지 않고, /api/trpc로 시작하는 건 미들웨어를 호출하도록 합니다.

    기본적으로는 export function middleware(req) { ... } 이런 식으로 미들웨어 함수를 정의하지만, NextAuthauth 함수를 통해 req 매개변수에 인증 정보(auth)를 포함시켜서 미들웨어 함수를 정의할 수 있습니다.

    현재 로직을 설명하자면, 먼저 /api로 시작하는 요청은 별다른 처리 없이 넘깁니다. 앞서 설명한 matcher와 상충되는 로직이긴 하나 추후에 api 요청 객체를 조작할 일이 있을 것 같아 남겨두었습니다.
    다음으로는 로그인 없이 접근할 수 있는 경로인지 확인하여 만약 로그인 된 상태라면 DEFAULT_LOGIN_REDIRECT인 /user 페이지로 리다이렉트 시킵니다. 그게 아니라면 별다른 처리 없이 넘깁니다.
    마지막으로 로그인도 안했고 공용 접근 경로(/group)도 아니라면 로그인하라고 하기 위해 /login으로 리다이렉트 시킵니다.

    추후에 이 리다이렉트 로직(미들웨어 사용법)은 많은 논의와 변경이 필요할 것 같으니 꼭 숙지해주셨으면 합니다.

📸 Screenshot

init

@ptyoiy ptyoiy added ✨ Feat 새로운 기능 구현 🎉 Init 개발 환경 초기 세팅 GYU 곽규한 labels Nov 2, 2024
Copy link
Member

@wuzoo wuzoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다 !!

1next-auth를 통한 인증/인가 구현.. 사실 사용해본 적이 한번도 없어서 신기하네요 ! next에서 인증 인가를 효율적으로 수행하고, 세션을 관리할 수 있다... 배우고 갑니다 !!

Comment on lines +28 to +31
startTransition(async () => {
await loginAction(values);
await session.update(await getSession());
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startTransition은 리액트의 클라이언트 컴포넌트에서 상태 업데이트의 우선순위를 낮추고, 다른 우선순위가 높은 긴급한 상태를 업데이트하기 위해 사용하는 것으로 알고 있어요 !

특히나 리액트의 상태 업데이트가 아닌 서버 액션과 세션을 해당 함수 안에서 업데이트하는 것이 의미가 있을 지 잘 모르겠는데 어떻게 생각하시나용 ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI가 일시적으로 멈춰보이는 것은, 비동기 작업이기 때문에 어쩔 수 없는 것이라고 생각해요 !

리액트의 동시성 렌더링이 사용되어야 하는 경우는 렌더링을 최적화하기 위해 네트워크마다 상이한 딜레이 타임을 가지게 하면서 어느 정도 유연하게 UI를 블로킹하면서 이전의 지연된 상태의 UI를 보여주려고 할 때 사용하는 것이 맞다구 알고있습니다 !

Copy link
Member

@j-nary j-nary left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

꼼꼼히 정리해주셔서 저도 공부하는데 너무 수월했네요!
고생 많으셨습니다! 저도 이번 기회에 NextAuth에 대해 많이 찾아봤는데 토론을 나눌 기회가 있으면 좋겠네요!

Comment on lines 16 to 28
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin": {
return { error: "Invalid credentials!" };
}
default: {
return { error: "Something went wrong!" };
}
}
}

throw error; // AuthError가 아닐 경우 다른 try catch로 보내주기 위함
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희 서비스는 에러 메시지를 페이지 이동 없이 회원가입 또는 로그인 페이지에서 그대로 출력하기 때문에 useFormState 훅에 서버 액션을 연결해서 처리하는게 더 좋아보이는데 어떻게 생각하시나요?

Comment on lines 17 to 22
if (isLoggedIn) {
return NextResponse.redirect(
new URL(DEFAULT_LOGIN_REDIRECT, req.nextUrl),
);
}
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바로 리다이렉트 시키는게 아니라 아래와 같은 뷰를 거쳐서 리다이렉트 시킬 수 있는 방법을 고안할 필요가 있을 것 같습니당!
image

.min(6)
.max(12)
.regex(/^[a-zA-Z0-9]+$/),
email: z.string().min(6).max(18),

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 왜 삭제하셨는지 이유가 궁금합니다!

@github-actions github-actions bot added size/l and removed size/m labels Dec 3, 2024
@j-nary j-nary closed this Jan 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ Feat 새로운 기능 구현 GYU 곽규한 🎉 Init 개발 환경 초기 세팅 size/l
Projects
None yet
Development

Successfully merging this pull request may close these issues.

로그인 API 연결 및 초기 세팅
3 participants