Supabase: RLS로 Todo 삭제 권한 관리하기

Todo 앱에 Supabase의 Row Level Security를 적용하여 사용자별 Todo 삭제 권한을 관리하는 방법과 테스트 방법을 설명합니다. Next.js와 Supabase를 사용한 실제 구현 예제를 통해 데이터베이스 수준의 보안 정책을 구현하는 방법을 알아봅니다.

2025-01-12

소개

Supabase: Todo 예제로 알아보기

지난 포스트에 이어 supabase todo 예제를 통해 RLS(Row Level Security) 기능을 살펴 봅니다.

RLS(Row Level Security) 란

Row Level Security | Supabase Docs

Row Level Security (RLS)는 데이터베이스 수준에서 행(row) 단위의 접근 제어를 가능하게 하는 보안 기능입니다. BaaS인 Supabase는 별도의 back end 프로그램 없이 PostgreSQL의 RLS 기능을 활용하여 비지니스 로직을 구현합니다.

먼저 공개 접근이 가능한 end point와 리버스 엔지니어링으로 유출 가능한 anon로 접근하는 Supabase인 점을 생각해 봅니다. 다시말해 누구든 Postgres에 있는 데이터에 접근이 가능하고 CRUD 작업을 할 수 있게 됩니다. Todo 서비스의 경우 Todo를 만든 사용자의 기준으로 목록이 보여지고 수정 및 삭제가 가능해야 할 것 입니다. 이를 위해 먼저 Supabase는 Authentication이라는 기능을 기본으로 지원하여 사용자 식별을 합니다. 그뒤 로그인한 사용자 정보를 바탕으로 RLS 정책을 생성하도록 합니다.

따라서 별도의 backend 프로그램 개발, 배포, 운영 없이도 비지니스 로직을 구현할 수 있습니다. 추가로 데이터 베이스 수준의 보안이 이루어 짐으로 서비스의 신뢰성도 높아 집니다.

Todo 삭제 정책 설계

이전 프로그램은 Todo를 작성한 사용자가 자신의 Todo 아이템을 삭제할 수 있습니다.

삭제에 있어서 다음과 같은 RLS 정책을 추가해 볼 수 있을 것 입니다.

특정 시간이 지난 후에만 삭제 가능하도록

create policy "Can only delete todos after 24 hours" on todos for
    delete using (
        auth.uid() = user_id and
        (now() - inserted_at) > interval '24 hours'
    );

완료한 todo만 삭제 가능하도록

create policy "Can only delete completed todos" on todos for
    delete using (
        auth.uid() = user_id and
        is_complete = true
    );

하루에 삭제할 수 있는 수를 제한

create policy "Can only delete 5 todos per day" on todos for
    delete using (
        auth.uid() = user_id and
        (
            select count(*)
            from todos
            where user_id = auth.uid()
            and deleted_at >= date_trunc('day', now())
        ) < 5
    );

특정 조건의 todo만 삭제 가능하도록

create policy "Can only delete short todos" on todos for
    delete using (
        auth.uid() = user_id and
        char_length(task) <= 100
    );

완료한 todo만 삭제 가능하도록 Todo 삭제 정책 추가하기

위의 여러 기능 중 완료한 todo만 삭제 가능하도록 정책 설정을 해 보겠습니다.

먼저 npm i -D supabase 명령으로 CLI 패키지를 인스톨합니다.

이는 supabase CLI 툴이며, npx supabase --help로 자세한 명령세트를 확인할 수 있습니다.

npx supabase link 명령을 사용하면 supabase 프로젝트와 연동할 수 있습니다. 이때 supabase/config.toml를 업데이트 하라는 메시지가 나오며, 필요에 따라 직접 수정해 줍니다.

예제의 초기 migration 파일은 다음과 같습니다.

create table todos (
  id bigint generated by default as identity primary key,
  user_id uuid references auth.users not null, -- auth schema에 있는 users 테이블과 연관을 맺습니다.
  task text check (char_length(task) > 3), -- 3 글자 이상이어야 합니다.
  is_complete boolean default false,
  inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);
