React와 Supabase를 활용한 사용자 인증 시스템 구축
1. 서론
현대 웹 애플리케이션 개발에서 사용자 인증은 더 이상 선택이 아닌 필수적인 기능으로 자리 잡았다. Supabase는 이러한 요구에 부응하여 강력하고 유연한 인증 솔루션인 Supabase Auth를 제공한다.1 Supabase는 오픈소스 기반의 Firebase 대안으로, 모든 프로젝트의 백엔드로 PostgreSQL 데이터베이스를 제공하며, 인증 시스템은 이 데이터베이스와 긴밀하게 통합되어 있다.2 이러한 구조는 개발자에게 단순한 인증 기능을 넘어 데이터베이스 레벨에서의 세밀한 접근 제어와 높은 수준의 유연성을 부여한다.
Supabase Auth는 이메일/비밀번호 기반 인증, 소셜 로그인, Magic Link, 전화번호 인증 등 다양한 인증 방법을 지원하며, 이 모든 사용자 정보는 프로젝트의 PostgreSQL 데이터베이스 내 auth 스키마에 안전하게 저장된다.1 이 데이터베이스와의 직접적인 연동은 인증(Authentication)과 인가(Authorization)가 데이터베이스 레벨에서부터 긴밀하게 연결됨을 의미한다. 예를 들어, 사용자의 인증 상태를 기반으로 데이터베이스의 특정 행(Row)에 대한 접근 권한을 제어하는 RLS(Row Level Security) 정책을 손쉽게 구현할 수 있다.1 이는 애플리케이션의 보안 모델을 근본적으로 단순화하고 강화하는 강력한 패러다임을 제시한다.
본 안내서는 React 환경에서 Supabase Auth를 활용하여 이메일 및 비밀번호 기반의 계정 등록 및 로그인 기능을 구현하는 전체 과정에 대한 심층적인 분석을 제공하는 것을 목표로 한다. 단순한 코드 예제 나열을 넘어, 프로젝트 환경 설정부터 핵심 인증 기능 구현, 전역 상태 관리 아키텍처, 그리고 프로덕션 수준의 견고한 오류 처리 전략에 이르기까지, 실제 애플리케이션 개발에 필요한 깊이 있는 지식과 모범 사례를 다룰 것이다.
2. 프로젝트 환경 설정
견고한 인증 시스템을 구축하기 위한 첫 단계는 안정적인 개발 환경을 구성하는 것이다. 이 과정은 Supabase 백엔드 설정과 React 프론트엔드 프로젝트 초기화의 두 가지 주요 부분으로 나뉜다.
2.1 Supabase 프로젝트 생성 및 데이터베이스 스키마 구성
먼저 Supabase 백엔드를 설정해야 한다. 이 과정은 Supabase 대시보드에서 직관적으로 진행할 수 있다.
-
Supabase 프로젝트 생성: Supabase 대시보드에 로그인한 후 ‘New Project’ 버튼을 클릭하여 새로운 프로젝트를 생성한다. 프로젝트 이름, 데이터베이스 비밀번호, 리전(Region)을 선택하면 몇 분 내에 전용 PostgreSQL 데이터베이스와 자동 생성된 API 엔드포인트를 포함한 완전한 백엔드 환경이 구축된다.2
-
데이터베이스 스키마 설정: 프로젝트가 생성되면, 사용자의 프로필 정보를 저장할 별도의 테이블을 구성해야 한다. Supabase는 인증 관련 데이터를 관리하는
auth.users테이블을 자동으로 생성하지만, 이 테이블은 직접 수정할 수 없다. 따라서 애플리케이션에 필요한 추가 사용자 정보(예: 사용자 이름, 아바타 URL)는public스키마 내에 별도의 테이블을 만들어 관리하는 것이 권장된다. Supabase는 이 과정을 간소화하기 위해 ’User Management Starter’라는 퀵스타트 SQL 템플릿을 제공한다.2
-
대시보드의 ‘SQL Editor’ 메뉴로 이동한다.
-
‘Quickstarts’ 탭에서 ’User Management Starter’를 선택하고 ‘Run’ 버튼을 클릭한다.
-
이 스크립트는
public.profiles테이블을 생성하며, 이 테이블은auth.users테이블의id를 외래 키(Foreign Key)로 참조한다. 또한,handle_new_user라는 PostgreSQL 트리거 함수를 함께 생성한다. 이 트리거는auth.users테이블에 새로운 사용자가 추가될 때마다 해당 사용자의id와 이메일 주소를public.profiles테이블에 자동으로 복사하는 역할을 수행한다.6
이러한 스키마 구조는 인증 로직과 비즈니스 로직을 데이터베이스 레벨에서부터 분리하는 ‘관심사의 분리(Separation of Concerns)’ 원칙을 따른다. 인증 시스템은 auth 스키마에 캡슐화되고, 애플리케이션 고유의 데이터 모델은 public 스키마에 집중되므로 시스템 전체의 유지보수성과 확장성이 크게 향상된다.
- 이메일 확인 비활성화 (개발 단계): 개발 초기 단계에서는 가입 절차를 간소화하기 위해 이메일 확인 기능을 잠시 비활성화하는 것이 편리하다.
-
대시보드의
Authentication>Providers메뉴로 이동한다. -
Email제공자 설정에서 ‘Confirm email’ 옵션을 비활성화한다.2 -
프로덕션 배포 전에는 보안을 위해 이 옵션을 반드시 다시 활성화해야 한다.
2.2 API 키 확보 및 환경 변수 설정
React 애플리케이션이 Supabase 프로젝트와 통신하기 위해서는 API 엔드포인트 URL과 인증 키가 필요하다.
- API 키 확인: Supabase 대시보드의
Settings>API페이지로 이동한다. 여기서 ’Project URL’과 ‘Project API Keys’ 섹션의anon(public) 키를 확인하여 복사한다.2
anon 키는 클라이언트 사이드(브라우저)에서 안전하게 사용할 수 있도록 설계된 공개 키다. 이 키가 브라우저에 노출되어도 안전한 이유는, 실제 데이터 접근 권한은 후술할 RLS(Row Level Security) 정책에 의해 서버 사이드에서 제어되기 때문이다.1
- 환경 변수 설정: API 키와 같은 민감한 정보는 소스 코드에 직접 하드코딩하지 않고 환경 변수를 통해 관리하는 것이 모범 사례다. Vite 기반의 React 프로젝트에서는 루트 디렉터리에
.env.local파일을 생성하여 환경 변수를 관리한다.3
#.env.local
VITE_SUPABASE_URL=YOUR_SUPABASE_URL
VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY
YOUR_SUPABASE_URL과 YOUR_SUPABASE_PUBLISHABLE_KEY를 앞서 복사한 값으로 대체한다. Vite는 VITE_ 접두사가 붙은 환경 변수만 클라이언트 사이드 코드에서 접근할 수 있도록 허용한다.4 이 .env.local 파일은 Git 버전 관리에서 제외하기 위해 .gitignore 파일에 추가해야 한다.
2.3 React 프로젝트 초기화 및 의존성 설치
이제 프론트엔드인 React 애플리케이션을 설정할 차례다.
- React 프로젝트 생성: 터미널을 열고 다음 명령어를 실행하여 Vite를 사용한 새로운 React 프로젝트를 생성한다. Vite는 빠른 개발 서버와 최적화된 빌드 프로세스를 제공하여 현대적인 웹 개발 환경에 적합하다.4
npm create vite@latest my-supabase-auth-app -- --template react
- 프로젝트 디렉터리 이동: 프로젝트 생성이 완료되면 해당 디렉터리로 이동한다.
cd my-supabase-auth-app
- Supabase 클라이언트 라이브러리 설치: Supabase 백엔드와 상호작용하기 위한 공식 JavaScript 클라이언트 라이브러리인
@supabase/supabase-js를 설치한다.4
npm install @supabase/supabase-js
이 라이브러리는 Supabase의 인증, 데이터베이스, 스토리지 등 모든 기능과 통신할 수 있는 편리한 API를 제공한다.
이로써 Supabase 백엔드와 React 프론트엔드 개발을 위한 모든 환경 설정이 완료되었다.
3. Supabase 클라이언트 초기화 및 구성
애플리케이션 전체에서 Supabase 클라이언트를 일관되게 사용하기 위해, 클라이언트 인스턴스를 초기화하고 구성하는 로직을 중앙에서 관리하는 것이 중요하다. 이는 싱글톤(Singleton) 패턴을 적용하여 불필요한 인스턴스 생성을 방지하고 애플리케이션의 상태 관리를 용이하게 한다.
src 디렉터리 내에 supabaseClient.js (또는 TypeScript를 사용한다면 supabaseClient.ts)라는 이름의 파일을 생성한다. 이 파일은 Supabase 클라이언트 인스턴스를 생성하고 export하는 유일한 장소가 될 것이다.3
// src/supabaseClient.js
import { createClient } from '@supabase/supabase-js';
//.env.local 파일에서 환경 변수를 가져온다.
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
// Supabase 클라이언트 인스턴스를 생성한다.
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
이 코드의 각 부분은 다음과 같은 중요한 역할을 수행한다.
-
import { createClient } from '@supabase/supabase-js': 설치한 Supabase 클라이언트 라이브러리에서createClient함수를 가져온다.4 -
import.meta.env.VITE_SUPABASE_URL: Vite가 제공하는import.meta.env객체를 사용하여.env.local파일에 정의된 환경 변수에 접근한다. 이 방식은 서버 사이드 렌더링(SSR) 환경과 클라이언트 사이드 환경 모두에서 일관되게 작동한다.4 -
export const supabase = createClient(...):createClient함수에 프로젝트 URL과anon키를 인자로 전달하여 클라이언트 인스턴스를 생성한다.export키워드를 사용하여 이 인스턴스를 다른 모듈에서import하여 사용할 수 있도록 한다.3
클라이언트 초기화 로직을 이처럼 별도의 모듈로 분리하는 것은 단순한 코드 구성을 넘어선 중요한 아키텍처적 결정이다. 만약 각 컴포넌트가 필요할 때마다 createClient를 호출한다면, 여러 개의 독립적인 클라이언트 인스턴스가 생성되어 불필요한 메모리를 소모하고, 각 인스턴스가 별도의 웹소켓 연결을 시도하거나 인증 상태를 불일치하게 관리하는 등의 예측 불가능한 문제를 야기할 수 있다.
Supabase 클라이언트는 내부적으로 인증 토큰의 자동 갱신, 실시간 구독을 위한 웹소켓 연결 관리 등 상태를 유지하는 로직을 포함하고 있다.2 단일 인스턴스, 즉 싱글톤을 사용함으로써 애플리케이션 전체에서 단 하나의 연결과 상태 관리 주체만이 존재하도록 보장할 수 있다. 이는 React의 ‘단일 진실 공급원(Single Source of Truth)’ 원칙을 API 클라이언트 수준에서 구현하는 것으로, 이후에 다룰 onAuthStateChange와 같은 전역 리스너가 중복 등록 없이 단 한 번만 설정되도록 하며, 이는 애플리케이션의 예측 가능성과 안정성을 크게 향상시키는 기반이 된다.
4. 핵심 인증 기능 구현
환경 설정과 클라이언트 초기화가 완료되었으므로, 이제 사용자가 직접 상호작용하는 핵심 인증 기능을 구현할 차례다. 이 섹션에서는 인증 상태를 관리하고 UI를 동적으로 변경하는 방법부터 계정 등록, 로그인, 로그아웃 기능까지 단계별로 구현한다.
4.1 인증 상태 관리 및 UI 분기 처리
React 애플리케이션에서 인증 상태에 따라 다른 화면을 보여주는 것은 가장 기본적인 요구사항이다. 이를 위해 최상위 컴포넌트(일반적으로 App.jsx)에서 사용자의 세션(session) 상태를 관리하고, 이 상태에 따라 조건부로 컴포넌트를 렌더링하는 패턴을 사용한다.
App.jsx 파일을 다음과 같이 수정한다.
// src/App.jsx
import { useState, useEffect } from 'react';
import { supabase } from './supabaseClient';
import Auth from './Auth';
import Account from './Account';
function App() {
const = useState(null);
useEffect(() => {
// 1. 컴포넌트 마운트 시 현재 세션 정보를 가져온다.
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
});
// 2. 인증 상태 변경을 감지하는 리스너를 설정한다.
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
// 3. 컴포넌트 언마운트 시 리스너를 클린업(해제)한다.
return () => subscription.unsubscribe();
},);
return (
<div className="container" style={{ padding: '50px 0 100px 0' }}>
{!session? ( <Auth />
) : ( <Account key={session.user.id} session={session} />
)} </div>
);
}
export default App;
이 코드의 핵심은 useEffect 훅 내부에 있다.
-
supabase.auth.getSession(): 이 함수는 컴포넌트가 처음 렌더링될 때 한 번 호출된다. 페이지를 새로고침했거나 다른 탭에서 로그인한 경우, 브라우저의 저장소(기본적으로localStorage)에 저장된 세션 정보가 있는지 확인하고, 유효한 세션이 있다면 해당 세션 객체를 반환한다. 이를setSession을 통해 초기 상태로 설정함으로써, 사용자가 로그인 상태를 유지할 수 있게 된다.7 -
supabase.auth.onAuthStateChange(): 이 함수는 Supabase 클라이언트와 React의 반응성 시스템을 연결하는 핵심적인 역할을 한다.onAuthStateChange는 인증 상태에 변화가 생길 때마다 등록된 콜백 함수를 실행하는 리스너를 구독한다.10 인증 이벤트에는
SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED 등이 포함된다. 사용자가 성공적으로 로그인하거나 로그아웃하면, 이 리스너가 새로운 세션 정보(로그인 시) 또는 null(로그아웃 시)을 받아 setSession을 호출한다. 이 상태 변경으로 인해 React는 UI를 자동으로 다시 렌더링하여 로그인 폼(Auth 컴포넌트)이나 계정 정보 페이지(Account 컴포넌트)를 보여주게 된다.12 이 메커니즘 덕분에 개발자는 “로그인했으니 화면을 바꿔라“와 같은 명령형 코드를 작성할 필요 없이, “세션이 있으면 계정 페이지를, 없으면 인증 페이지를 보여줘라“라는 선언적 방식으로 UI를 구성할 수 있다.
subscription.unsubscribe():useEffect의 반환(return) 함수는 컴포넌트가 언마운트될 때 실행되는 클린업(cleanup) 함수다. 여기서unsubscribe를 호출하여 더 이상 필요 없는 리스너를 제거한다. 이는 메모리 누수를 방지하고 예기치 않은 동작을 막는 중요한 과정이다.10
4.2 계정 등록 (Sign Up) 기능
이제 사용자가 이메일과 비밀번호를 입력하여 계정을 생성할 수 있는 Auth.jsx 컴포넌트를 구현한다.
// src/Auth.jsx
import { useState } from 'react';
import { supabase } from './supabaseClient';
export default function Auth() {
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSignUp = async (event) => {
event.preventDefault();
setLoading(true);
const { data, error } = await supabase.auth.signUp({
email: email,
password: password,
});
if (error) {
alert(error.error_description |
| error.message);
} else {
// 이메일 확인 기능이 활성화된 경우, 사용자에게 안내 메시지를 표시할 수 있다.
alert('Check your email for the login link!');
}
setLoading(false);
};
// 로그인 기능은 다음 섹션에서 구현한다.
const handleSignIn = async (event) => {
//...
};
return (
<div>
<h1>Supabase + React</h1>
<p>Sign up or sign in to your account</p>
<form onSubmit={handleSignUp}>
<input
type="email"
placeholder="Your email"
value={email}
required
onChange={(e) => setEmail(e.target.value)} /> <input
type="password"
placeholder="Your password"
value={password}
required
onChange={(e) => setPassword(e.target.value)} /> <button type="submit" disabled={loading}>
{loading? <span>Loading...</span> : <span>Sign Up</span>} </button>
</form>
</div>
);
}
-
상태 관리:
useState훅을 사용하여loading(API 요청 진행 여부),email,password상태를 관리한다.2 -
handleSignUp함수:async/await문법을 사용하여supabase.auth.signUp함수를 비동기적으로 호출한다. 이 함수는 이메일과 비밀번호를 담은 객체를 인자로 받는다.14 -
오류 처리:
signUp함수는{ data, error }형태의 객체를 반환한다.error객체가 존재할 경우,alert를 통해 사용자에게 간단한 오류 메시지를 표시한다. 프로덕션 환경에서는alert대신 사용자 경험을 고려한 UI 컴포넌트를 사용하는 것이 바람직하다.4 -
options객체:signUp함수는 세 번째 인자로options객체를 받을 수 있다. 이를 통해 가입 시 추가적인 메타데이터를 저장하거나, 이메일 확인 후 리디렉션될 URL을 지정하는 등 고급 기능을 사용할 수 있다.14
| 속성 (Property) | 타입 (Type) | 설명 (Description) | 예제 코드 (Example Code) |
|---|---|---|---|
data | object | 계정 생성 시 auth.users 테이블의 raw_user_meta_data 컬럼에 저장할 추가적인 사용자 메타데이터. first_name, age 등 자유로운 키-값 쌍을 포함할 수 있다. | options: { data: { full_name: '홍길동', age: 30 } } |
emailRedirectTo | string | 사용자가 가입 확인 이메일의 링크를 클릭했을 때 리디렉션될 URL. 지정하지 않으면 Supabase 프로젝트에 설정된 기본 SITE_URL로 이동한다. | options: { emailRedirectTo: 'https://myapp.com/welcome' } |
4.3 로그인 (Sign In) 기능
계정 등록과 유사하게, 기존 사용자가 로그인할 수 있는 기능을 Auth.jsx 컴포넌트에 추가한다.
Auth.jsx 파일에 handleSignIn 함수를 구현하고, 로그인 버튼을 추가한다.
// src/Auth.jsx (일부)
//... (handleSignUp 함수 아래에 추가)
const handleSignIn = async (event) => {
event.preventDefault();
setLoading(true);
const { data, error } = await supabase.auth.signInWithPassword({
email: email,
password: password,
});
if (error) {
alert(error.error_description |
| error.message);
}
// 성공 시 별도의 처리가 필요 없다. onAuthStateChange가 감지하여 처리한다.
setLoading(false);
};
return (
<div>
{/*... (기존 폼 수정 및 버튼 추가) */} <form>
<input
type="email"
placeholder="Your email"
value={email}
required
onChange={(e) => setEmail(e.target.value)} /> <input
type="password"
placeholder="Your password"
value={password}
required
onChange={(e) => setPassword(e.target.value)} /> <div>
<button onClick={handleSignIn} disabled={loading}>
{loading? <span>Loading...</span> : <span>Sign In</span>} </button>
<button onClick={handleSignUp} disabled={loading}>
{loading? <span>Loading...</span> : <span>Sign Up</span>} </button>
</div>
</form>
</div>
);
-
handleSignIn함수:supabase.auth.signInWithPassword함수를 사용한다는 점을 제외하면handleSignUp과 구조가 매우 유사하다. 이 함수 역시 이메일과 비밀번호를 인자로 받는다.16 -
성공 처리: 로그인에 성공하면,
signInWithPassword함수는 새로운 세션 정보를 반환한다. 이 때,App.jsx에 설정해 둔onAuthStateChange리스너가SIGNED_IN이벤트를 감지하고session상태를 업데이트한다. 그 결과App.jsx는Account컴포넌트를 렌더링하게 되므로,handleSignIn함수 내에서 별도의 화면 전환 로직을 구현할 필요가 없다. 이것이 바로 반응형 상태 관리의 강력함이다.
4.4 로그아웃 (Sign Out) 기능
마지막으로, 로그인된 사용자가 세션을 종료할 수 있는 로그아웃 기능을 Account.jsx 컴포넌트에 구현한다.
// src/Account.jsx
import { useState, useEffect } from 'react';
import { supabase } from './supabaseClient';
export default function Account({ session }) {
// 이 컴포넌트는 프로필 정보를 보여주고 수정하는 역할을 할 수 있다.
// 여기서는 간단히 로그아웃 기능만 구현한다.
const handleSignOut = async () => {
const { error } = await supabase.auth.signOut();
if (error) {
console.error('Error signing out:', error);
}
// 성공 시 onAuthStateChange가 감지하여 App 컴포넌트의 UI를 자동으로 변경한다.
};
return (
<div>
<h2>Account</h2>
<p>Welcome, {session.user.email}!</p>
<button onClick={handleSignOut}>Sign Out</button>
</div>
);
}
-
handleSignOut함수:supabase.auth.signOut()함수를 호출하여 현재 세션을 무효화한다.7 이 함수는 인자를 받지 않는다. -
UI 전환: 로그아웃이 성공적으로 완료되면,
onAuthStateChange리스너가SIGNED_OUT이벤트를 수신하고session상태를null로 변경한다. 이에 따라App.jsx는 다시Auth컴포넌트를 렌더링하여 로그인 화면으로 돌아가게 된다.
이로써 React와 Supabase를 사용한 기본적인 사용자 인증 플로우(가입, 로그인, 로그아웃) 구현이 모두 완료되었다.
5. 심층 분석: 고급 세션 관리 및 오류 처리 전략
기본적인 인증 기능 구현을 넘어, 실제 프로덕션 환경에서 요구되는 확장성과 안정성을 갖춘 애플리케이션을 구축하기 위해서는 더 정교한 아키텍처 패턴이 필요하다. 이 섹션에서는 React Context API를 활용한 전역 상태 관리 방법과 체계적인 오류 처리 전략을 심층적으로 분석한다.
5.1 React Context API를 활용한 전역 인증 상태 관리
지금까지 App 컴포넌트에서 session 상태를 직접 관리했다. 이는 간단한 애플리케이션에서는 효과적이지만, 애플리케이션의 규모가 커지고 컴포넌트 구조가 깊어지면 문제가 발생한다. 예를 들어, 최상위 App 컴포넌트의 session 정보를 아주 깊은 곳에 있는 UserProfileHeader 컴포넌트에서 사용해야 한다면, 중간에 있는 모든 컴포넌트를 거쳐 props를 전달해야 하는 ‘prop drilling’ 현상이 발생한다.12 이는 코드의 가독성을 해치고 유지보수를 어렵게 만든다.
이 문제를 해결하기 위한 React의 공식적인 해결책이 바로 Context API다. Context를 사용하면 props를 통해 데이터를 전달하지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다. 인증 상태와 같이 애플리케이션의 여러 부분에서 필요로 하는 전역적인 데이터 관리에 매우 적합하다.12
다음은 Context API를 사용하여 인증 상태를 관리하는 AuthProvider를 구현하는 방법이다.
AuthContext.js파일 생성:src디렉터리에contexts폴더를 만들고 그 안에AuthContext.js파일을 생성한다.
// src/contexts/AuthContext.js
import React, { createContext, useState, useEffect, useContext } from 'react';
import { supabase } from '../supabaseClient';
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// App.jsx에 있던 세션 관리 로직을 이곳으로 이전한다.
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setUser(session?.user?? null);
setLoading(false);
});
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setSession(session);
setUser(session?.user?? null);
setLoading(false);
}
);
return () => subscription.unsubscribe();
},);
// 하위 컴포넌트에 제공할 값들
const value = {
session,
user,
signOut: () => supabase.auth.signOut(),
};
return (
<AuthContext.Provider value={value}>
{!loading && children} </AuthContext.Provider>
);
};
// 컨텍스트를 쉽게 사용할 수 있도록 커스텀 훅을 제공한다.
export const useAuth = () => {
return useContext(AuthContext);
};
이 AuthProvider 컴포넌트는 App 컴포넌트에 있던 모든 인증 상태 관리 로직(session, user, loading)과 useEffect 훅을 내장하고 있다. 그리고 AuthContext.Provider를 통해 session, user, signOut 함수 등을 value 객체로 묶어 모든 하위 컴포넌트에 제공한다.10
useAuth 커스텀 훅은 다른 컴포넌트에서 useContext(AuthContext)를 직접 사용하는 대신, 간결하게 useAuth()를 호출하여 컨텍스트 값에 접근할 수 있도록 돕는 유틸리티다.12
- 애플리케이션 래핑: 애플리케이션 전체에서 이 컨텍스트를 사용할 수 있도록,
main.jsx(또는index.js) 파일에서 최상위 컴포넌트를AuthProvider로 감싸준다.
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { AuthProvider } from './contexts/AuthContext';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>
);
App.jsx리팩토링: 이제App.jsx는 더 이상 인증 상태를 직접 관리할 필요가 없다.useAuth훅을 사용하여 컨텍스트로부터 상태를 가져오기만 하면 된다.
// src/App.jsx (리팩토링 후)
import { useAuth } from './contexts/AuthContext';
import Auth from './Auth';
import Account from './Account';
function App() {
const { session } = useAuth();
return (
<div className="container" style={{ padding: '50px 0 100px 0' }}>
{!session? <Auth /> : <Account key={session.user.id} session={session} />} </div>
);
}
export default App;
이 아키텍처를 적용하면 prop drilling 문제가 해결될 뿐만 아니라, 인증 관련 로직이 AuthProvider라는 하나의 장소에 캡슐화되어 코드의 응집도가 높아진다. 또한 react-router-dom과 같은 라우팅 라이브러리와 결합하여 사용자가 로그인해야만 접근할 수 있는 ’보호된 라우트(Protected Route)’를 매우 효율적으로 구현할 수 있는 기반이 마련된다.10
5.2 견고한 오류 처리 아키텍처
사용자 경험의 질은 예외 상황, 즉 오류를 얼마나 잘 처리하는지에 따라 크게 좌우된다. Supabase 인증 함수가 반환하는 error 객체를 체계적으로 처리하는 것은 프로덕션 수준의 애플리케이션을 위한 필수 요건이다.
단순히 if (error) { alert(error.message) }와 같이 처리하는 방식은 몇 가지 심각한 문제점을 가진다.4
-
사용자 비친화적 메시지:
Invalid login credentials와 같은 개발자 중심의 영문 메시지는 최종 사용자에게 혼란을 준다.19 -
오류 유형 식별 불가:
error.message문자열에 의존하여 오류를 분기 처리하는 것은 불안정하다. 메시지 텍스트는 라이브러리 업데이트에 따라 변경될 수 있기 때문이다. -
다국어 지원의 어려움: 메시지 문자열을 하드코딩하여 비교하면 다국어 환경에 대응하기 어렵다.
Supabase 클라이언트가 반환하는 error 객체는 AuthApiError 타입의 구조화된 객체로, message 외에도 status (HTTP 상태 코드)와 code (서버 정의 오류 코드)와 같은 유용한 정보를 포함하고 있다.20 이 중 error.code는 오류의 근본 원인을 프로그램적으로 식별할 수 있는 가장 신뢰할 수 있는 수단이다.
예를 들어, handleSignIn 함수를 다음과 같이 개선할 수 있다.
// src/Auth.jsx (오류 처리 개선)
//...
const [errorMessage, setErrorMessage] = useState('');
const handleSignIn = async (event) => {
event.preventDefault();
setLoading(true);
setErrorMessage(''); // 이전 오류 메시지 초기화
const { error } = await supabase.auth.signInWithPassword({
email: email,
password: password,
});
if (error) {
// error.code를 기반으로 사용자 친화적인 메시지를 생성한다.
switch (error.code) {
case 'invalid_grant':
setErrorMessage('이메일 또는 비밀번호가 올바르지 않습니다.');
break;
case 'user_not_found':
setErrorMessage('등록되지 않은 이메일입니다.');
break;
case 'email_not_confirmed':
setErrorMessage('이메일 인증을 완료해주세요.');
break;
default:
setErrorMessage('로그인 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
}
}
setLoading(false);
};
//...
return (
<div>
{/*... 폼... */} {errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>} </div>
);
이 접근 방식은 error.code를 사용하여 각기 다른 오류 상황에 대해 구체적이고 사용자 친화적인 피드백을 제공한다. 이는 사용자가 문제를 스스로 해결하도록 돕고, 불필요한 고객 지원 문의를 줄이며, 전반적인 애플리케이션의 신뢰도를 높인다. 이처럼 error.code 기반의 체계적인 오류 처리는 애플리케이션을 단순한 ‘작동하는’ 수준에서 ‘사용자 친화적이고 신뢰할 수 있는’ 수준으로 격상시키는 중요한 기술적 결정이다.
다음 표는 Supabase 인증 시 자주 발생하는 주요 AuthApiError 코드와 그에 대한 권장 대응 방안을 정리한 것이다.
오류 코드 (code) | HTTP 상태 (status) | 의미 (Meaning) | 사용자에게 보여줄 추천 메시지 (한국어) |
|---|---|---|---|
invalid_grant | 400 | “Invalid login credentials“와 함께 반환됨. 이메일이 존재하지 않거나 비밀번호가 틀렸음을 의미. | “이메일 또는 비밀번호가 올바르지 않습니다. 다시 확인해주세요.” |
user_not_found | 400 | 제공된 이메일에 해당하는 사용자를 찾을 수 없음. | “등록되지 않은 이메일 주소입니다. 회원가입을 먼저 진행해주세요.” |
email_not_confirmed | 400 | 사용자가 이메일 인증을 완료하지 않았음. | “이메일 인증이 필요합니다. 가입 시 발송된 이메일을 확인해주세요.” |
weak_password | 422 | 제공된 비밀번호가 프로젝트에 설정된 보안 정책(최소 길이, 문자 종류 등)을 만족하지 못함. | “비밀번호가 너무 단순합니다. 최소 8자 이상, 영문/숫자/특수문자를 포함하여 다시 설정해주세요.” |
over_email_send_rate_limit | 429 | 동일한 이메일 주소로 너무 많은 요청(예: 비밀번호 재설정)이 단시간에 발생함. | “요청 횟수를 초과했습니다. 잠시 후 다시 시도해주세요.” |
user_already_exists | 422 | 가입하려는 이메일 주소가 이미 시스템에 존재함. | “이미 가입된 이메일 주소입니다. 로그인을 시도하거나 비밀번호를 찾아주세요.” |
6. (선택) UI 라이브러리를 활용한 빠른 구현: @supabase/auth-ui-react
지금까지 인증 UI를 직접 HTML과 CSS로 구현하는 방법을 살펴보았다. 이 방식은 디자인의 완전한 자율성을 보장하지만, 모든 UI 요소를 처음부터 만드는 데 시간이 소요된다. 빠른 프로토타이핑이나 표준적인 디자인으로 충분한 프로젝트의 경우, Supabase가 공식적으로 제공하는 UI 라이브러리인 @supabase/auth-ui-react를 사용하는 것이 효율적인 대안이 될 수 있다.
이 라이브러리는 이메일/비밀번호 로그인, 소셜 로그인, Magic Link 등 Supabase Auth가 제공하는 대부분의 기능을 지원하는 사전 제작된(pre-made) React 컴포넌트를 제공한다.7
6.1 라이브러리 설치
먼저 필요한 패키지를 설치한다. @supabase/auth-ui-shared는 테마와 스타일을 공유하기 위한 패키지다.11
npm install @supabase/auth-ui-react @supabase/auth-ui-shared
6.2 <Auth /> 컴포넌트 사용법
기존에 직접 만들었던 Auth.jsx 컴포넌트를 @supabase/auth-ui-react의 <Auth /> 컴포넌트를 사용하도록 수정할 수 있다.
// src/Auth.jsx (auth-ui-react 사용)
import { Auth } from '@supabase/auth-ui-react';
import { ThemeSupa } from '@supabase/auth-ui-shared';
import { supabase } from './supabaseClient';
export default function AuthComponent() {
return (
<div style={{ maxWidth: '420px', margin: 'auto' }}>
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }} providers={['google', 'github']} localization={{
variables: { sign_in: { email_label: '이메일 주소', password_label: '비밀번호', email_input_placeholder: '이메일 주소를 입력하세요', password_input_placeholder: '비밀번호를 입력하세요', button_label: '로그인', link_text: '이미 계정이 있으신가요? 로그인하세요', }, sign_up: { email_label: '이메일 주소', password_label: '비밀번호', button_label: '가입하기', link_text: '계정이 없으신가요? 가입하세요', }, //... 기타 텍스트 현지화 }, }} />
</div>
);
}
-
supabaseClient: 필수 prop으로, 이전에 생성한supabase클라이언트 인스턴스를 전달한다.7 -
appearance: UI의 테마를 지정한다.ThemeSupa는 Supabase 대시보드와 유사한 기본 테마를 제공한다.11 -
providers: 배열 형태로 사용할 소셜 로그인 제공자의 ID를 전달한다. 예를 들어['google', 'github']를 전달하면 구글과 깃헙 로그인 버튼이 자동으로 생성된다.7 이 기능을 사용하려면 Supabase 대시보드의
Authentication > Providers 메뉴에서 해당 제공자를 미리 활성화하고 클라이언트 ID와 시크릿 키를 설정해야 한다.7
localization: UI에 표시되는 텍스트를 다른 언어로 변경(현지화)할 수 있는 기능을 제공한다.
이 라이브러리를 사용하는 것은 개발 속도와 커스터마이징 사이의 트레이드오프 관계에 있다.
-
장점:
-
개발 속도 향상: 수십 줄의 코드를 단 하나의 컴포넌트로 대체하여 인증 UI를 즉시 구현할 수 있다.
-
검증된 UI/UX: 접근성과 사용자 경험을 고려하여 설계된 컴포넌트를 바로 사용할 수 있다.
-
다양한 기능 내장: 비밀번호 재설정, 소셜 로그인 등 복잡한 기능을 별도의 구현 없이 사용할 수 있다.
-
단점:
-
제한적인 커스터마이징:
appearanceprop을 통해 색상, 폰트, 간격 등 일부 스타일을 변경할 수 있지만, HTML 구조 자체를 변경하거나 복잡한 디자인 시스템에 완벽하게 통합하는 데는 한계가 있을 수 있다.
@supabase/auth-ui-react의 존재는 Backend-as-a-Service(BaaS) 플랫폼이 백엔드 로직 제공을 넘어, 프론트엔드 개발의 상용구 코드(boilerplate)까지 줄여주려는 진화 방향을 보여준다. 개발자는 ’인증 폼을 어떻게 만들까’라는 반복적인 고민에서 벗어나, ’우리 서비스에 가장 적합한 인증 경험은 무엇인가’와 같은 핵심 비즈니스 로직에 더 많은 시간을 할애할 수 있게 된다. 프로젝트의 요구사항, 디자인의 복잡성, 개발 일정을 종합적으로 고려하여 직접 UI를 구현할지, 아니면 이 라이브러리를 활용할지 전략적으로 선택하는 것이 중요하다.
7. 결론
본 안내서는 React 애플리케이션에 Supabase를 활용하여 사용자 인증 시스템을 구축하는 전 과정을 체계적으로 다루었다. 이 과정에서 다음과 같은 핵심적인 개념과 아키텍처 패턴을 분석했다.
-
체계적인 환경 설정: Supabase 프로젝트 생성, ’User Management Starter’를 통한 데이터베이스 스키마 구성, 그리고 Vite 기반 React 프로젝트 초기화에 이르는 견고한 개발 환경 구축의 중요성을 확인했다.
-
반응형 세션 관리:
supabase.auth.onAuthStateChange리스너가 Supabase의 비동기 인증 이벤트와 React의 선언적 상태 관리 시스템을 연결하는 핵심적인 역할을 수행함을 분석했다. 이를 통해 로그인, 로그아웃과 같은 상태 변화에 따라 UI가 자동으로 업데이트되는 반응형 아키텍처를 구현했다. -
핵심 인증 API:
signUp,signInWithPassword,signOut함수의 정확한 사용법과 반환 값을 이해하고, 이를 통해 기본적인 인증 플로우를 완성했다. -
전역 상태 관리 아키텍처:
prop drilling문제점을 해결하고 코드의 유지보수성을 높이기 위해 React Context API를 활용한AuthProvider패턴을 도입했다. 이는 확장 가능한 애플리케이션을 위한 필수적인 아키텍처다. -
견고한 오류 처리 전략: 사용자 경험을 저해하는
error.message기반의 단순한 오류 처리를 지양하고,error.code를 활용하여 오류 유형별로 구체적이고 사용자 친화적인 피드백을 제공하는 체계적인 오류 처리 전략의 중요성을 강조했다.
이 안내서에서 다룬 내용을 기반으로 실제 프로덕션 환경에 애플리케이션을 배포하기 위해서는 다음과 같은 후속 단계들을 고려해야 한다.
-
이메일 인증 활성화: 개발 단계에서 비활성화했던 ‘Confirm email’ 옵션을 다시 활성화하고, 사용자가 이메일 링크를 클릭해야만 로그인이 완료되는 전체 사용자 플로우를 구현한다.
-
보호된 라우트(Protected Routes) 구현:
react-router-dom과AuthProvider를 결합하여, 로그인한 사용자만 접근할 수 있는 페이지(예: 대시보드, 마이페이지)를 구현한다.10 -
소셜 로그인(OAuth) 연동: Google, GitHub, Facebook 등 다양한 OAuth 제공자를 연동하여 사용자에게 더 편리한 로그인 옵션을 제공한다.7
-
비밀번호 재설정 기능 구현: 사용자가 비밀번호를 잊어버렸을 때 재설정할 수 있는 기능을
supabase.auth.resetPasswordForEmail과supabase.auth.updateUser함수를 사용하여 구현한다.17
Supabase는 개발자가 백엔드 인프라 구축의 복잡성에서 벗어나 핵심 비즈니스 로직과 사용자 경험 향상에 집중할 수 있도록 강력한 도구를 제공한다. 본 안내서에서 제시된 원칙과 패턴을 적용함으로써, 개발자는 안정적이고 확장 가능하며 보안이 강화된 현대적인 웹 애플리케이션을 효율적으로 구축할 수 있을 것이다.
8. 참고 자료
- Auth | Supabase Docs, https://supabase.com/docs/guides/auth
- Build a User Management App with Expo React Native | Supabase Docs, https://supabase.com/docs/guides/getting-started/tutorials/with-expo-react-native
- Authentication in React with Supabase - OpenReplay Blog, https://blog.openreplay.com/authentication-in-react-with-supabase/
- Build a User Management App with React | Supabase Docs, https://supabase.com/docs/guides/getting-started/tutorials/with-react
- Build a User Management App with Next.js | Supabase Docs, https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs
- Supabase + React Router: Signup and Auth Setup (Part 2) - DEV Community, https://dev.to/kevinccbsg/supabase-react-router-signup-and-auth-setup-part-2-3ngp
- How to authenticate React applications with Supabase Auth …, https://blog.logrocket.com/authenticate-react-applications-supabase-auth/
- Use Supabase with React, https://supabase.com/docs/guides/getting-started/quickstarts/reactjs
- User sessions | Supabase Docs, https://supabase.com/docs/guides/auth/sessions
- Protected Routes in React Router 6 with Supabase Authentication …, https://medium.com/@seojeek/protected-routes-in-react-router-6-with-supabase-authentication-and-oauth-599047e08163
- Authenticate your React App with Supabase - Arindam Majumder, https://arindam1729.hashnode.dev/authenticate-your-react-app-with-supabase
- Manage Authentication State in React with AuthContext | by Jad …, https://medium.com/@jadghamloush/manage-authentication-state-in-react-with-authcontext-2d3129eac92b
- Use Supabase Auth with React Native, https://supabase.com/docs/guides/auth/quickstarts/react-native
- JavaScript: Create a new user | Supabase Docs, https://supabase.com/docs/reference/javascript/auth-signup
- Supabase signup with react state email and password - Stack Overflow, https://stackoverflow.com/questions/75437776/supabase-signup-with-react-state-email-and-password
- JavaScript: Sign in a user | Supabase Docs, https://supabase.com/docs/reference/javascript/auth-signinwithpassword
- Password-based Auth | Supabase Docs, https://supabase.com/docs/guides/auth/passwords
- Nexts.js 13 + Supabase > What’s the proper way to create a user context - Stack Overflow, https://stackoverflow.com/questions/74661653/nexts-js-13-supabase-whats-the-proper-way-to-create-a-user-context
- Supabase Auth: auth.signInWithPassword always returns Invalid login credentials, https://stackoverflow.com/questions/79754981/supabase-auth-auth-signinwithpassword-always-returns-invalid-login-credentials
- Error Codes | Supabase Docs, https://supabase.com/docs/guides/auth/debugging/error-codes
- Issue with error handling in Supabase Auth - Reddit, https://www.reddit.com/r/Supabase/comments/1du3sxv/issue_with_error_handling_in_supabase_auth/
- The correct way to handle errors in AuthApiError : r/Supabase - Reddit, https://www.reddit.com/r/Supabase/comments/1es0c2p/the_correct_way_to_handle_errors_in_authapierror/