Supabase RLS (Row Level Security) 사용하지 않고 개발하기

namm'm'm·2025년 7월 9일

Project

목록 보기
1/8
post-thumbnail

Next.js 포트폴리오 프로젝트를 진행하면서 Supabase를 채택하게 되었고, RLS의 필요성을 느꼈지만, 사용하지 않기로 했다.

RLS없이 어떻게 안전하게 데이터를 보호할 수 있을지, 그리고 환경변수는 어떻게 관리해야 하는지에 대한 고민 과정에 관한 내용이다.



🍑 Supabase가 뭔데

일반적 DB와 Supabase DB의 구조적인 차이

일반DB 구조 : 🙋(클라이언트) ⇒ 💻(백엔드서버) ⇒ 📦일반DB

supabaseDB 구조 : 🙋(클라이언트) ⇒ ⚡(supabase)(자동 REST API 생성) ⇒ 📦PostgreSQL

Supabase는 테이블마다 자동으로 REST API를 생성해 준다. (아래는 참고 링크 및 사진)

https://supabase.com/blog/realtime-row-level-security-in-postgresql



🍑 Q. 클라이언트는 Supabase가 관리하는 DB에 어떻게 접근하는데?

Answer

  1. Supabase는 anon key를 제공한다.
  2. 웹서비스에서는 클라이언트에게 anon key를 이용하여 백엔드 서버를 거치지 않고 서버리스 방식으로 DB 접근이 가능하도록 한다. 사용자는 anon key를 이용하여 테이블별 CRUD API를 사용할 수 있다.
  3. 즉, 서버없이도 사용자는 직접 DB에 접근한다.


🍑 Q. 보안 측면에서 위험해 보이는데?

Answer

  1. 추가적인 보안장치가 없다면, anon key를 가지고 있는 누구나 나의 DB를 조작할 수 있는 상태이다.
  2. 때문에 RLS (Row Level Security)를 이용하여 데이터를 보호해야 한다.
  3. RLS설정을 통하여, 테이블별로 접근권한에 대한 설정이 가능하다.
  4. 예를 들어,
-- RLS 활성화
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- 정책: 자신의 데이터만 볼 수 있음
CREATE POLICY "users_can_view_own_data" ON users
FOR SELECT USING (auth.uid() = id);

-- 정책: 인증된 사용자만 삽입 가능
CREATE POLICY "authenticated_can_insert" ON users  
FOR INSERT TO authenticated WITH CHECK (true);

실제 사용법은 supabase dashboard에서 쉽게 확인 가능하다.



🍑그럼에도 나의 포트폴리오 사이트에서는 왜 RLS를 사용하지 않는 걸까!

  1. 익명 서비스라서 사용자 정보가 없다.
  2. 클라이언트에게 anon key를 제공하지 않으면 된다.

🍑Next.js의 API Routes 방식을 사용하자

앞서 봤던, DB 접근 구조를 확인해 보자

일반DB 구조 : 🙋(클라이언트) ⇒ 💻(백엔드서버) ⇒ 📦일반DB

기본 SupabaseDB 구조 : 🙋(클라이언트) ⇒ ⚡(Supabase) ⇒ 📦PostgreSQL

나의 구조 : 🙋(클라이언트) ⇒ 🖤 Next.js API Routes ⇒ ⚡(Supabase) ⇒ 📦PostgreSQL

  1. Next.js API Routes에서만 anon key를 이용하여 DB에 접근하도록 하며, 클라이언트는 API Routes에 정의된, 내가 만든 함수만을 이용하도록 제한한다.
  2. Next.js API Routes에서만 DB호출을 하기 위해서는 app/api 하위에서 관리되는 route.ts 파일들에서만 supabase호출 코드를 사용하자.

🍑anon key 대신 service_role key를 사용해 보자.

  1. 사실 supabase에서는 서버를 위한 service_role key도 제공한다.
  2. anon key의 목적은 클라이언트에서의 사용이라, RLS를 사용하지 않는 나의방법에는 권장되지 않는다.

🍉프로젝트에 적용!

🍑Supabase로부터 service_role key를 가져온다.

🍑Vercel 환경변수 등록

🍑 server.ts에서 환경변수 가져오기

Supabase 클라이언트 생성함수

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

const supabaseUrl = process.env.SUPABASE_URL || '';
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || '';

if (!supabaseUrl || !supabaseKey) {
  throw new Error('Missing Supabase environment variables');
}

// 서버 컴포넌트와 API Routes에서 사용할 Supabase 클라이언트
export function createServerClient() {
  return createClient(supabaseUrl, supabaseKey, {
    auth: {
      persistSession: false, // 서버에서는 세션 유지 불필요
      autoRefreshToken: false,
    },
  });
}

🍑 Next.js API Routes

feedback Table에 Insert하는 API

import { createServerClient } from '@/shared/lib/supabase/server';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  try {
    const supabase = createServerClient();

... 중략

    const { data: feedbackData, error: feedbackError } = await supabase
      .from('feedback')
      .insert([
        {
          has_rating: !!body.ratings,
          has_comment: !!body.comment_text,
          has_job_share: !!(body.compony_name || body.job_link),
          has_bug_report: !!body.bug_description,
        },
      ])
      .select('feedback_id')
      .single();

... 중략

    }
    return NextResponse.json({
      success: true,
      data: {
        message: '소중한 피드백 감사합니다!',
        feedback_id: feedbackId,
      },
    });
  } catch (error) {
    console.error('Error sending feedback', error);
    return NextResponse.json({
      success: false,
      error: {
        code: 'INTERNAL_SERVER_ERROR',
        message: '잠시 후 다시 시도해주세요.',
      },
    });
  }
}

