EP11. Supabase Edge Functions Architecture
์๋น์ค๊ฐ ์ฑ์ฅํ๋ฉด์ ์ฝ๋๊ฐ ๋ง์์ง๋๋ผ๋ ์ฝ๋์ ๋ณต์ก๋๋ฅผ ๋ฎ์ถฐ ์ ์ง๋ณด์ ๊ฐ๋ฅํ ์๋น์ค๊ฐ ๋ ์ ์๋๋ก ๊ตฌ์กฐ๋ฅผ ์ค๊ณํฉ๋๋ค.
๋์์ธํจํด, ์ํคํ
์ฒ๋ ์ทจํฅ์ด๋ฉฐ ์ ๋ต์ ์์ต๋๋ค. ๋ ์ณ๋ค๊ณ ํ๋จ๋๋ ์ค๊ณ๊ฐ ์๋ค๋ฉด ๋ค๋ฅธ ์ค๊ณ๋ก ์ ์ฉํ์
๋ ๋ฉ๋๋ค.
๊ฐ์์์๋ โ๋ ์ด์ด๋ ์ํคํ
์ฒโ๋ฅผ ์ ์ฉํฉ๋๋ค.
์ถํ Push Notification๊ธฐ๋ฅ ๊ตฌํ์ ์ํด Push Token์ ๊ด๋ฆฌํ๋ API๋ฅผ ์ค๊ณํด๋ณด๋ฉด์ ์ด๋ป๊ฒ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ ธ๊ฐ์ผ ํ๋์ง ์ดํด๋ด
๋๋ค.
1. ๋ ์ด์ด๋ ์ํคํ ์ฒ(Layered Architecture)
1.
Router: ์์ฒญ์ ์์ ํ๊ณ ์ ์ ํ ์ปจํธ๋กค๋ฌ๋ก ๋ผ์ฐํ
ํฉ๋๋ค.
2.
Controller: ์ฌ์ฉ์ ์
๋ ฅ์ ์ฒ๋ฆฌํ๊ณ ์๋น์ค ๊ณ์ธต์ ํธ์ถํ์ฌ ๋น์ฆ๋์ค ๋ก์ง์ ์คํํฉ๋๋ค.
3.
Service: ๋น์ฆ๋์ค ๋ก์ง์ ๊ตฌํํฉ๋๋ค. ์ด ๊ณ์ธต์์๋ ์ฃผ๋ก ์ ํ๋ฆฌ์ผ์ด์
์ ํต์ฌ ๊ธฐ๋ฅ์ด ๊ตฌํ๋ฉ๋๋ค.
4.
Repository: ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ํธ์์ฉ์ ์ฒ๋ฆฌํฉ๋๋ค. ๋ฐ์ดํฐ ์ ์ฅ์์ ๊ด๋ จ๋ ์์
์ ์บก์ํํฉ๋๋ค.
2. ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
FCM Token์ ์ ์ ์ ๋ณด์ ํ๋์ด๋ฏ๋ก ์ ์ ์ ๋ณด๋ฅผ ๋ค๋ฃจ๋ Router, Controller, Service, Repository๋ฅผ ์ถ๊ฐํฉ๋๋ค.
3. ์ฝ๋ ๊ตฌํ
ํ์ Repository ๋ถํฐ ์์ Controller, ๊ทธ๋ฆฌ๊ณ index ์์ผ๋ก ์ฝ๋๋ฅผ ๊ตฌํํฉ๋๋ค.
1.
api/repositories/userRepository.ts
export class UserRepository {
// userID๋ฅผ ์ด์ฉํด ํด๋น ์ ์ ์ FCM ํ ํฐ ๋ชฉ๋ก์ ๋ฐํํ๋ ํจ์
async getFCMTokensByUserID(userID: string): Promise<string[]> {
// TODO: ๋ค์ Supabase Database ๊ฐ์์์ ๊ตฌํ
// FCM ํ ํฐ ๋ฐฐ์ด ๋ฐํ
return ["token_1", "token_2", "token_3", "token_4"];
}
// userID์ fcmToken์ ์ธํ์ผ๋ก ๋ฐ์์ DB์ ์ถ๊ฐํ๋ ํจ์
async addFCMToken(userID: string, fcmToken: string): Promise<boolean> {
// TODO: ๋ค์ Supabase Database ๊ฐ์์์ ๊ตฌํ
return true;
}
// userID์ fcmToken์ ์ธํ์ผ๋ก ๋ฐ์์ DB์์ ์ญ์ ํ๋ ํจ์
async deleteFCMToken(userID: string, fcmToken: string): Promise<boolean> {
// TODO: ๋ค์ Supabase Database ๊ฐ์์์ ๊ตฌํ
return true;
}
}
TypeScript
๋ณต์ฌ
Database ๋ ๋ฒจ์ ์ ๊ทผํด ๋ฐ์ดํฐ๋ฅผ CRUD ์ฒ๋ฆฌํฉ๋๋ค. ์์ง Supabase Database๋ ๋ค๋ฃจ์ง ์์์ผ๋ฏ๋ก mock data๋ฅผ ๋ฐํํ๋๋ก ํฉ๋๋ค.
2.
api/services/userService.ts
import { UserRepository } from "./../repositories/userRepository.ts";
export class UserService {
private userRepository: UserRepository;
constructor() {
this.userRepository = new UserRepository();
}
// userID๋ฅผ ์ด์ฉํด ํด๋น ์ ์ ์ FCM ํ ํฐ ๋ชฉ๋ก์ ๋ฐํํ๋ ํจ์
async getFCMTokensByUserID(userID: string): Promise<string[]> {
return await this.userRepository.getFCMTokensByUserID(userID);
}
// userID์ fcmToken์ ์ธํ์ผ๋ก ๋ฐ์์ DB์ ์ถ๊ฐํ๋ ํจ์
async addFCMToken(userID: string, fcmToken: string): Promise<boolean> {
return await this.userRepository.addFCMToken(userID, fcmToken);
}
// userID์ fcmToken์ ์ธํ์ผ๋ก ๋ฐ์์ DB์์ ์ญ์ ํ๋ ํจ์
async deleteFCMToken(userID: string, fcmToken: string): Promise<boolean> {
return await this.userRepository.deleteFCMToken(userID, fcmToken);
}
}
TypeScript
๋ณต์ฌ
โข
Repository๋ฅผ ์์ ํ๋ฉฐ Repository๋ฅผ ์ด์ฉํด ๋ฐ์ดํฐ๋ฅผ CRUD ํฉ๋๋ค.
โข
Service๋ ๋ค์๊ฐ์ Repository๋ฅผ ๊ฐ์ง ์ ์๊ณ ์ฌ๋ฌ ๋ฐ์ดํฐ๋ค์ ์ฐ์ฐ, ๊ฐ๊ณตํฉ๋๋ค. ํ์ฌ ์์ ์ ๊ฒฝ์ฐ ๋ณต์กํ ๊ฒฝ์ฐ๊ฐ ์๋๋ผ Repository์ 1:1๋ก๋ง ๋งค์นญ๋์ด ์์ต๋๋ค.
3.
api/controllers/userController.ts
import { Context } from "https://deno.land/x/hono/mod.ts";
import { UserService } from "../services/userService.ts";
// UserController ํด๋์ค๋ FCM ํ ํฐ์ ๊ด๋ฆฌํ๊ธฐ ์ํ ์ปจํธ๋กค๋ฌ์
๋๋ค.
export class UserController {
private userService: UserService;
// ์์ฑ์์์๋ UserService ์ธ์คํด์ค๋ฅผ ์ด๊ธฐํํฉ๋๋ค.
constructor() {
this.userService = new UserService();
}
// postFCMTokenV1 ๋ฉ์๋๋ FCM ํ ํฐ์ ์ถ๊ฐํ๋ API ์๋ํฌ์ธํธ์
๋๋ค.
async postFCMTokenV1(c: Context) {
// ์์ฒญ์์ fcmToken์ ์ถ์ถํฉ๋๋ค.
const { fcmToken } = await c.req.json();
// fcmToken์ด ์๋ ๊ฒฝ์ฐ ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ฐํํฉ๋๋ค.
if (!fcmToken) {
return c.json("Missing userID or fcmToken");
}
// UserService๋ฅผ ํตํด FCM ํ ํฐ์ ์ถ๊ฐํฉ๋๋ค.
const result = await this.userService.addFCMToken("mock_user_id", fcmToken);
// ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ ์ ํ ๋ฉ์์ง๋ฅผ ๋ฐํํฉ๋๋ค.
return c.json(
result ? "FCM token added successfully" : "Failed to add FCM token",
);
}
// getFCMTokensV1 ๋ฉ์๋๋ ํน์ ์ฌ์ฉ์์ FCM ํ ํฐ๋ค์ ๊ฐ์ ธ์ค๋ API ์๋ํฌ์ธํธ์
๋๋ค.
async getFCMTokensV1(c: Context) {
// UserService๋ฅผ ํตํด ์ฌ์ฉ์ ID์ ํด๋นํ๋ FCM ํ ํฐ๋ค์ ๊ฐ์ ธ์ต๋๋ค.
const tokens = await this.userService.getFCMTokensByUserID("mock_user_id");
// ํ ํฐ ๋ชฉ๋ก์ JSON ํ์์ผ๋ก ๋ฐํํฉ๋๋ค.
return c.json(tokens);
}
// deleteFCMTokenV1 ๋ฉ์๋๋ FCM ํ ํฐ์ ์ญ์ ํ๋ API ์๋ํฌ์ธํธ์
๋๋ค.
async deleteFCMTokenV1(c: Context) {
// ์์ฒญ์์ fcmToken์ ์ถ์ถํฉ๋๋ค.
const { fcmToken } = await c.req.json();
// fcmToken์ด ์๋ ๊ฒฝ์ฐ ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ฐํํฉ๋๋ค.
if (!fcmToken) {
return c.json("Missing userID or fcmToken");
}
// UserService๋ฅผ ํตํด FCM ํ ํฐ์ ์ญ์ ํฉ๋๋ค.
const result = await this.userService.deleteFCMToken(
"mock_user_id",
fcmToken,
);
// ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ ์ ํ ๋ฉ์์ง๋ฅผ ๋ฐํํฉ๋๋ค.
return c.json(
result ? "FCM token deleted successfully" : "Failed to delete FCM token",
);
}
}
TypeScript
๋ณต์ฌ
4.
api/routers/userRouter.ts
import { UserController } from "./../controllers/userController.ts";
import { Hono } from "https://deno.land/x/hono/mod.ts";
// Hono ์ธ์คํด์ค๋ฅผ ์์ฑํ์ฌ ๋ผ์ฐํฐ๋ฅผ ์ค์ ํฉ๋๋ค.
const userRouter = new Hono();
// UserController ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
const userController = new UserController();
// POST ์์ฒญ์ ์ฒ๋ฆฌํ์ฌ FCM ํ ํฐ์ ์ถ๊ฐํ๋ ์๋ํฌ์ธํธ๋ฅผ ์ ์ํฉ๋๋ค.
userRouter.post(
"/v1/fcmToken",
(c) => userController.postFCMTokenV1(c),
);
// GET ์์ฒญ์ ์ฒ๋ฆฌํ์ฌ ํน์ ์ฌ์ฉ์์ FCM ํ ํฐ ๋ชฉ๋ก์ ๊ฐ์ ธ์ค๋ ์๋ํฌ์ธํธ๋ฅผ ์ ์ํฉ๋๋ค.
userRouter.get(
"/v1/fcmToken",
(c) => userController.getFCMTokensV1(c),
);
// DELETE ์์ฒญ์ ์ฒ๋ฆฌํ์ฌ FCM ํ ํฐ์ ์ญ์ ํ๋ ์๋ํฌ์ธํธ๋ฅผ ์ ์ํฉ๋๋ค.
userRouter.delete(
"/v1/fcmToken",
(c) => userController.deleteFCMTokenV1(c),
);
// userRouter๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ๋ด๋ณด๋
๋๋ค.
export default userRouter;
TypeScript
๋ณต์ฌ
โข
API ๋ฒ์ ๊ด๋ฆฌ๋ฅผ ์ํด Path์ ๋ฒ์ ์ ์ถ๊ฐํ๊ณ , Controller์ ํจ์์๋ ๋ฒ์ ๋. 1:1 ๋งค์นญ
5.
index.ts
import { Hono } from "https://deno.land/x/hono/mod.ts";
import userRouter from "./routers/userRouter.ts";
// Hono ์ธ์คํด์ค๋ฅผ ์์ฑํ์ฌ ์ ํ๋ฆฌ์ผ์ด์
์ ์ค์ ํฉ๋๋ค.
const app = new Hono();
// ๊ธฐ๋ณธ ๊ฒฝ๋ก๋ฅผ "/api"๋ก ์ค์ ํ๊ณ , "/users" ๊ฒฝ๋ก์ ๋ํด userRouter๋ฅผ ์ค์ ํฉ๋๋ค.
app.basePath("/api")
.route("/users", userRouter);
// ์ ํ๋ฆฌ์ผ์ด์
์ ์์ํ์ฌ ์์ฒญ์ ์์ ํฉ๋๋ค.
Deno.serve(app.fetch);
TypeScript
๋ณต์ฌ
โข
{supabase base url}/{api}/{route}/{userRouter ํ์ path}
4. ๋ก์ปฌ ํ ์คํธ
1.
GET
2.
POST
3.
DELETE
5. Public ๋ฐฐํฌ
bash deploy.sh
TypeScript
๋ณต์ฌ
public ๋ฐฐํฌ ํ public API endpoint ํ
์คํธ๋ ์งํํฉ๋๋ค.