Project-hongsta(9)

Project

목록 보기
9/9
post-thumbnail

Supabase バックエンド - 한입(ひとくち)ログ 完全(かんぜん)セットアップ

🚀 Supabase 소개(しょうかい)

Supabase란?

クラウドベースの完全(かんぜん)なバックエンドソリューションです。
データベース、認証(にんしょう)、ストレージなどを提供(ていきょう)します。

클라우드 기반의 완전한 백엔드 솔루션입니다.
데이터베이스, 인증, 스토리지 등을 제공합니다.

Supabase가 제공하는 기능:

✅ PostgreSQL 데이터베이스
   - 관계형 DB
   - SQL 쿼리
   - 실시간 구독

✅ 인증/인가 (Authentication/Authorization)
   - 이메일 로그인
   - 소셜 로그인
   - JWT 토큰

✅ 스토리지 (Storage)
   - 이미지 업로드
   - 파일 관리
   - CDN 제공

✅ Edge Functions
   - 서버리스 함수
   - API 엔드포인트

✅ 실시간 기능
   - WebSocket
   - 데이터 동기화

→ Firebase의 오픈소스 대안!

📝 Supabase 프로젝트(ぷろじぇくと) 생성(せいせい)

1단계(だんかい): 프로젝트(ぷろじぇくと) 시작(しさく)

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분 소요)

2단계(だんかい): React プロジェクトと連携(れんけい)

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

🗄️ データベーステーブル作成(さくせい)

Post テーブル設計(せっけい)

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란?
   - 행 단위 보안
   - 사용자별 접근 제어
   - "자기 게시글만 수정" 등

カラム構成(こうせい)

カラム名(めい)타입(たいぷ)설정(せってい)의미(いみ)
idint8Primary Key, Auto-increment게시글 고유(こゆう) ID
created_attimestamptzDefault: now()생성(せいせい) 시간(じかん)
contenttextNot Nullable게시글 내용(ないよう)
image_urlstext[]Nullable, Array이미지 URL 배열(はいれつ)
like_countint8Default: 0, Not Nullable좋아요(すき) 수(すう)
author_iduuidDefault: 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: textIs Nullable: 해제 (내용 필수!)

/*
text:
- 무제한 길이 문자열
- varchar보다 유연
- 긴 게시글 가능
*/

-- === 4. image_urls 컬럼 ===
Column Name: image_urls
Type: textIs Nullable: 체크 (이미지 선택사항)
✅ Define as Array: 체크

/*
text[]:
- 문자열 배열
- PostgreSQL 네이티브 배열
- 예: ["url1.jpg", "url2.png"]

왜 배열?
- 여러 이미지 첨부 가능
- 순서 유지
- 쿼리 편리
*/

-- === 5. like_count 컬럼 ===
Column Name: like_count
Type: int8
Default Value: 0Is 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" 버튼 클릭!

🔧 TypeScript 타입(たいぷ) 자동(じどう) 생성(せいせい)

Supabase CLI 설정(せってい)

コマンドラインツールをインストールします。
データベーススキーマから型(かた)を生成(せいせい)します。

커맨드라인 도구를 설치합니다.
데이터베이스 스키마에서 타입을 생성합니다.

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,
//   ...
// }
*/

Supabase クライアントに타입(たいぷ) 적용(てきよう)

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 파이프라인

🔐 認証(にんしょう)と認可(にんか) - 完全(かんぜん)理解(りかい)

認証(にんしょう) vs 認可(にんか)

基本(きほん)概念(がいねん)

認証(にんしょう)(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 사용자 로그인, 자기 게시글 삭제:
   → 인증 성공 ✅
   → 인가 성공 ✅
   → 삭제 완료 ✅
*/

両方(りょうほう)必要(ひつよう)한 理由(りゆう)

보안을 위해서는 둘 다 필요!

인증만 있으면:
❌ 로그인한 사람은 누구나 모든 게시글 삭제 가능
❌ 관리자 페이지 아무나 접근
❌ 다른 사람 정보 수정 가능

인가만 있으면:
❌ 누가 요청했는지 모름
❌ 익명 사용자가 권한 우회 가능
❌ 신원 확인 불가

인증 + 인가:
✅ 누가 (인증)
✅ 무엇을 할 수 있는지 (인가)
✅ 완벽한 보안!

🎫 認証(にんしょう)方式(ほうしき) - セッション vs トークン

セッション方式(ほうしき)

サーバーにセッションオブジェクトを保存(ほぞん)します。
セッション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

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 + Refresh Token 전략(せんりゃく)

二重(にじゅう)トークン戦略(せんりゃく)

=== 토큰 분리 이유 ===

문제:
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 ✨
*/

📝 会員加入(かいいんかにゅう) 実装(じっそう)

Supabase 認証(にんしょう)設定(せってい)

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" 클릭

会員加入(かいいんかにゅう) UI 実装(じっそう)

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>
  );
}

API 関数(かんすう) 実装(じっそう)

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, ... }
*/

Mutation Hook 実装(じっそう)

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 저장
profile
日本での就職を目指している26歳の韓国人開発者です。 アプリとweb開発、両方準備中で、日本語で技術概念を整理しながら日本語も一緒に勉強する予定です。 コツコツ続けるのが好きな開発者の成長記録を、一緒に見守っていただけると嬉しいです! 일본에서의 취업을 목표로 하고 있는 26살의 한국인 개발자입니다. 앱과 웹 개발, 둘 다 준비 중이며, 일본어로 기술 개념을 정리하면서 일본어도 함께 공부할 예정입니다. 꾸준히 계속하는 것을 좋아하는 개발자의 성장 기록을, 함께 지켜봐

0개의 댓글