alter table todos enable row level security;
create policy "Individuals can create todos." on todos for
    insert with check (auth.uid() = user_id);
create policy "Individuals can view their own todos. " on todos for
    select using (auth.uid() = user_id);
create policy "Individuals can update their own todos." on todos for
    update using (auth.uid() = user_id);
create policy "Individuals can delete their own todos." on todos for
    delete using (auth.uid() = user_id);

위의 SQL은 초기 deploy 과정에서 실행한 상태이며, Authentication > Policies 화면에서 다음과 같이 확인할 수 있습니다.

RLS 목록

이제 완료한 경우에만 삭제가 가능하도록 변경해 주어야 합니다.

create policy "Can only delete completed todos" on todos for
    delete using (
        auth.uid() = user_id and
        is_complete = true
    );

다음과 같이 RLS 정책을 UI를 통해 생성할 수도 있습니다.

RLS 생성

하지만 이 경우, 데이터 베이스의 수정 사항을 기록에 남겨 혹시있을 재설치 및 트러블 슈팅이 어려울 수 있습니다.

따라서 테이블 및 RLS 정책 생성 등 DB에 가해지는 변경이 필요할 경우, UI를 사용하는 것 보다는 migration 기능을 활용하는 것이 좋습니다.

먼저 npx supabase migration new add_delete_policy_for_todos 명령으로 빈 migration 파일을 생성합니다.

Supabase CLI는 {프로젝트 root}/supabase/migrations/{timestamp}_add_delete_policy_for_todos.sql와 같은 형식으로 파일을 생성해 줍니다.

이 곳에 RLS 정책을 설정하는 SQL 구문을 작성합니다.

-- 기존 정책 삭제
drop policy if exists "Individuals can delete their own todos." on todos;

-- 새로운 정책 추가
create policy "Can only delete completed todos" on todos for
    delete using (
        auth.uid() = user_id and
        is_complete = true
    );

npx supabase db push 명령을 수행하여 새로 추가한 migration 파일의 SQL 구문을 DB에 적용합니다.

대쉬보드의 Database > Migrations 화면에서 다음과 같이 확인할 수도 있습니다.

migrations 목록

feat: RLS: 완료 했을 경우에만 삭제 가능 · swcho/supabase-nextjs-todo-list@bee1598

테스트 케이스

Supabase의 비지니스 로직은 사실상 RLS와 함수 등으로 구현하기 때문에 자주 변경이 이루어질 수 밖에 없습니다.

특별히 비지니스 로직은 여러 사용자가 있는 시나리오에 대해서 세심하게 설계하고 충분히 검증해야 합니다.

따라서, 비지니스 로직 자체를 문서화 하고 검증할 수 있는 test case를 가져가는 것이 best practice가 될 것입니다.

vitest

먼저 vitest를 설정 합니다.

npm i -D vitest @vitejs/plugin-react vite-tsconfig-paths dotenv @testing-library/jest-dom

vitest.config.mts 파일을 생성하고, test/setup.ts을 통해 Supabase의 end point와 anon 키 정보를 가지는 환경 변수를 로드 합니다.

테스트 계정 생성

테스트 코드 수행에 사용할 테스트 계정을 별도로 생성해 줍니다.

test users

사용자는 Add user -> Create a new user 기능을 활용해 UI 상에서 직접 생성할 수 있습니다.

add user

테스트 코드

테스트 코드는 다음과 같습니다.

  1. 사용자 로그인
  2. Todo 생성
  3. 삭제 시도 후 오류 확인
  4. Todo complete 설정
  5. 삭제 시도 후 정상 동작 확인

마치며

Supabase의 RLS 기능을 실제로 todo 삭제 시나리오에 적용해 보고 그 과정을 학습해 보았습니다.

또한 앞으로 있을 RLS 기능의 업데이트에 따른 비지니스 로직의 동작 검증을 위한 테스트 코드도 짜 보았습니다.

별도의 서버 구현없이 비지니스 로직을 구현하고 서비스에 적용할 수 있는 점에서 Supabase의 독특함과 매력을 느낄 수 있었습니다.

Loading script...