
Next.js 포트폴리오 프로젝트를 진행하면서 Supabase를 채택하게 되었고, RLS의 필요성을 느꼈지만, 사용하지 않기로 했다.
RLS없이 어떻게 안전하게 데이터를 보호할 수 있을지, 그리고 환경변수는 어떻게 관리해야 하는지에 대한 고민 과정에 관한 내용이다.
일반적 DB와 Supabase DB의 구조적인 차이
일반DB 구조 : 🙋(클라이언트) ⇒ 💻(백엔드서버) ⇒ 📦일반DB
supabaseDB 구조 : 🙋(클라이언트) ⇒ ⚡(supabase)(자동 REST API 생성) ⇒ 📦PostgreSQL
Supabase는 테이블마다 자동으로 REST API를 생성해 준다. (아래는 참고 링크 및 사진)
https://supabase.com/blog/realtime-row-level-security-in-postgresql

Answer
- Supabase는 anon key를 제공한다.
- 웹서비스에서는 클라이언트에게 anon key를 이용하여 백엔드 서버를 거치지 않고 서버리스 방식으로 DB 접근이 가능하도록 한다. 사용자는 anon key를 이용하여 테이블별 CRUD API를 사용할 수 있다.
- 즉, 서버없이도 사용자는 직접 DB에 접근한다.
Answer
- 추가적인 보안장치가 없다면, anon key를 가지고 있는 누구나 나의 DB를 조작할 수 있는 상태이다.
- 때문에 RLS (Row Level Security)를 이용하여 데이터를 보호해야 한다.
- RLS설정을 통하여, 테이블별로 접근권한에 대한 설정이 가능하다.
- 예를 들어,
-- 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에서 쉽게 확인 가능하다.
앞서 봤던, DB 접근 구조를 확인해 보자
- Next.js API Routes에서만 anon key를 이용하여 DB에 접근하도록 하며, 클라이언트는 API Routes에 정의된, 내가 만든 함수만을 이용하도록 제한한다.
- Next.js API Routes에서만 DB호출을 하기 위해서는
app/api하위에서 관리되는route.ts파일들에서만 supabase호출 코드를 사용하자.
- 사실 supabase에서는 서버를 위한 service_role key도 제공한다.
- anon key의 목적은 클라이언트에서의 사용이라, RLS를 사용하지 않는 나의방법에는 권장되지 않는다.


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,
},
});
}
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 공식 문서를 참고하여 작성함)
Node.js 환경(서버)에서만 접근 가능
// .env.local
DATABASE_URL=asdfasdfasdf...
API_SECRET=asdfasdfasdf...
빌드시점에 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)
undefined 처리된다.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