본문 바로가기
무료 API 및 오픈소스 리뷰

[이미지/동영상 업로드, 서버 없이 무료로 해결하는 CDN API #04] "내 프로필 사진은 나만 지운다" 인증 기반 무료 파일 스토리지 (Firebase 대안)

by 코드메이트 2026. 4. 20.

안녕하세요! 프론트엔드 개발자들의 성장 치트키, codeBricks입니다. 🧱✨

지난 시간에 우리는 해커톤이나 토이 프로젝트에서 1분 만에 뚝딱! 이미지를 올릴 수 있는 마법의 'Imgur API'를 배웠습니다. 그런데 말입니다. 여러분이 지금 만들고 있는 프로젝트가 그냥 익명 게시판이 아니라, "유저별로 회원가입을 하고 본인만의 프로필 사진(아바타)을 관리하는 진짜 서비스"라면 어떻게 해야 할까요?

내가 올린 소중한 프로필 사진을 옆집 철수가 마음대로 수정하거나 지워버리면 대참사가 벌어지겠죠? 혹은 나만 봐야 하는 프라이빗한 파일(계약서, 다이어리 사진 등)을 누군가 URL만 알면 볼 수 있다면요? 앞서 배운 Imgur나 단순 CDN 방식으로는 이 '보안(Security)과 권한(Authorization)' 문제를 프론트엔드 혼자서 해결하기가 매우 까다롭습니다.

그래서 오늘은 "로그인한 유저 본인만 자신의 파일을 관리할 수 있는" 진짜 프로덕션 레벨의 스토리지! 요즘 Firebase의 가장 강력한 대안으로 떠오르고 있는 오픈소스 BaaS(Backend as a Service), [Supabase Storage]를 파헤쳐 보겠습니다.

백엔드 코드 한 줄 없이, 프론트엔드에서 데이터베이스와 스토리지를 완벽하게 통제하는 'RLS 보안 규칙'의 신세계로 여러분을 초대합니다! 🚀

 

🔥 왜 다들 Firebase 버리고 Supabase 스토리지로 넘어갈까?

React나 Next.js로 개인 프로젝트를 좀 해보신 분들이라면 "로그인 + DB + 이미지 저장 = Firebase 아니야?"라고 생각하실 겁니다. 구글이 만든 Firebase는 훌륭하지만, 최근 개발자 생태계에서는 Supabase(수파베이스)가 무서운 속도로 점유율을 뺏어오고 있습니다. 왜 그럴까요?

 

1. NoSQL의 한계 탈출! 강력한 PostgreSQL 기반 Firebase는 데이터 구조가 낯선 NoSQL 방식이라, 나중에 유저 테이블과 이미지 테이블을 연결(JOIN)하려고 하면 피눈물을 흘리게 됩니다. 반면 Supabase는 우리가 환장하는(?) 근본 관계형 DB인 PostgreSQL을 엔진으로 씁니다. 프로필 이미지를 스토리지에 올리고, 그 URL을 유저 테이블에 업데이트하는 과정이 너무나 직관적이고 깔끔합니다.

 

2. 혜자스러운 무료 티어 (Free Tier) 개인 프로젝트나 초기 스타트업 입장에서 가장 중요한 부분이죠. Supabase는 가입만 해도 무려 1GB의 넉넉한 파일 스토리지와 2GB의 대역폭을 무료로 퍼줍니다. 프로필 사진이나 게시물 썸네일 수천 장을 담기에 차고 넘치는 용량입니다.

 

3. 내 파일은 나만 건드린다! 철통 보안 RLS (Row Level Security) 오늘 포스팅의 핵심입니다. Supabase는 스토리지 자체가 데이터베이스의 로그인 기능(Auth)과 완벽하게 찰떡으로 연동되어 있습니다. 그래서 프론트엔드에서 "이 폴더에 있는 사진은 로그인한 본인(UID)만 수정/삭제할 수 있어!"라는 규칙을 마우스 클릭 몇 번으로, 혹은 SQL 한 줄로 완벽하게 걸어 잠글 수 있습니다.

 

 

Supabase Storage의 RLS(Row Level Security) 규칙을 통한 타인 프로필 이미지 수정 및 삭제 차단 개념도

 

🛡️ 스토리지 보안의 핵심: RLS (Row Level Security) 설정법

코드를 치기 전에 먼저 보안 문부터 든든하게 잠그고 시작하겠습니다. Supabase 대시보드에 로그인하고 프로젝트를 만들었다면, 왼쪽 메뉴에서 'Storage' 탭으로 들어가 주세요.

  1. 버킷(Bucket) 생성하기: avatars라는 이름의 새 버킷(폴더)을 만듭니다. 이때 Public bucket 체크박스를 켜면 누구나 사진을 '볼 수' 있게 되고, 끄면 나만 볼 수 있는 프라이빗 버킷이 됩니다. 프로필 사진이니까 'Public'으로 만들어 주시면 됩니다.
  2. RLS 정책(Policies) 추가하기: 이제 이 avatars 버킷에 규칙을 정해야 합니다. 버킷 이름 옆에 있는 'Policies' 버튼을 누릅니다.
  3. 새 규칙 만들기:
    • 누가 이 폴더에 사진을 올릴(INSERT) 수 있나요? -> "로그인한 유저(Authenticated)만 가능해!"
    • 누가 이 사진을 지우거나 수정(UPDATE/DELETE)할 수 있나요? -> "사진을 처음 올렸던 본인(Owner)만 가능해!"

Supabase 대시보드에서는 이 복잡한 규칙들을 템플릿(Template)으로 제공합니다. Enable RLS를 켜고, Auth.uid() = owner 같은 템플릿을 딸깍 클릭해서 저장만 해주세요.

축하합니다! 방금 여러분은 그 복잡하다는 백엔드의 '인가(Authorization) 로직'을 마우스 클릭 세 번으로 끝내셨습니다. 이제 해커가 프론트엔드 코드를 뚫고 들어와 남의 사진을 지우려고 DELETE 요청을 날려도, Supabase 서버가 0.1초 만에 "너 이 사진 주인 아니잖아. 컷!" 하고 막아줍니다.

 

💻 실전! React에서 나만의 프로필 이미지 업로드하기

자, 보안 문도 잠갔으니 이제 브라우저(React)에서 직접 이미지를 밀어 넣어 봅시다. Supabase의 공식 JS 클라이언트(@supabase/supabase-js)를 사용하면 코드가 정말 우아해집니다.

Copyimport React, { useState } from 'react';
import { createClient } from '@supabase/supabase-js';

// 환경변수에서 Supabase 설정값 불러오기 (보안 철저!)
const SUPABASE_URL = process.env.REACT_APP_SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.REACT_APP_SUPABASE_ANON_KEY;

// 클라이언트 초기화 (BaaS의 축복이 시작됩니다)
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);

