Supabase: 팀 기능 추가와 로컬 개발 환경 구축하기

Todo 서비스에 팀 기능을 추가하고 RLS 정책을 적용하는 과정과 Supabase 로컬 개발 환경 설정 방법을 소개합니다.

2025-01-25

소개

Supabase: Todo 예제로 알아보기 Supabase: RLS로 Todo 삭제 권한 관리하기

지난 포스트에 이어 supabase todo 예제에 팀 기능을 추가해 봅니다.

데이터베이스 스키마

alt text

먼저 teams 테이블을 생성하고 users 테이블과 연관을 맺기위한 team_members 테이블을 생성합니다.

팀 기능 추가에 따른 RLS 정책은 다음과 같습니다.

  • 사용자는 팀을 생성할 수 있다.
    CREATE POLICY "User can create teams." ON teams FOR
    INSERT
      WITH CHECK (auth.uid() = owner_id);
    
  • 팀 구성원은 팀원들을 볼 수 있다.
    CREATE POLICY "Team members can view their teams." ON teams FOR
    SELECT
      USING (
        EXISTS (
          SELECT
            1
          FROM
            team_members
          WHERE
            team_members.team_id = teams.id
            AND team_members.user_id = auth.uid()
        )
      );
    
  • owner만 팀을 수정할 수 있다.
    CREATE POLICY "Only owner can update teams." ON teams FOR
    UPDATE
      USING (auth.uid() = owner_id);
    
  • owner만 팀을 삭제할 수 있다.
    CREATE POLICY "Only owner can delete teams." ON teams FOR DELETE USING (auth.uid() = owner_id);
    
  • team_members 테이블에 RLS 설정.
    ALTER TABLE
      team_members ENABLE ROW LEVEL SECURITY;
    
  • 팀 owner는 팀원을 추가할 수 있다.
    CREATE POLICY "Team owner can add members." ON team_members FOR
    INSERT
      WITH CHECK (
        EXISTS (
          SELECT
            1
          FROM
            teams
          WHERE
            teams.id = team_id
            AND teams.owner_id = auth.uid()
        )
      );
    
  • 팀 구성원은 다른 구성원을 볼 수 있다.
    CREATE POLICY "Team members can view other members." ON team_members FOR
    SELECT
      USING (
        EXISTS (
          SELECT
            1
          FROM
            team_members
          WHERE
            team_members.team_id = team_members.team_id
            AND team_members.user_id = auth.uid()
        )
      );
    
  • owner만 팀원을 삭제할 수 있다.
    CREATE POLICY "Only owner can delete members." ON team_members FOR DELETE USING (
      EXISTS (
        SELECT
          1
        FROM
          teams
        WHERE
          teams.id = team_id
          AND teams.owner_id = auth.uid()
      )
    );
    
  • 팀 구성원은 팀의 todo를 확인할 수 있다.
    CREATE POLICY "Team members can view team todos." ON todos FOR
    SELECT
      USING (
        auth.uid() = user_id
        OR (
          team_id IS NOT NULL
          AND EXISTS (
            SELECT
              1
            FROM
              team_members
            WHERE
              team_members.team_id = todos.team_id
              AND team_members.user_id = auth.uid()
          )
        )
      );
    
  • 팀 구성원은 todo를 추가할 수 있다.
    CREATE POLICY "Team members can add todos." ON todos FOR
    INSERT
      WITH CHECK (
        auth.uid() = user_id
        AND (
          team_id IS NULL
          OR EXISTS (
            SELECT
              1
            FROM
              team_members
            WHERE
              team_members.team_id = todos.team_id
              AND team_members.user_id = auth.uid()
          )
        )
      )
    

npx supabase migration new teams 명령으로 테이블 생성 및 RLS 설정 SQL을 작성한 뒤 npx supabase db push 명령으로 적용합니다.

팀 기능 추가 · swcho/supabase-nextjs-todo-list

팀 생성

팀 생성을 위해서는 다음의 코드를 수행합니다.

