rhanziy

Next.js - Supabase에 등록한 authentication으로 로그인 처리하기(feat.AuthScreen) 본문

Next.js

Next.js - Supabase에 등록한 authentication으로 로그인 처리하기(feat.AuthScreen)

rhanziy 2025. 1. 15. 23:23

이번 글은 로그인 기능을 추가하면서, 프로젝트 관리자 페이지에 접근할 사용자 수가 한정적이라는 점을 고려해, 최대한 간편하게 Supabase Auth를 활용해 인증 처리를 구현했다.
특히, 처음으로 Next.js 프로젝트를 진행하다 보니, 서버에서 로그인 세션을 받아오고 이를 검증하는 절차가 꽤 까다롭게 느껴졌다.ㅎㅎ
Supabase Auth에서 제공하는 다양한 소셜 로그인 기능을 사용하지 않았는데, 그 이유는 프로젝트 자체가 정보 공유를 위한 정적 웹사이트였고, 관리자 계정만 필요했기 때문이다.
따라서 Supabase 프로젝트에 사전에 지정된 관리자 계정을 추가한 뒤, 로그인 시 해당 계정을 기준으로 검증하는 방식으로 인증 흐름을 구현했다.
 

1. Create New User / NPM설치

supabase 프로젝트 좌측 메뉴를 보면 Authentication 메뉴가 있는데 거기에서 Add user를 해주면 된다. 
나는 Email 형식으로 진행했다. 다양한 방식으로 로그인을 지원하니 변경하려면 Authentication - Providers에서 변경해주자.

 
여기서 추가로 공식문서에서는 Email 방식은 기본적으로 사용자가 로그인하기 전에 이메일 주소를 확인해야 한다고 한다.
이 부분이 불필요하다면 Authentication - Providers에서 활성/비활성으로 설정할 수 있다.
Email 확인이 활성 상태일때, session은 null로 반환되고 user 값만 return 되지만
비활성 상태인 경우 user와 session 값이 return 된다.

  • Confirm email determines if users need to confirm their email address after signing up.
    • If Confirm email is enabled, a user is returned but session is null.
    • If Confirm email is disabled, both a user and a session are returned.

▶️ Email 확인을 활성화했을때 return 데이터 참고

더보기
// Some fields may be null if "confirm email" is enabled.
{
  "data": {
    "user": {
      "id": "11111111-1111-1111-1111-111111111111",
      "aud": "authenticated",
      "role": "authenticated",
      "email": "example@email.com",
      "email_confirmed_at": "2024-01-01T00:00:00Z",
      "phone": "",
      "last_sign_in_at": "2024-01-01T00:00:00Z",
      "app_metadata": {
        "provider": "email",
        "providers": [
          "email"
        ]
      },
      "user_metadata": {},
      "identities": [
        {
          "identity_id": "22222222-2222-2222-2222-222222222222",
          "id": "11111111-1111-1111-1111-111111111111",
          "user_id": "11111111-1111-1111-1111-111111111111",
          "identity_data": {
            "email": "example@email.com",
            "email_verified": false,
            "phone_verified": false,
            "sub": "11111111-1111-1111-1111-111111111111"
          },
          "provider": "email",
          "last_sign_in_at": "2024-01-01T00:00:00Z",
          "created_at": "2024-01-01T00:00:00Z",
          "updated_at": "2024-01-01T00:00:00Z",
          "email": "example@email.com"
        }
      ],
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    },
    "session": {
      "access_token": "<ACCESS_TOKEN>",
      "token_type": "bearer",
      "expires_in": 3600,
      "expires_at": 1700000000,
      "refresh_token": "<REFRESH_TOKEN>",
      "user": {
        "id": "11111111-1111-1111-1111-111111111111",
        "aud": "authenticated",
        "role": "authenticated",
        "email": "example@email.com",
        "email_confirmed_at": "2024-01-01T00:00:00Z",
        "phone": "",
        "last_sign_in_at": "2024-01-01T00:00:00Z",
        "app_metadata": {
          "provider": "email",
          "providers": [
            "email"
          ]
        },
        "user_metadata": {},
        "identities": [
          {
            "identity_id": "22222222-2222-2222-2222-222222222222",
            "id": "11111111-1111-1111-1111-111111111111",
            "user_id": "11111111-1111-1111-1111-111111111111",
            "identity_data": {
              "email": "example@email.com",
              "email_verified": false,
              "phone_verified": false,
              "sub": "11111111-1111-1111-1111-111111111111"
            },
            "provider": "email",
            "last_sign_in_at": "2024-01-01T00:00:00Z",
            "created_at": "2024-01-01T00:00:00Z",
            "updated_at": "2024-01-01T00:00:00Z",
            "email": "example@email.com"
          }
        ],
        "created_at": "2024-01-01T00:00:00Z",
        "updated_at": "2024-01-01T00:00:00Z"
      }
    }
  },
  "error": null
}

 

그 후 사용하는 패키지매니저를 통해 라이브러리를 설치한다.

npm install @supabase/supabase-js
yarn add @supabase/supabase-js
pnpm add @supabase/supabase-js

 

2. Sign in a User

그럼 이제 등록한 계정으로 로그인하는 방법을 살펴보자~! 공식문서에서는 아래와 같이 로그인 처리를 하라고 안내되어있다.
처음에 supabase.auth.signUp({...})을 사용했다가 계속 오류가나서 보니 존재하는 유저 정보는 signInWithPassword를 사용해야 됐었다. ㅋㅋ

