クラウドベースの完全(かんぜん)なバックエンドソリューションです。
データベース、認証(にんしょう)、ストレージなどを提供(ていきょう)します。
클라우드 기반의 완전한 백엔드 솔루션입니다.
데이터베이스, 인증, 스토리지 등을 제공합니다.
Supabase가 제공하는 기능:
✅ PostgreSQL 데이터베이스
- 관계형 DB
- SQL 쿼리
- 실시간 구독
✅ 인증/인가 (Authentication/Authorization)
- 이메일 로그인
- 소셜 로그인
- JWT 토큰
✅ 스토리지 (Storage)
- 이미지 업로드
- 파일 관리
- CDN 제공
✅ Edge Functions
- 서버리스 함수
- API 엔드포인트
✅ 실시간 기능
- WebSocket
- 데이터 동기화
→ Firebase의 오픈소스 대안!
Supabaseサイトでプロジェクトを作成(さくせい)します。
アカウント登録(とうろく)とプロジェクト設定(せってい)を行(おこな)います。
Supabase 사이트에서 프로젝트를 생성합니다.
계정 등록과 프로젝트 설정을 진행합니다.
https://supabase.com 접속
1. "Start your project" 클릭
2. 프로젝트 정보 입력:
- Organization: 개인 또는 팀
- Project Name: hanib-log
- Database Password: MajGlIrZ2VKuiqyP
- Region: Northeast Asia (Seoul)
3. "Create new project" 클릭
4. 프로젝트 생성 완료!
(약 2분 소요)
Supabase クライアント設定(せってい)
프로젝트 대시보드에서:
1. "Connect" 버튼 클릭
2. Framework 선택:
App Frameworks → React → Vite
3. 제공된 코드 확인:
- .env 파일 내용
- supabase.ts 파일 내용
環境変数(かんきょうへんすう) 設定(せってい) (.env)
# .env (프로젝트 루트에 생성)
VITE_SUPABASE_URL=https://aeohvjutjbvfpvfsajsf.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# ⚠️ 중요:
# - 실제 값은 Supabase 대시보드에서 복사
# - .env는 .gitignore에 추가 (공개 금지!)
# - VITE_ 접두사 필수 (Vite 환경변수 규칙)
Supabase クライアント作成(さくせい)
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
// 환경변수에서 값 가져오기
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
// Supabase 클라이언트 생성
const supabase = createClient(supabaseUrl, supabaseKey);
export default supabase;
/*
작동 원리:
1. createClient():
- Supabase와 연결
- 자동 인증 처리
- API 요청 관리
2. supabaseUrl:
- 프로젝트 고유 URL
- API 엔드포인트
3. supabaseKey:
- 익명 접근 키
- 공개해도 안전 (RLS로 보호)
- 클라이언트 사이드 전용
*/
라이브러리(らいぶらりー) 설치(せっち)
# Supabase JavaScript 클라이언트
npm install @supabase/supabase-js
# 설치 확인
npm list @supabase/supabase-js
# @supabase/supabase-js@2.39.0
SNS投稿(とうこう)データを保存(ほぞん)するテーブルです。
各(かく)カラムの意味(いみ)と設定(せってい)を理解(りかい)します。
SNS 게시글 데이터를 저장하는 테이블입니다.
각 컬럼의 의미와 설정을 이해합니다.
Supabase ダッシュボードで作成(さくせい)
1. Database → Table Editor 이동
2. "New table" 클릭
3. 테이블 설정:
- Name: post
- Description: SNS 게시글
4. RLS (Row Level Security) 설정:
❌ Enable Row Level Security 체크 해제
(나중에 설정 예정)
RLS란?
- 행 단위 보안
- 사용자별 접근 제어
- "자기 게시글만 수정" 등
カラム構成(こうせい)
| カラム名(めい) | 타입(たいぷ) | 설정(せってい) | 의미(いみ) |
|---|---|---|---|
id | int8 | Primary Key, Auto-increment | 게시글 고유(こゆう) ID |
created_at | timestamptz | Default: now() | 생성(せいせい) 시간(じかん) |
content | text | Not Nullable | 게시글 내용(ないよう) |
image_urls | text[] | Nullable, Array | 이미지 URL 배열(はいれつ) |
like_count | int8 | Default: 0, Not Nullable | 좋아요(すき) 수(すう) |
author_id | uuid | Default: auth.uid(), Not Nullable | 작성자(さくせいしゃ) ID |
詳細(しょうさい)な列(れつ)設定(せってい)
-- === 1. id 컬럼 ===
Column Name: id
Type: int8 (bigint)
✅ Is Identity: 체크 (자동 증가)
✅ Is Primary Key: 체크
Default Value: (자동)
/*
int8 = 64비트 정수
- 범위: -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
- SNS 게시글은 수백만 개 가능
→ int8 필요!
*/
-- === 2. created_at 컬럼 ===
Column Name: created_at
Type: timestamptz (timestamp with timezone)
Default Value: now()
✅ Is Nullable: 해제
/*
timestamptz:
- 시간대 포함 타임스탬프
- UTC 자동 변환
- 예: 2026-02-01T12:34:56.789+09:00
now():
- 현재 시간 자동 입력
- 게시글 생성 시각 기록
*/
-- === 3. content 컬럼 ===
Column Name: content
Type: text
✅ Is Nullable: 해제 (내용 필수!)
/*
text:
- 무제한 길이 문자열
- varchar보다 유연
- 긴 게시글 가능
*/
-- === 4. image_urls 컬럼 ===
Column Name: image_urls
Type: text
✅ Is Nullable: 체크 (이미지 선택사항)
✅ Define as Array: 체크
/*
text[]:
- 문자열 배열
- PostgreSQL 네이티브 배열
- 예: ["url1.jpg", "url2.png"]
왜 배열?
- 여러 이미지 첨부 가능
- 순서 유지
- 쿼리 편리
*/
-- === 5. like_count 컬럼 ===
Column Name: like_count
Type: int8
Default Value: 0
✅ Is Nullable: 해제
/*
좋아요 카운터:
- 초기값 0
- 클릭 시 +1
- 취소 시 -1
*/
-- === 6. author_id 컬럼 ===
Column Name: author_id
Type: uuid
Default Value: auth.uid()
✅ Is Nullable: 해제
/*
uuid:
- 128비트 고유 식별자
- 예: 550e8400-e29b-41d4-a716-446655440000
auth.uid():
- Supabase 인증 함수
- 현재 로그인한 사용자 ID
- 자동으로 설정됨!
작동 원리:
1. 사용자 로그인
2. JWT 토큰 발급
3. 게시글 작성 시 auth.uid() 호출
4. 자동으로 author_id에 사용자 ID 저장
*/
テーブル作成(さくせい) 완료(かんりょう)
-- 최종 SQL (자동 생성됨)
CREATE TABLE public.post (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
content text NOT NULL,
image_urls text[],
like_count bigint NOT NULL DEFAULT 0,
author_id uuid NOT NULL DEFAULT auth.uid()
);
-- 테이블 생성 후:
"Save" 버튼 클릭!
コマンドラインツールをインストールします。
データベーススキーマから型(かた)を生成(せいせい)します。
커맨드라인 도구를 설치합니다.
데이터베이스 스키마에서 타입을 생성합니다.
1단계(だんかい): Supabase CLI 설치(せっち)
# DevDependency로 설치
npm install supabase@">=1.8.1" --save-dev
# 설치 확인
npx supabase --version
# Supabase CLI 1.142.0
2단계(だんかい): 로그인(ろぐいん)
# Supabase 계정으로 로그인
npx supabase login
# 브라우저가 자동으로 열림
# → "Authorize" 클릭
# → 터미널에 성공 메시지 표시
# 확인:
# ✓ Logged in as your-email@example.com
3단계(だんかい): 프로젝트(ぷろじぇくと) 초기화(しょきか)
# 현재 프로젝트에 Supabase 설정 생성
npx supabase init
# 질문에 답하기:
? Generate VS Code settings for Deno? [y/N] n
? Generate IntelliJ Settings for Deno? [y/N] n
# 결과:
# ✓ supabase/config.toml 생성됨
4단계(だんかい): 타입(たいぷ) 생성(せいせい) 스크립트(すくりぷと) 추가(ついか)
// package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
// ✅ 타입 생성 스크립트 추가
"type-gen": "npx supabase gen types typescript --project-id \"aeohvjutjbvfpvfsajsf\" --schema public > src/database.types.ts"
}
}
/*
명령어 분석:
npx supabase gen types typescript
→ TypeScript 타입 생성
--project-id "aeohvjutjbvfpvfsajsf"
→ 프로젝트 ID (Supabase 대시보드에서 확인)
--schema public
→ public 스키마만 (기본값)
> src/database.types.ts
→ 파일로 저장
*/
5단계(だんかい): 타입(たいぷ) 생성(せいせい) 실행(じっこう)
# 타입 생성
npm run type-gen
# 성공 메시지:
# ✓ Generated types from public schema
# 결과 파일:
# src/database.types.ts 생성됨!
src/database.types.ts (주요(しゅよう) 부분(ぶぶん))
// === JSON 타입 ===
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
// === 데이터베이스 전체 타입 ===
export type Database = {
public: {
Tables: {
// === post 테이블 타입 ===
post: {
// 조회 시 반환되는 타입
Row: {
author_id: string // uuid → string
content: string
created_at: string // timestamptz → string
id: number // int8 → number
image_urls: string[] | null
like_count: number
}
// 삽입 시 사용하는 타입 (선택적 필드)
Insert: {
author_id?: string // auth.uid() 기본값
content?: string
created_at?: string // now() 기본값
id?: number // 자동 증가
image_urls?: string[] | null
like_count?: number // 0 기본값
}
// 업데이트 시 사용하는 타입 (모두 선택적)
Update: {
author_id?: string
content?: string
created_at?: string
id?: number
image_urls?: string[] | null
like_count?: number
}
Relationships: [] // 외래키 관계 (없음)
}
}
Views: {}
Functions: {}
Enums: {}
CompositeTypes: {}
}
}
// === 편리한 헬퍼 타입 ===
export type Tables<TableName extends keyof Database['public']['Tables']> =
Database['public']['Tables'][TableName]['Row']
export type TablesInsert<TableName extends keyof Database['public']['Tables']> =
Database['public']['Tables'][TableName]['Insert']
export type TablesUpdate<TableName extends keyof Database['public']['Tables']> =
Database['public']['Tables'][TableName]['Update']
/*
사용 예시:
// Row 타입
type Post = Tables<'post'>
// {
// author_id: string,
// content: string,
// created_at: string,
// id: number,
// image_urls: string[] | null,
// like_count: number
// }
// Insert 타입
type PostInsert = TablesInsert<'post'>
// {
// author_id?: string,
// content?: string,
// ...
// }
*/
src/lib/supabase.ts (업데이트)
import { createClient } from '@supabase/supabase-js';
import { type Database } from '@/database.types'; // ✅ 타입 import
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
// ✅ Generic으로 Database 타입 지정
const supabase = createClient<Database>(supabaseUrl, supabaseKey);
export default supabase;
/*
타입 안전성 향상!
Before (타입 없음):
const { data } = await supabase.from('post').select('*');
// data 타입: any
After (타입 있음):
const { data } = await supabase.from('post').select('*');
// data 타입: Post[] | null
// → IDE 자동완성!
// → 오타 방지!
*/
src/types.ts
import { type Tables } from '@/database.types';
// === Post 타입 (더 쉬운 이름) ===
export type Post = Tables<'post'>;
/*
사용:
import { type Post } from '@/types';
let post: Post = {
id: 1,
content: "안녕하세요!",
author_id: "...",
created_at: "2026-02-01T...",
image_urls: null,
like_count: 0
};
*/
// === 추가 타입 (필요시) ===
export type PostWithAuthor = Post & {
author: {
id: string;
email: string;
name: string;
}
};
/*
조인 쿼리 결과용:
const { data } = await supabase
.from('post')
.select('*, author:profiles(*)')
data 타입: PostWithAuthor[]
*/
타입(たいぷ) 자동(じどう) 업데이트(あっぷでーと) 워크플로우(わーくふろー)
# 데이터베이스 수정 후:
# 1. Supabase 대시보드에서 테이블 변경
# (컬럼 추가, 수정 등)
# 2. 타입 재생성
npm run type-gen
# 3. 변경사항 확인
git diff src/database.types.ts
# 4. 코드 업데이트
# (타입 에러 수정)
# 자동화 가능!
# - Git hooks
# - CI/CD 파이프라인
基本(きほん)概念(がいねん)
認証(にんしょう)(Authentication)とは本人確認(ほんにんかくにん)です。
認可(にんか)(Authorization)とは権限確認(けんげんかくにん)です。
인증(Authentication)은 신원 확인입니다.
인가(Authorization)는 권한 확인입니다.
=== 인증 (Authentication) ===
질문: "당신은 누구입니까?"
예시:
- 로그인 (이메일 + 비밀번호)
- 지문 인식
- OTP 인증
- 소셜 로그인 (Google, Kakao)
목적:
사용자의 신원을 확인
프로세스:
1. 사용자 정보 입력
2. 데이터베이스 확인
3. 일치하면 인증 성공
4. 토큰/세션 발급
=== 인가 (Authorization) ===
질문: "당신은 무엇을 할 수 있습니까?"
예시:
- 관리자 페이지 접근
- 자기 게시글만 삭제
- 유료 콘텐츠 조회
- 파일 업로드 권한
목적:
인증된 사용자의 권한 확인
프로세스:
1. 인증 완료 (이미 로그인됨)
2. 요청한 작업 확인
3. 권한 체크
4. 허용/거부
=== 차이점 요약 ===
┌──────────┬──────────┬──────────┐
│ │ 인증 │ 인가 │
├──────────┼──────────┼──────────┤
│ 질문 │ 누구? │ 뭘 할 수? │
│ 순서 │ 먼저 │ 나중에 │
│ 예시 │ 로그인 │ 권한체크 │
│ 결과 │ 토큰발급 │ 허용/거부 │
└──────────┴──────────┴──────────┘
実例(じつれい): SNS 게시글(げしこうきゅう) 삭제(さくじょ)
// === 게시글 삭제 프로세스 ===
async function deletePost(postId: number) {
// === 1. 인증 체크 ===
const user = await supabase.auth.getUser();
if (!user.data.user) {
throw new Error("로그인이 필요합니다"); // 인증 실패!
}
console.log(`사용자 인증됨: ${user.data.user.id}`);
// === 2. 인가 체크 ===
const { data: post } = await supabase
.from('post')
.select('author_id')
.eq('id', postId)
.single();
if (post.author_id !== user.data.user.id) {
throw new Error("삭제 권한이 없습니다"); // 인가 실패!
}
console.log("권한 확인 완료!");
// === 3. 삭제 실행 ===
const { error } = await supabase
.from('post')
.delete()
.eq('id', postId);
if (error) throw error;
console.log("삭제 완료!");
}
/*
시나리오:
1. 로그인 안 함:
→ 인증 실패 ❌
2. A 사용자 로그인, B의 게시글 삭제 시도:
→ 인증 성공 ✅
→ 인가 실패 ❌
3. A 사용자 로그인, 자기 게시글 삭제:
→ 인증 성공 ✅
→ 인가 성공 ✅
→ 삭제 완료 ✅
*/
両方(りょうほう)必要(ひつよう)한 理由(りゆう)
보안을 위해서는 둘 다 필요!
인증만 있으면:
❌ 로그인한 사람은 누구나 모든 게시글 삭제 가능
❌ 관리자 페이지 아무나 접근
❌ 다른 사람 정보 수정 가능
인가만 있으면:
❌ 누가 요청했는지 모름
❌ 익명 사용자가 권한 우회 가능
❌ 신원 확인 불가
인증 + 인가:
✅ 누가 (인증)
✅ 무엇을 할 수 있는지 (인가)
✅ 완벽한 보안!
サーバーにセッションオブジェクトを保存(ほぞん)します。
セッションIDでユーザーを管理(かんり)します。
서버에 세션 객체를 저장합니다.
세션 ID로 사용자를 관리합니다.
動作(どうさ)原理(げんり)
=== 세션 방식 프로세스 ===
1. 로그인 요청:
Client → Server
{ email: "user@example.com", password: "****" }
2. 서버가 세션 생성:
Server Memory/DB:
{
sessionId: "abc123xyz",
userId: "550e8400-...",
email: "user@example.com",
loginAt: "2026-02-01T12:00:00",
expiresAt: "2026-02-01T14:00:00"
}
3. 세션 ID를 쿠키로 전송:
Server → Client
Set-Cookie: sessionId=abc123xyz; HttpOnly; Secure
4. 클라이언트가 저장:
Browser Cookie Storage:
sessionId=abc123xyz
5. 이후 요청마다 자동 전송:
Client → Server
Cookie: sessionId=abc123xyz
GET /api/posts
6. 서버가 세션 확인:
sessionId로 DB 조회
→ 사용자 정보 획득
→ 요청 처리
實裝(じっそう)例(れい) (Express.js)
// === 서버 코드 (Node.js + Express) ===
const express = require('express');
const session = require('express-session');
const app = express();
// 세션 미들웨어 설정
app.use(session({
secret: 'my-secret-key', // 암호화 키
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60, // 1시간
httpOnly: true, // JavaScript 접근 차단
secure: true // HTTPS만
}
}));
// 로그인
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// 사용자 확인 (DB 조회)
const user = await db.findUser(email, password);
if (user) {
// 세션에 사용자 정보 저장
req.session.userId = user.id;
req.session.email = user.email;
res.json({ success: true });
} else {
res.status(401).json({ error: "로그인 실패" });
}
});
// 인증 필요한 엔드포인트
app.get('/api/posts', (req, res) => {
// 세션 확인
if (!req.session.userId) {
return res.status(401).json({ error: "로그인 필요" });
}
// 로그인된 사용자만 접근
const posts = db.getPosts();
res.json(posts);
});
// 로그아웃
app.post('/logout', (req, res) => {
req.session.destroy(); // 세션 삭제
res.json({ success: true });
});
長所(ちょうしょ)와 短所(たんしょ)
✅ 장점:
1. 사용자 상태 관리 가능:
- 현재 접속자 수 확인
- 중복 로그인 방지
- 강제 로그아웃 가능
2. 보안 강화:
- 서버에서 세션 제어
- 즉시 무효화 가능
- 세션 ID만 클라이언트 노출
3. 추가 정보 저장:
- 장바구니
- 임시 데이터
- 사용자 설정
❌ 단점:
1. 메모리 과다 사용:
사용자 1,000명:
→ 1,000개 세션 객체
→ 메모리 부담 증가
2. DB 부하:
모든 요청마다:
→ DB에서 세션 조회
→ 사용자 많으면 과부하
3. 확장성 문제:
서버 여러 대 사용 시:
→ 세션 공유 필요
→ Redis 등 추가 인프라
4. 모바일 앱 불편:
쿠키 사용 어려움
JWT (JSON Web Token) 구조(こうぞう)
トークンを使用(しよう)して認証(にんしょう)します。
サーバーに状態(じょうたい)を保存(ほぞん)しません。
토큰을 사용해서 인증합니다.
서버에 상태를 저장하지 않습니다.
=== JWT 구조 ===
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
구조 (3부분):
Header.Payload.Signature
=== 1. Header (헤더) ===
{
"alg": "HS256", // 암호화 알고리즘
"typ": "JWT" // 토큰 타입
}
Base64 인코딩:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
=== 2. Payload (페이로드) ===
{
"sub": "550e8400-...", // 사용자 ID
"email": "user@example.com", // 이메일
"name": "강민수", // 이름
"iat": 1709280000, // 발급 시간
"exp": 1709283600 // 만료 시간 (1시간 후)
}
Base64 인코딩:
eyJzdWIiOiI1NTBlODQwMC0uLi4iLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJuYW1lIjoi6rCV66+86IiYIiwiaWF0IjoxNzA5MjgwMDAwLCJleHAiOjE3MDkyODM2MDB9
=== 3. Signature (서명) ===
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret_key
)
결과:
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT 動作(どうさ)原理(げんり)
=== JWT 인증 프로세스 ===
1. 로그인:
Client → Server
POST /login
{ email: "user@example.com", password: "****" }
2. 서버가 JWT 생성:
- Payload에 사용자 정보 포함
- Secret Key로 Signature 생성
- 토큰 반환
Server → Client
{
accessToken: "eyJhbGci...",
refreshToken: "eyJhbGci..."
}
3. 클라이언트가 저장:
LocalStorage or SessionStorage:
accessToken: "eyJhbGci..."
4. 이후 요청마다 토큰 전송:
Client → Server
Authorization: Bearer eyJhbGci...
GET /api/posts
5. 서버가 토큰 검증:
✓ Signature 확인 (위조 여부)
✓ 만료 시간 확인
✓ Payload에서 사용자 정보 추출
→ DB 조회 없이 인증 완료!
偽造(ぎぞう)防止(ぼうし)原理(げんり)
=== 토큰 위조 시도 ===
해커가 Payload 변경 시도:
원본:
Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "userId": "123", "role": "user" }
Secret: "my-secret-key"
Signature = HMAC(Header + Payload, Secret)
= "abc123..."
해커가 role을 admin으로 변경:
Payload: { "userId": "123", "role": "admin" } // 변조!
하지만 Signature는 그대로:
Signature = "abc123..." // 원본 그대로
서버 검증:
New Signature = HMAC(Header + New Payload, Secret)
= "xyz789..." // 다름!
"abc123..." ≠ "xyz789..."
→ 위조 탐지! ❌
결론:
Secret Key 없이는 유효한 Signature 생성 불가!
→ 위조 사실상 불가능!
實裝(じっそう)例(れい)
// === 서버 코드 (Node.js) ===
import jwt from 'jsonwebtoken';
const SECRET_KEY = 'my-super-secret-key-keep-it-safe';
// JWT 생성
function generateToken(user: User) {
const payload = {
sub: user.id,
email: user.email,
name: user.name
};
// 1시간 유효
const accessToken = jwt.sign(payload, SECRET_KEY, {
expiresIn: '1h'
});
// 2주 유효
const refreshToken = jwt.sign(payload, SECRET_KEY, {
expiresIn: '14d'
});
return { accessToken, refreshToken };
}
// JWT 검증
function verifyToken(token: string) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
return decoded;
} catch (error) {
throw new Error("유효하지 않은 토큰");
}
}
// 인증 미들웨어
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: "토큰 없음" });
}
try {
const user = verifyToken(token);
req.user = user; // 요청 객체에 사용자 정보 추가
next();
} catch (error) {
return res.status(401).json({ error: "유효하지 않은 토큰" });
}
}
// === 클라이언트 코드 (React) ===
// 로그인
async function login(email: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const { accessToken, refreshToken } = await response.json();
// LocalStorage에 저장
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
}
// API 요청
async function fetchPosts() {
const token = localStorage.getItem('accessToken');
const response = await fetch('/api/posts', {
headers: {
'Authorization': `Bearer ${token}`
}
});
return await response.json();
}
長所(ちょうしょ)와 短所(たんしょ)
✅ 장점:
1. DB 조회 불필요:
- 토큰 자체에 정보 포함
- 서버 부하 감소
- 빠른 인증
2. 확장성 우수:
- Stateless (무상태)
- 서버 여러 대 가능
- 별도 인프라 불필요
3. 모바일 친화적:
- HTTP Header 사용
- 쿠키 불필요
- 앱 개발 편리
❌ 단점:
1. 상태 관리 불가:
- 접속자 수 확인 어려움
- 중복 로그인 제어 어려움
- 강제 로그아웃 불가
2. 토큰 탈취 시 대처 어려움:
- 유효기간 만료까지 사용 가능
- 즉시 무효화 불가
→ Access/Refresh Token 분리 필요!
3. 토큰 크기:
- 세션 ID보다 큼
- 매 요청마다 전송
- 네트워크 부담 증가
二重(にじゅう)トークン戦略(せんりゃく)
=== 토큰 분리 이유 ===
문제:
Access Token 유효기간:
- 길게 설정: 탈취 시 위험 ⚠️
- 짧게 설정: 자주 재로그인 😡
해결:
두 개의 토큰 사용!
=== Access Token ===
- 유효기간: 30분 ~ 1시간 (짧음)
- 용도: API 요청
- 저장: Memory or SessionStorage
- 탈취 시: 피해 최소화 (곧 만료)
=== Refresh Token ===
- 유효기간: 2주 ~ 1달 (김)
- 용도: Access Token 갱신
- 저장: HttpOnly Cookie (더 안전)
- 탈취 시: 큰 문제 (하지만 드뭄)
=== 동작 과정 ===
1. 로그인 성공:
Server → Client
{
accessToken: "...", // 1시간
refreshToken: "..." // 2주
}
2. API 요청 (정상):
Client → Server
Authorization: Bearer {accessToken}
→ 성공 ✅
3. Access Token 만료:
Client → Server
Authorization: Bearer {만료된 Token}
→ 401 Unauthorized
4. 자동 갱신:
Client → Server
POST /refresh
{ refreshToken: "..." }
Server → Client
{ accessToken: "새 토큰" }
5. 재시도:
Client → Server
Authorization: Bearer {새 토큰}
→ 성공 ✅
実装(じっそう) (Axios Interceptor)
// === 자동 토큰 갱신 (React + Axios) ===
<import axios from 'axios';
// Axios 인스턴스 생성
const api = axios.create({
baseURL: 'https://api.example.com'
});
// 요청 인터셉터: Access Token 자동 추가
api.interceptors.request.use((config) => {
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
// 응답 인터셉터: 401 시 자동 갱신
api.interceptors.response.use(
(response) => response, // 성공 시 그대로
async (error) => {
const originalRequest = error.config;
// 401 에러 && 재시도 안 했으면
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Refresh Token으로 갱신
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/auth/refresh', {
refreshToken
});
const { accessToken } = response.data;
// 새 토큰 저장
localStorage.setItem('accessToken', accessToken);
// 원래 요청 재시도
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// Refresh Token도 만료
localStorage.clear();
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default api;
/*
사용자 경험:
1. API 요청
2. Access Token 만료
3. 자동으로 갱신
4. 자동으로 재시도
5. 사용자는 아무것도 모름!
→ 끊김 없는 UX ✨
*/
Authentication 設定(せってい)
Supabase Dashboard:
1. Authentication → Providers 이동
2. Email Provider 설정:
✅ Enable Email provider
✅ Confirm email (이메일 확인 활성화)
❌ Secure email change (개발 중에는 끄기)
3. Password Requirements:
Minimum password length: 6 characters
Password requirements: No required characters
4. Email OTP 설정:
Email OTP Expiration: 3600 seconds (1시간)
Email OTP Length: 6 digits
5. Allow new users to sign up:
✅ 체크 (회원가입 허용)
6. "Save" 클릭
src/pages/sign-up-page.tsx
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useSignUp } from "@/hooks/mutations/use-sign-up";
import { useState } from "react";
import { Link } from "react-router";
/**
* 회원가입 페이지
*/
export default function SignUpPage() {
// === State 관리 ===
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
// === Mutation Hook ===
const { mutate: signUp } = useSignUp();
/**
* 회원가입 버튼 클릭 핸들러
*/
const handleSignUpClick = () => {
// 유효성 검사
if (email.trim() === "") {
alert("이메일을 입력하세요");
return;
}
if (password.trim() === "") {
alert("비밀번호를 입력하세요");
return;
}
// 회원가입 요청
signUp({
email,
password,
});
};
return (
<div className="flex flex-col gap-8">
{/* === 제목 === */}
<div className="text-xl font-bold">회원가입</div>
{/* === 입력 폼 === */}
<div className="flex flex-col gap-2">
{/* 이메일 */}
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
className="py-6"
type="email"
placeholder="example@abc.com"
/>
{/* 비밀번호 */}
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
className="py-6"
type="password"
placeholder="password"
/>
</div>
{/* === 회원가입 버튼 === */}
<div>
<Button
onClick={handleSignUpClick}
className="w-full"
>
회원가입
</Button>
</div>
{/* === 로그인 링크 === */}
<div>
<Link
className="text-muted-foreground hover:underline"
to={"/sign-in"}
>
이미 계정이 있다면? 로그인
</Link>
</div>
</div>
);
}
src/api/auth.ts
import supabase from "@/lib/supabase";
/**
* 회원가입 API
*
* @param email - 사용자 이메일
* @param password - 비밀번호
* @returns Supabase 인증 응답
*/
export async function signUp({
email,
password,
}: {
email: string;
password: string;
}) {
// Supabase Auth에 회원가입 요청
const { data, error } = await supabase.auth.signUp({
email,
password,
});
// 에러 발생 시 throw
if (error) throw error;
// 성공 시 data 반환
return data;
}
/*
응답 데이터 구조:
{
user: {
id: "550e8400-e29b-41d4-a716-446655440000",
email: "user@example.com",
created_at: "2026-02-01T12:00:00Z",
...
},
session: {
access_token: "eyJhbGci...",
refresh_token: "eyJhbGci...",
expires_in: 3600,
...
}
}
자동으로 LocalStorage에 저장됨!
- Key: sb-{project-ref}-auth-token
- Value: { access_token, refresh_token, ... }
*/
src/hooks/mutations/use-sign-up.ts
import { signUp } from "@/api/auth";
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router";
/**
* 회원가입 Mutation Hook
*/
export function useSignUp() {
const navigate = useNavigate();
return useMutation({
// Mutation 함수
mutationFn: signUp,
// 성공 시 실행
onSuccess: () => {
alert("회원가입 성공! 로그인 페이지로 이동합니다.");
navigate("/sign-in");
},
// 실패 시 실행
onError: (error: Error) => {
alert(`회원가입 실패: ${error.message}`);
},
});
}
/*
TanStack Query의 useMutation:
장점:
✅ 로딩 상태 자동 관리 (isPending)
✅ 에러 상태 자동 관리 (error)
✅ 성공/실패 콜백 지원
✅ 재시도 기능
✅ UI 업데이트 간편
사용 예시:
const { mutate, isPending, error } = useSignUp();
<Button
onClick={() => mutate({ email, password })}
disabled={isPending}
>
{isPending ? "처리 중..." : "회원가입"}
</Button>
*/
会員加入(かいいんかにゅう) 프로세스(ぷろせす)
1. 사용자 입력:
Email: user@example.com
Password: password123
2. "회원가입" 버튼 클릭
3. handleSignUpClick() 실행:
- 유효성 검사
- signUp() Mutation 호출
4. API 요청:
POST https://aeohvjutjbvfpvfsajsf.supabase.co/auth/v1/signup
{
"email": "user@example.com",
"password": "password123"
}
5. Supabase 처리:
- 사용자 생성
- JWT 토큰 발급
- 이메일 확인 메일 발송
6. 응답 수신:
{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci...",
"user": { ... }
}
7. 자동 저장:
LocalStorage에 토큰 저장
Key: sb-aeohvjutjbvfpvfsajsf-auth-token
8. onSuccess 실행:
- alert 표시
- /sign-in으로 이동
LocalStorage 確認(かくにん)
// 개발자 도구 (F12) → Console
// 저장된 토큰 확인
localStorage.getItem('sb-aeohvjutjbvfpvfsajsf-auth-token');
/*
결과:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 3600,
"expires_at": 1738483200,
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
...
}
}
Supabase가 자동으로:
✅ 토큰 저장
✅ 만료 시 갱신
✅ 로그아웃 시 삭제
→ 개발자는 신경 안 써도 됨!
*/
Supabase Dashboard 確認(かくにん)
Authentication → Users:
┌──────────────────────────────────────┐
│ Email │ Created │
├──────────────────────────────────────┤
│ user@example.com │ 2026-02-01 12:00 │
└──────────────────────────────────────┘
사용자 정보:
- ID: 550e8400-...
- Email: user@example.com
- Email Confirmed: false (이메일 확인 필요)
- Created At: 2026-02-01T12:00:00Z
- Last Sign In: null (아직 로그인 안 함)
✅ Supabase 설정:
클라우드 백엔드 솔루션
PostgreSQL + 인증 + 스토리지
무료 요금제 사용 가능
✅ 데이터베이스:
post 테이블 생성
TypeScript 타입 자동 생성
타입 안전성 확보
✅ 인증 vs 인가:
인증: 신원 확인 (누구?)
인가: 권한 확인 (무엇을?)
둘 다 필요!
✅ 세션 vs 토큰:
세션: 서버 저장, 상태 관리 가능
토큰: Stateless, 확장성 우수
Supabase는 JWT 사용
✅ JWT 구조:
Header: 알고리즘, 타입
Payload: 사용자 정보
Signature: 위조 방지
✅ Access/Refresh Token:
Access: 짧은 유효기간 (1시간)
Refresh: 긴 유효기간 (2주)
자동 갱신으로 UX 향상
✅ 회원가입:
Supabase Auth 사용
자동 토큰 관리
LocalStorage 저장