[TIL] Supabase Auth + react-hook-form 사용법 정리 - 로그인/회원가입 구현까지

JongYeon·2025년 4월 3일

TIL

목록 보기
54/69
post-thumbnail

1️⃣ 필요한 라이브러리 패키지 설치

  • Supabase
npm install @supabase/supabase-js
  • react-hook-form
npm install react-hook-form

2️⃣ supabase client 생성

우리 팀에서는 api폴더 아래 만들기로 결정했다.

// src/app/api/client.ts

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL as string;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY as string;

// 환경 변수(.env파일)에 정보 설정이 되어있지 않은 경우 검사
if (!supabaseUrl || !supabaseAnonKey) {
  throw new Error('Supabase 환경 변수가 설정되지 않았습니다.');
}

const supabase = createClient(supabaseUrl, supabaseAnonKey);

export default supabase;
  • Browser Client: 쉽게 말하자면 클라이언트 컴포넌트에서도 사용할 수 있는 Client

  • Server Client: 쉽게 말하자면 서버 컴포넌트에서 사용되는 Client

📄NEXT_PUBLIC 유무 차이점

NEXT_PUBLIC_이 붙지 않은 환경변수는 서버(Node.js)에서만 접근 가능하며 브라우저에는 포함되지 않는다.
NEXT_PUBLIC_ 접두사가 붙은 환경변수는 클라이언트에서도 사용할 수 있도록 빌드 시에 번들에 포함된다.

예를 들어 supabaseClient.ts를 만든다면?

// utils/supabaseClient.ts
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

위 코드에서 process.env.NEXTPUBLIC_SUPABASE_URL과 ...ANON_KEY를 사용했는데, **NEXT_PUBLIC 접두사가 붙은 환경변수이므로 이 값들이 클라이언트에서도 Supabase에 접속할 수 있다.
만약 여기서
NEXTPUBLIC 없이 process.env.SUPABASE_SERVICE_ROLE_KEY 같은 비공개 변수를 사용했다면, undefined(정의되지 않음)로 나타나거나 빌드시 빈 문자열로 대체**되어 동작하지 않게 된다.
즉, NEXT_PUBLIC 접두사가 없는 변수는 클라이언트 측 코드에서는 접근할 수 없다.

🚨보안 관점에서 NEXT_PUBLIC 구분의 중요성

환경변수에 NEXT_PUBLIC 접두사를 붙이는 구분은 보안상 매우 중요하다. Next.js는 비공개 환경변수가 클라이언트 번들에 노출되지 않도록 자동으로 차단하거나 빈 값으로 대체함으로써 민감한 정보 유출을 방지한다.

NEXT_PUBLIC을 붙이면 해당 값은 누구나 브라우저에서 확인할 수 있게 공개된다는 의미입니다. 따라서 절대로 노출되어서는 안 될 비밀 값 (예: 데이터베이스 비밀번호, 결제 서비스 시크릿 키 등)은 NEXT_PUBLIC 없이 서버 측에서만 사용해야 한다. 만약 이러한 값을 잘못하여 NEXT_PUBLIC을 붙이면 애플리케이션 빌드 시 해당 값이 그대로 클라이언트에 포함되어 누구나 소스나 개발자 도구를 통해 볼 수 있게 되어 심각한 보안 위험이 됩니다

📊환경변수 비교표

환경변수 이름 형식사용 가능한 위치클라이언트 접근 가능 여부보안 위험도
NEXT_PUBLIC_ (공개 변수)클라이언트, 서버 모두 사용 가능가능(브라우저에서 확인됨)🔓 낮음 (공개 허용 정보만 사용)
(접두사 없음) 일반 환경변수서버 전용 (서버 사이드에서만 접근)불가능(브라우저 접근 차단)🔐 높음 (민감 정보 보호 필요)

참고할만한 블로그

3️⃣ 로그인/회원가입 기본 구조 작성

🔐회원가입 페이지 기본 구조

'use client';

const SignupPage = () => {
  return (
    <form>
      <div>
        <label htmlFor='email'>이메일</label>
        <input
          type='email'
          id='email'
          placeholder='이메일을 입력해주세요'
        />
      </div>

      <div>
        <label htmlFor='password'>비밀번호</label>
        <input
          type='password'
          id='password'
          placeholder='비밀번호를 입력해주세요'
        />
      </div>

      <div>
        <label htmlFor='nickname'>닉네임</label>
        <input
          type='text'
          id='nickname'
          placeholder='닉네임을 입력해주세요'
        />
      </div>
      <button type='submit'>회원가입</button>
    </form>
  );
};

export default SignupPage;

🔓로그인 페이지

'use client';

const LoginPage = () => {
  return (
    <form>
      <div>
        <label htmlFor='email'>이메일</label>
        <input
          type='email'
          id='email'
          placeholder='이메일을 입력해주세요'
        />
      </div>

      <div>
        <label htmlFor='password'>비밀번호</label>
        <input
          type='password'
          id='password'
          placeholder='비밀번호를 입력해주세요'
        />
      </div>
      <button type='submit'>로그인</button>
    </form>
  );
};