const { data, error } = await supabase.auth.signInWithPassword({
  email: 'example@email.com',
  password: 'example-password',
})

 
client side
정보를 입력하고 로그인 버튼을 눌렀을때 /api/auth로 데이터를 보내주고 응답값에 따라 에러처리를 해줬다.
여기서 setIsAuthenticated를 true로 처리해줬는데 useEffect내부에서 IsAuthenticated 상태로 authScreen을 분기처리하기 위해 작성했다.

  const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    try {
      const response = await fetch('/api/auth', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          email: inputEmail,
          password: inputPw,
        }),
      });

      if (!response.ok) {
        throw new Error('로그인 실패했습니다.');
      }
      setIsAuthenticated(true);
    } catch (error) {
      alert('계정 정보가 일치하지 않습니다.');
      console.error('로그인 실패', error);
    }
  };

 
server side
api/auth post 작성

export async function POST(req: NextRequest) {
  const supabase = createClient();

  try {
    const { email, password } = await req.json();

    if (!email || !password) {
      return NextResponse.json(
        { error: '이메일과 비밀번호는 필수입니다.' },
        { status: 400 },
      );
    }

    const {
      data: { user },
      error,
    } = await supabase.auth.signInWithPassword({
      email,
      password,
    });

    if (error) {
      return NextResponse.json({ error: error.message }, { status: 401 });
    }

    return NextResponse.json({ user }, { status: 200 });
  } catch (err) {
    console.error('요청 처리 중 오류 발생:', err);
    return NextResponse.json(
      { error: '오류가 발생했습니다.' },
      { status: 400 },
    );
  }
}

 
로그인이 성공적으로 되면 {session: null} 상태에서 예쁘게 토큰이 담겨져서 들어온다.^_^

 

3. 로그인 화면 분기처리

처음에는 2개의 파일로 기능을 분리한 뒤, 또 다른 파일에서 인증 세션 값을 기준으로 분기 처리하는 방법을 시도했다. 하지만 이 접근 방식에서는 인증 처리가 여러 단계로 나뉘게 되어 불필요한 페이지와 복잡도가 늘어나는 단점이 있었다. 결국, 이 과정 자체가 인증 처리의 일부로 포함된다고 생각되어 더 간결하고 효율적인 구조를 고민했던 것 같다.
결과적으로, <AuthScreen> 컴포넌트로 인증 로직을 감싸는 방법을 선택했다. 이렇게 하면서 인증과 관련된 처리를 단일 컴포넌트로 책임지도록 설계했으며, 인증 상태에 따라 자식 컴포넌트를 동적으로 렌더링할 수 있는 구조를 완성했다.
 
먼저 로그인 관련 client side 페이지 접속 시 getSession을 통해 session data를 확인하고, isAuthenticated(boolean) 값을 업데이트해준다. 

  useEffect(() => {
    const checkSession = async () => {
      const supabase = createClient();
      const { data } = await supabase.auth.getSession();
      setIsAuthenticated(!!data.session);
    };
    checkSession();
  }, [setIsAuthenticated]);

 
참고로 isAuthenticated은 Next.js에서 아래와 같은 장점으로 인해 전역상태로 관리하고 있다.

 
그 후 AuthScreen 컴포넌트에서 인증처리가 성공적으로 마쳤을 때 보여질 페이지를 children props로 넘겨주고,

import { FormEvent, useEffect, useState } from 'react';
import { TextField } from '@mui/material';
import { wrapper } from '@/app/styles/container.css';
import Button from '../Button';
import useAuthStore from './_store';
import createClient from '@/config/supabase/client';

export default function AuthScreen({
  children,
}: {
  children: React.ReactNode;
}) {
  
  // 1. 세션 확인
  useEffect(() => {
    const checkSession = async () => {
      const supabase = createClient();
      const { data } = await supabase.auth.getSession();
      setIsAuthenticated(!!data.session);
    };
    checkSession();
  }, [setIsAuthenticated]);
  
  // 2. 위에 작성한 로그인 기능
  
  if (!isAuthenticated) {
    return (
      // 아이디, 패스워드 입력창 생략
    );
  }

  return <>{children}</>;
}

 
admin page에서 AuthScreen 컴포넌트로 감싸주었다. 완성!

<AuthScreen>
  // 관리자페이지 접속 시 보여질 페이지
</AuthScreen>

 
 
이번 작업에서는 규모가 작은 프로젝트임에도 기능별 로직을 분리하여 가독성을 높였고, 미들웨어를 대신하는 컴포넌트를 활용해 인증 처리를 최적화했다는 점에서 만족스러움을 느꼈다. 또, AuthScreen은 관리자 페이지가 확장되더라도 재사용이 가능하다는 구조적 이점까지 갖추고 있어 더욱 효율적이였다.
이 과정을 통해 느낀 경험과 고민들은 앞으로의 작업에서도 중요한 기준이 될 것 같다.
기능별로 코드를 명확히 분리하여 가독성을 높이고, 확장성을 고려한 구조를 설계하는 과정에서 많은 배움을 얻었고, 이러한 접근 방식이 단순하면서도 큰 가치를 만들어낼 수 있다는 것을 깨달았다.
앞으로도 단순히 기능 구현에 그치지 않고 항상 코드의 재사용성/확장성 그리고 유지 보수성을 고려하는 개발자가 되고 싶다!

Comments