const AvatarUpload = ({ userId }) => {
  const [avatarUrl, setAvatarUrl] = useState(null);
  const [uploading, setUploading] = useState(false);

  const uploadAvatar = async (event) => {
    try {
      setUploading(true);

      const file = event.target.files[0];
      if (!file) return;

      // 1. 파일 이름 짓기: 시간 기반의 난수나 유저 ID를 활용해 고유한 이름 생성
      const fileExt = file.name.split('.').pop();
      const fileName = `${userId}-${Math.random()}.${fileExt}`;
      const filePath = `public/${fileName}`; // 버킷 안의 상세 경로

      // 2. 백엔드 없이 Supabase Storage로 다이렉트 업로드! (너무 쉽죠?)
      const { data, error: uploadError } = await supabase.storage
        .from('avatars') // 아까 대시보드에서 만든 버킷 이름
        .upload(filePath, file);

      if (uploadError) {
        throw uploadError;
      }

      // 3. 업로드가 성공하면, 사진을 꺼내볼 수 있는 공개 URL(Public URL)을 요청합니다.
      const { data: publicUrlData } = supabase.storage
        .from('avatars')
        .getPublicUrl(filePath);

      // 4. 성공! 반환된 URL을 화면에 보여줍니다. 
      // (이제 이 URL을 유저 테이블의 DB에 UPDATE 치면 끝납니다.)
      console.log("새 프로필 사진 URL:", publicUrlData.publicUrl);
      setAvatarUrl(publicUrlData.publicUrl);

    } catch (error) {
      alert("프로필 사진 업로드에 실패했습니다 😭: " + error.message);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h3>👤 내 프로필 사진 바꾸기</h3>
      
      {avatarUrl ? (
        <img 
          src={avatarUrl} 
          alt="Avatar" 
          style={{ width: '150px', height: '150px', borderRadius: '50%', objectFit: 'cover' }} 
        />
      ) : (
        <div style={{ width: '150px', height: '150px', backgroundColor: '#ddd', borderRadius: '50%', margin: '0 auto' }} />
      )}
      
      <br /><br />
      
      <input 
        type="file" 
        accept="image/*" 
        onChange={uploadAvatar} 
        disabled={uploading} 
      />
      {uploading && <p>열심히 올리는 중... ⏳</p>}
    </div>
  );
};

export default AvatarUpload;
Copy

[코드 핵심 해설] 코드를 보시면 아시겠지만, 프론트엔드 개발자가 이미지 파일을 들고 supabase.storage.from('avatars').upload(...) 이 한 줄의 메서드만 호출하면 모든 과정이 끝납니다.

게다가 위 코드를 실행하는 유저가 로그인을 안 한 상태라면? 아까 우리가 설정해 둔 RLS 정책에 의해 바로 '401 Unauthorized' 에러를 뱉어내며 업로드를 차단합니다. 남의 userId를 변조해서 사진을 덮어씌우려고(UPDATE) 해도 철벽 방어! 정말 든든하지 않나요?

업로드가 끝난 뒤에 호출하는 getPublicUrl 메서드는 방금 올린 이미지가 전 세계 어디서든 보일 수 있도록 CDN이 적용된 깔끔한 공개 주소를 만들어줍니다. 이제 이 주소를 users 데이터베이스 테이블에 저장만 하시면 프로필 수정 기능은 완벽하게 마무리됩니다.

 

 

백엔드 없이 프론트엔드에서 데이터베이스와 스토리지 로직을 한 번에 처리하는 Supabase BaaS 구조의 장점

 

🎯 마무리: BaaS의 축복, Supabase로 독립하세요!

서버리스(Serverless) 시대가 열리면서 프론트엔드 개발자들의 권한과 능력이 무한대로 확장되고 있습니다. 과거에는 파일 하나 올리려면 백엔드 개발자에게 "저기요... 이미지 받는 API 좀 파주세요..." 하고 눈치를 봐야 했지만, 이제는 Supabase 같은 훌륭한 툴 덕분에 프론트엔드 혼자서도 완벽한 보안을 갖춘 프로덕션 서비스를 론칭할 수 있게 되었습니다.

Firebase의 NoSQL에 지치셨나요? 내 프로젝트의 소중한 유저 데이터를 완벽하게 통제하고 싶으신가요? 그렇다면 오늘 당장 Supabase Storage로 갈아타 보세요. 여러분의 개발 속도가 3배는 빨라질 것이라 확신합니다.

다음 포스팅에서는 이 시리즈의 대망의 마지막 편! Next.js와 React 환경에서 "개발자 경험(DX)의 끝판왕"이라 불리며 최근 미친듯한 인기를 끌고 있는 [Uploadthing]에 대해 알아보겠습니다. 프론트엔드의 무기는 많을수록 좋으니까요! 다음 시간에도 codeBricks와 함께 재밌게 코딩해 봐요! 👋