Search
๐Ÿ“š

EP11. Supabase Edge Functions Architecture

์ƒ์„ฑ์ผ
2024/06/06 02:19
๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ
2024/06/06

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 ํ…Œ์ŠคํŠธ๋„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

๊ฐ•์˜ ์ฝ”๋“œ

3
pull