export async function createTeam(name: string) {
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Not authenticated')

  // console.log('createTeam', { user, name })
  const { data, error } = await supabase
    .from('teams')
    .insert({
      name,
      owner_id: user.id
    })
    .select(`*`)
    .single()

  // console.log('createTeam.inserted', { data, error })
  if (error) throw error
  
  // // Add owner as team member
  const { error: memberError } = await supabase
    .from('team_members')
    .insert({
      team_id: data.id,
      user_id: user.id,
      role: 'admin'
    })

  if (memberError) throw memberError

  return data
}
  1. 로그인한 사용자 정보를 가져 옵니다.
  2. teams 테이블에 새로운 팀 정보를 추가합니다.
    1. 이 때, DB는 RLS 정책에 의해 owner_idauth.uid() 즉 로그인한 사용자인지 검증을 합니다.
  3. 추가한 팀 정보를 다시 가져옵니다.
  4. 새로 만든 팀 정보를 바탕으로 team_membersadmin 롤의 사용자를 추가합니다.

일련의 과정은 FE에서 순차적인 API 호출에 의해 이루어 집니다. teamsteams_members 추가 구문은 무결성을 보장을 위해 transaction안에 묶을 수 없게 됩니다. 따라서 이를 보완해 주기 위해 DB의 function 기능을 추가해야 합니다.

또한 teams 테이블에 insert 후 바로 select를 하여 바로 추가한 아이템을 가져오는데요. 향후에는 RLS를 사용하여 내가 속한 팀 정보만 가져올 수 있도록 보완해야 합니다. 다만 이렇게 할 경우, 내가 속한 팀을 판단하기 위해서는 team_members까지 정보가 기입된 상태여야 합니다. 하지만, 위의 코드 처럼 teams 테이블에만 추가한 상태에서는 해당 팀에 내가 속해 있는지 알 수 없게 됩니다. 따라서 내가 owner인지, 팀에 속해 있는지를 모두 확인하는 방식으로 RSL를 보완해야 합니다.

데이터베이스 설정 초기화

RLS를 정의하고 디버깅하는 과정은 쉽지 않습니다. 특별히 지금까지는 production db를 직접 수정하는 방식을 사용했습니다. 이로인해 테스트 코드 역시 production db에 직접 수정을 가하는 방식이였습니다.

이제는 supabase local, preview, production 환경으로 나누어 개발하는 환경을 설정해 봅니다.

먼저 그간의 migration 파일을 모두 삭제해 줍니다.

npx supabase init --force 명령으로 설정 파일들을 한번 초기화 해 줍니다. npx supabase start로 local 개발환경을 구동합니다. docker image를 가져오고 실행합니다. npx supabase link로 확인 차원에서 remote project와 연관을 가집니다.

이후, 먼저 remote의 migration db를 다음과 같이 초기화합니다.

DELETE FROM supabase_migrations.schema_migrations;

npx supabase db pull을 수행하면, remote db의 ddl을 바탕으로 새로운 migration 파일을 생성하고 가져옵니다. npx supabase db dump --data-only -f supabase/seed.sql을 수행하고, 불필요한 insert 구문을 삭제하고 테스트 사용자 계정 정보나 그 밖의 필요한 insert 구문을 남겨 놓습니다. npx supabase db reset 명령으로 local db를 초기화 하고 새로운 migration 파일로 테이블 및 RLS 정책을 생성하고 seed.sql에 있는 정보를 저장합니다.

마지막으로 .env 파일을 local 환경 주소에 맞게 설정합니다.

NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY={{ anon key }}
NEXT_SITE_URL=http://localhost:3000
NEXT_REDIRECT_URLS=http://localhost:3000/

이제 테스트 코드를 local db 대상으로 수행할 수 있어 빠르고 안정적인 개발 환경을 가질 수 있습니다.

마치며

local 개발 환경없이 remote 환경만으로 개발을 하면 RLS 및 검증에 어려움이 있습니다.

db branch 기능등을 활용하여 local에서 db 수정 및 검증을 거쳐 production에 적용하는 과정이 바람직 합니다.

local 개발 환경은 remote dashboard와 같은 supabase studio를 가질 수도 있고 vscode 확장을 통해 IDE 벗어나지 않는 매끄러운 개발환경을 지원합니다.

Loading script...