export default LoginPage;

4️⃣ react-hook-form 적용

🔐회원가입

  1. react-hook-form에서 제공되는 useForm을 import한다.
  2. useForm객체에 있는 register, handleSubmit를 구조분해할당으로 사용한다.
  3. 아래 코드와 같이 register를 사용해서 input태그를 react-hook-form에 연결시켜준다.
    첫 번째 인자는 해당input에 이름이고, 두 번째 인자는 유효성 검사 규칙을 설정하는 객체다.

    현재는 필수 작성 요소를 의미하는 required만 작성했다.
<input type='email' id='email' placeholder='이메일을 입력해주세요' 
       {...register('email', {required: '이메일 작성은 필수입니다.',})} />
  1. form태그 onSubmit옵션에 handleSubmit을 호출한다. 이 때 매개변수에는 supabase auth api를 사용한 회원가입 로직이 들어간다.

    즉, onSignupHandler에는 회원가입 로직이 들어간다.
  <form onSubmit={handleSubmit(onSignupHandler)}>
  1. 회원가입 버튼을 눌렀을 때 handleSubmit(onSubmitHandler)가 실행되면서 회원가입이 실행된다.
'use client';

import { FieldValues, useForm } from 'react-hook-form';
import { useRouter } from 'next/navigation';
import supabase from '../api/browserClient';

const SignupPage = () => {
  const { register, handleSubmit } = useForm();
  const router = useRouter();

  const onSignupHandler = async (value: FieldValues) => {
    const { email, password, nickname } = value;

    const { data, error } = await supabase.auth.signUp({
      email,
      password,
      options: { data: { nickname } },
    });
    if (error) {
      throw new Error(error.message);
    } else {
      // setUser(data)  //setUser는 추후 zustand를 사용하면 사용될 로직이다.
      router.push('/login');
    }
  };

  return (
    <form onSubmit={handleSubmit(onSignupHandler)}>
      <div>
        <label htmlFor='email'>이메일</label>
        <input
          type='email'
          id='email'
          placeholder='이메일을 입력해주세요'
          {...register('email', {
            required: '이메일 작성은 필수입니다.',
          })}
        />
      </div>

      <div>
        <label htmlFor='password'>비밀번호</label>
        <input
          type='password'
          id='password'
          placeholder='비밀번호를 입력해주세요'
          {...register('password', { required: true })}
        />
      </div>

      <div>
        <label htmlFor='nickname'>닉네임</label>
        <input
          type='text'
          id='nickname'
          placeholder='닉네임을 입력해주세요'
          {...register('nickname', { required: true })}
        />
      </div>
      <button type='submit'>회원가입</button>
    </form>
  );
};

export default SignupPage;

🔓로그인

  1. react-hook-form에서 제공되는 useForm을 import한다.
  2. useForm객체에 있는 register, handleSubmit를 구조분해할당으로 사용한다.
  3. 아래 코드와 같이 register를 사용해서 input태그를 react-hook-form에 연결시켜준다.
    첫 번째 인자는 해당input에 이름이고, 두 번째 인자는 유효성 검사 규칙을 설정하는 객체다.

    현재는 필수 작성 요소를 의미하는 required만 작성했다.
<input type='email' id='email' placeholder='이메일을 입력해주세요' 
       {...register('email', {required: '이메일 작성은 필수입니다.',})} />
  1. form태그 onSubmit옵션에 handleSubmit을 호출한다. 이 때 매개변수에는 supabase auth api를 사용한 회원가입 로직이 들어간다.

    즉, onLoginHandler에는 로그인 로직이 들어간다.
  <form onSubmit={handleSubmit(onLoginHandler)}>
  1. 로그인 버튼을 눌렀을 때 handleSubmit(onSubmitHandler)가 실행되면서 회원가입이 실행된다.
'use client';

import { FieldValues, useForm } from 'react-hook-form';
import supabase from '../api/browserClient';
import { useRouter } from 'next/navigation';

const LoginPage = () => {
  const { register, handleSubmit } = useForm();
  const router = useRouter();

  const onLoginHandler = async (value: FieldValues) => {
    const { email, password } = value;

    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });
    if (error) {
      throw new Error(error.message);
    } else {
      // setUser(data) //setUser는 추후 zustand를 사용하면 사용될 로직이다.
      router.push('/');
    }
  };

  return (
    <form onSubmit={handleSubmit(onLoginHandler)}>
      <div>
        <label htmlFor='email'>이메일</label>
        <input
          type='email'
          id='email'
          placeholder='이메일을 입력해주세요'
          {...register('email', {
            require
      </div>

      <div>
        <label htmlFor='password'>비밀번호</label>
        <input
          type='password'
          id='password'
          placeholder='비밀번호를 입력해주세요'
          {...register('password', { required: true })}
        />
      </div>

      <button type='submit'>로그인</button>
    </form>
  );
};

export default LoginPage;
profile
프론트엔드 공부중

0개의 댓글