본문 바로가기

코딩강의/Maker 마스터클래스(노마드코더)

#14 Deployment

> Drizzle RLS

 

드리즐을 이용해서 RLS세팅하는 방법을 배워보자. (이전에는 SQL 형식으로 직접 supabase에서 세팅했는데, 그거보다 이게 좋다고 한다. 타입스크립트 자동완성 기능활용 할 수 있고, 깃헙에 올릴수있으니)

 

- users/schema.ts

import {
  bigint,
  boolean,
  jsonb,
  pgEnum,
  pgPolicy,
  pgSchema,
  pgTable,
  primaryKey,
  text,
  timestamp,
  uuid,
} from "drizzle-orm/pg-core";
import { authenticatedRole, authUid, authUsers } from "drizzle-orm/supabase";
import { products } from "../products/schema";
import { posts } from "../community/schema";
import { sql } from "drizzle-orm";

// export const users = pgSchema("auth").table("users", {
//   id: uuid().primaryKey(),
// });

export const roles = pgEnum("role", [
  "developer",
  "designer",
  "marketer",
  "founder",
  "product-manager",
]);

export const profiles = pgTable("profiles", {
  profile_id: uuid()
    .primaryKey()
    .references(() => authUsers.id, { onDelete: "cascade" }),
  avatar: text(),
  name: text().notNull(),
  username: text().notNull(),
  headline: text(),
  bio: text(),
  role: roles().default("developer").notNull(),
  stats: jsonb()
    .$type<{
      followers: number;
      following: number;
    }>()
    .default({ followers: 0, following: 0 }),
  views: jsonb(),
  created_at: timestamp().notNull().defaultNow(),
  updated_at: timestamp().notNull().defaultNow(),
});

export const follows = pgTable(
  "follows",
  {
    follower_id: uuid()
      .references(() => profiles.profile_id, {
        onDelete: "cascade",
      })
      .notNull(),
    following_id: uuid()
      .references(() => profiles.profile_id, {
        onDelete: "cascade",
      })
      .notNull(),
    created_at: timestamp().notNull().defaultNow(),
  },
  (table) => [primaryKey({ columns: [table.follower_id, table.following_id] })]
);

export const notificationType = pgEnum("notification_type", [
  "follow",
  "review",
  "reply",
]);

export const notifications = pgTable("notifications", {
  notification_id: bigint({ mode: "number" })
    .primaryKey()
    .generatedAlwaysAsIdentity(),
  source_id: uuid().references(() => profiles.profile_id, {
    onDelete: "cascade",
  }),
  product_id: bigint({ mode: "number" }).references(() => products.product_id, {
    onDelete: "cascade",
  }),
  post_id: bigint({ mode: "number" }).references(() => posts.post_id, {
    onDelete: "cascade",
  }),
  target_id: uuid()
    .references(() => profiles.profile_id, {
      onDelete: "cascade",
    })
    .notNull(),
  seen: boolean().default(false).notNull(),
  type: notificationType().notNull(),
  created_at: timestamp().notNull().defaultNow(),
});

export const messageRooms = pgTable("message_rooms", {
  message_room_id: bigint({ mode: "number" })
    .primaryKey()
    .generatedAlwaysAsIdentity(),
  created_at: timestamp().notNull().defaultNow(),
});

export const messageRoomMembers = pgTable(
  "message_room_members",
  {
    message_room_id: bigint({ mode: "number" }).references(
      () => messageRooms.message_room_id,
      {
        onDelete: "cascade",
      }
    ),
    profile_id: uuid().references(() => profiles.profile_id, {
      onDelete: "cascade",
    }),
    created_at: timestamp().notNull().defaultNow(),
  },
  (table) => [
    primaryKey({ columns: [table.message_room_id, table.profile_id] }),
    pgPolicy("message_room_members_policy", {
      for: "select",
      to: authenticatedRole,
      as: "permissive",
      using: sql`public.is_user_member(${table.message_room_id}, auth.uid())`,
    }),
  ]
);

export const messages = pgTable("messages", {
  message_id: bigint({ mode: "number" })
    .primaryKey()
    .generatedAlwaysAsIdentity(),
  message_room_id: bigint({ mode: "number" })
    .references(() => messageRooms.message_room_id, {
      onDelete: "cascade",
    })
    .notNull(),
  sender_id: uuid()
    .references(() => profiles.profile_id, {
      onDelete: "cascade",
    })
    .notNull(),
  content: text().notNull(),
  created_at: timestamp().notNull().defaultNow(),
});

export const todos = pgTable(
  "todos",
  {
    todo_id: bigint({ mode: "number" })
      .primaryKey()
      .generatedAlwaysAsIdentity(),
    title: text().notNull(),
    completed: boolean().notNull().default(false),
    created_at: timestamp().notNull().defaultNow(),
    profile_id: uuid()
      .references(() => profiles.profile_id, {
        onDelete: "cascade",
      })
      .notNull(),
  },
  (table) => [
    pgPolicy("todos-insert-policy", {
      for: "insert",
      to: authenticatedRole,
      as: "permissive",
      withCheck: sql`${authUid} = ${table.profile_id}`,
    }),
    pgPolicy("todos-select-policy", {
      for: "select",
      to: authenticatedRole,
      as: "permissive",
      using: sql`${authUid} = ${table.profile_id}`,
    }),
  ]
);

 

1) RLS를 바로 테이블 생성시에 지정해줄 수 있다. (이게 좋을듯 이렇게 하자)

2) 기존에 users를 쓰려면 auth스키마에서 가지고 온 후 써야했는데, 이렇게 하면 아래 드리즐 orm에서 바로 authUsers를 가져다 쓸 수 있기 때문에 필요가 없음

// export const users = pgSchema("auth").table("users", {
//   id: uuid().primaryKey(),
// });
import { authenticatedRole, authUid, authUsers } from "drizzle-orm/supabase";
 

 

이렇게 하고 npm run db:generate를 하게 되면 아래 부분이 생성 되는데, 이거는 삭제하고 마이그레이션을 진행해주자. (auth스키마 부분을 해당 파일에서 삭제했기 때문에 나오는 것임/ 실수로 나는 삭제해버렸다..)

DROP TABLE "auth"."users" CASCADE;--> statement-breakpoint
 

 

> vercel 배포

npm i @vercel/react-router 설치

 

- react-router-config.ts

import type { Config } from "@react-router/dev/config";
import { vercelPreset } from "@vercel/react-router/vite";

export default {
  // Config options...
  // Server-side render by default, to enable SPA mode set this to `false`
  ssr: true,
  presets: [vercelPreset()],
} satisfies Config;

위와 같이 설정 후, git push

 

그 후 vercel에서 해당 git 주소를 연결해주면 된다.

 

이 후 Cloudflare에 도메인 연결해주는 것과, cloudeflare의 방화벽 설정 등은 강의를 보자!

 

> 방화벽 관련

특정 나라에서 접속 못하게 막거나, request를 몇초안에 몇번하면 막게 하는등의 방화벽을 만들 수 있음

 

> sentry관련

코드 특정 부분에 에러가 나면, 그 부분을 잡아주는 서비스이다. (에러 녹화 기능 / 사용자가 여러번 클릭한 경우 등 등, 일부는 유료)

 

 

'코딩강의 > Maker 마스터클래스(노마드코더)' 카테고리의 다른 글

#13 Toss Payments  (0) 2025.05.29
#12 Transactional Emails  (0) 2025.05.27
#11 GPT & CRON Jobs  (0) 2025.05.23
#10 DMs  (0) 2025.05.21
#9 Fetchers  (0) 2025.05.14