🍑클라이언트 컴포넌트에서 /api/feedback 으로 POST 요청 보내면된다.

클라이언트는 직접 DB에 접근할 수 없으며, supabase 접근을 위한 Key를 얻을 수 없다. api/feedback/route.ts 에 정의해둔 API(/api/feedback)를 사용할 뿐이다.



🍉환경변수 관리할 때 주의할 점!

마지막으로 환경변수 등록하는 상황에서 주의할 점에 대해 고민해 봤다.

Guides: Environment Variables (Next.js 공식 문서를 참고하여 작성함)

🍑일반 환경변수 (Non-NEXT_PUBLIC) 는 안전하다

Node.js 환경(서버)에서만 접근 가능

// .env.local
DATABASE_URL=asdfasdfasdf...
API_SECRET=asdfasdfasdf...

🍑NEXT_PUBLIC 변수는 클라이언트를 위한 변수이다. 즉, 위험하다

빌드시점에 NEXT_PUBLIC 환경변수는 실제값으로 변경된다. 그대로 클라이언트에게 전송된다.

// .env.local
NEXT_PUBLIC_ANALYTICS_ID=abcdefghijk...

// 빌드 전 코드
setupAnalyticsService(process.env.NEXT_PUBLIC_ANALYTICS_ID)

// 빌드 후 실제 브라우저로 전송되는 코드
setupAnalyticsService('abcdefghijk') // 값이 하드코딩됨!
  • 단, 동적참조 시에는 작동되지 않는다.
// ❌ 이렇게 하면 인라이닝되지 않음
const varName = 'NEXT_PUBLIC_ANALYTICS_ID'
setupAnalyticsService(process.env[varName]) // undefined!

// ❌ 이것도 작동하지 않음
const env = process.env
setupAnalyticsService(env.NEXT_PUBLIC_ANALYTICS_ID) // undefined!

// ✅ 이렇게만 작동함
setupAnalyticsService(process.env.NEXT_PUBLIC_ANALYTICS_ID)
  • 때문에 Docker에서 문제 발생할 수 있다는데… 흠

🍑 추가내용으로 깊게 이해해보자

  1. NEXT_PUBLIC을 붙여놓은 환경변수에 대해 코드상에서 사용하지 않았다면, 빌드 번들에 포함되지 않기 때문에, 클라이언트에게 노출되지 않는다.
  2. NEXT_PUBLIC을 붙여놓은 환경변수에 대해 API route폴더인 app/api 하위에서 사용한다면, 클라이언트에게 노출되지 않는다.
  3. NEXT_PUBLIC을 붙여놓은 환경변수에 대해 서버컴포넌트, 클라이언트컴포넌트 상관없이 코드상에서 사용되었다면, 클라이언트에게 환경변수가 노출될 수 있다. (서버컴포넌트에 정의된 환경변수가 return될때 노출가능)
  4. NEXT_PUBLIC을 붙여놓은 환경변수에 대해 서버리스형태의 서비스라면, 악용할 여지가 있기 때문에 적절한 RLS를 적용 시켜줘야 한다. 따로 백엔드서버를 운영 중이라면, 애초에 중요한 환경변수는 절대 NEXT_PUBLIC을 붙여서 사용해서는 안된다.
  5. Non-NEXT_PUBLIC 환경변수만을 사용했다면, RLS를 적용하는 것이 권장된다.
  6. Non-NEXT_PUBLIC 환경변수는 기본적으로 클라이언트에 노출되지 않는다.
  7. Non-NEXT_PUBLIC 환경변수는 API route폴더인 app/api 하위에서 사용한다면, 클라이언트에게 노출되지 않는다.
  8. Non-NEXT_PUBLIC 환경변수는 서버컴포넌트에서 사용한다면, 클라이언트에게 노출되지 않으며, 서버에서만 실행된다.
  9. Non-NEXT_PUBLIC 환경변수는 "use client" 해 둔 클라이언트컴포넌트에서 사용한다면, 빌드시에 Non-NEXT_PUBLIC 환경변수를 찾을 수 없기 때문에 undefined 처리된다.
  10. API Routes 폴더 및 파일 등 관련 내용은 애초에 빌드시에 클라이언트에게 제공되는 이미지에 포함이 되지 않고 독립된 환경에서 만들어진다.

🍑환경변수 파일 우선순위가 있지만, 신경쓰지 않아도 될듯하다

1. process.env (시스템 환경변수)
2. .env.development.local (개발용 로컬)
3. .env.local (모든 환경용 로컬)
4. .env.development (개발용)
5. .env (기본값)
# .env
DATABASE_URL=default-db

# .env.local  
DATABASE_URL=local-db # 이 값이 사용됨!

🍑테스트 환경에서 참고사항

# 테스트 시에는 .env.local이 무시됨!

# 로드되는 파일들:
# ✅ .env.test.local
# ❌ .env.local (무시됨)
# ✅ .env.test
# ✅ .env

0개의 댓글