1. Concept πŸ‘©β€πŸ’»

Edge Middleware

Serverless Functions λŠ” μœ„ κ·Έλ¦Όκ³Ό 같이 Middleware λ₯Ό 톡해 μ‹€ν–‰λœλ‹€. Proxy μ„œλ²„κ°€ Middleware κΈ°λŠ₯을 ν•΄μ„œ 톡신을 μ€‘κ°œν•˜λŠ” 역할을 ν•˜λŠ”λ° μ‚¬μš©λ˜λ“―μ΄ Edge Middleware μ—­μ‹œ 톡신을 μ€‘κ°œν•œλ‹€.

ν•˜μ§€λ§Œ λ‘˜μ€ λΆ„λͺ…ν•œ 차이점이 μ‘΄μž¬ν•œλ‹€. Proxy λŠ” App Layer μ—μ„œ μž‘λ™ν•œλ‹€λŠ” κ²ƒλ§Œ μ œμ™Έν•˜λ©΄ VPN κ³Ό λ§ˆμ°¬κ°€μ§€λ‘œ ν΄λΌμ΄μ–ΈνŠΈμ™€ μ„œλ²„ 사이에 Middleware 둜 μœ„μΉ˜ν•΄ 직접 μ–‘λ°©ν–₯으둜 톡신을 ν•œλ‹€. ν•˜μ§€λ§Œ Serverless Functions λ₯Ό μ‚¬μš©ν•˜κΈ° μœ„ν•œ Edge Middleware λŠ” ν΄λΌμš°λ“œ ν™˜κ²½μ—μ„œ μ‹€ν–‰λ˜λŠ” μž‘μ€ μ½”λ“œμ‘°κ°μœΌλ‘œ νŠΉμ • μ΄λ²€νŠΈκ°€ λ°œμƒν•  λ•Œ ν•„μš”ν•œ λ¦¬μ†ŒμŠ€λ§Œ μ‚¬μš©ν•΄ μž‘λ™ν•œλ‹€.

Serverless Functions λ₯Ό μ™œ μ‚¬μš©ν•˜λŠ”κ°€μ— λŒ€ν•΄μ„œλŠ” ν΄λΌμš°λ“œ μΈ‘ μ„€λͺ…은 μ„œλ²„λ₯Ό κ΅¬μΆ•ν•˜κ³  관리, ν™•μž₯ν•˜λŠ” 것과 같은 것이 ν•„μš”ν•˜μ§€ μ•Šμ•„ κ°œλ°œμžλŠ” μ½”λ“œλ₯Ό μž‘μ„±ν•˜κ³  λ°°ν¬ν•˜λŠ” 데 집쀑할 수 있게 ν•΄μ€€λ‹€κ³  ν•œλ‹€.

λ˜ν•œ λ…λ¦½μ μœΌλ‘œ μ‹€ν–‰λ˜κΈ° λ•Œλ¬Έμ— ν•¨μˆ˜μ˜ 였λ₯˜κ°€ μ•± 전체에 영ν–₯을 주지 μ•ŠμœΌλ©°, μ‚¬μš©ν•œ 만큼의 λΉ„μš©λ§Œ μ§€λΆˆν•˜κΈ° λ•Œλ¬Έμ— 직접 μ„œλ²„λ₯Ό ꡬ좕해 항상 μš΄μ˜ν•˜λŠ” 것보닀 μ €λ ΄ν•˜λ‹€κ³ ν•œλ‹€.

ν•˜μ§€λ§Œ 이것은 μ–΄λ””κΉŒμ§€λ‚˜ ν΄λΌμš°λ“œ μ„œλΉ„μŠ€λ₯Ό μ‚¬μš©ν•΄ ν”„λ‘ νŠΈμ—”λ“œλ₯Ό μ„œλ²„λ₯Ό μš΄μ˜ν•˜λŠ” 것을 κΈ°μ€€μœΌλ‘œ ν•œ μ„€λͺ…이닀. Serverless Functions κ°€ ν•„μš”ν•œ ꢁ극적인 μ΄μœ λŠ” API Key 와 같은 λ³΄μ•ˆ 정보λ₯Ό λ…ΈμΆœν•˜μ§€ μ•ŠκΈ° μœ„ν•¨μ΄ λ”μš± 크닀.

λ°±μ—”λ“œ μ€‘μ‹¬μ˜ μ›Ή ν™˜κ²½μ—μ„œλŠ” λ°±μ—”λ“œκ°€ API μš”μ²­μ€ 물둠이고, 화면을 λ§Œλ“œλŠ” λ Œλ”λ§ μž‘μ—…κΉŒμ§€ λͺ¨λ‘ λ°±μ—”λ“œ μ„œλ²„μ—μ„œ ν–ˆλ‹€. 이 경우 ν΄λΌμ΄μ–ΈνŠΈλŠ” μžμ‹ μ˜ 인증 μ •λ³΄λ§Œ λ³΄κ΄€ν•˜λ‹€ μ„œλ²„μ— μš”μ²­μ„ 보내면 μ„œλ²„κ°€ 검증 ν›„ λͺ¨λ“  μš”μ²­κ±΄μ„ μ²˜λ¦¬ν•˜κΈ° λ•Œλ¬Έμ— API Key κ°€ λ…ΈμΆœλ  ν•„μš”κ°€ μ—†μ—ˆλ‹€. λ¬Έμ œλŠ” ν”„λ‘ νŠΈμ—”λ“œκ°€ λ‹¨μˆœ Document κ°€ μ•„λ‹Œ μ•±μœΌλ‘œ μž‘λ™ν•˜κΈ° μ‹œμž‘ν•˜λ©΄μ„œλΆ€ν„°λ‹€. ν”„λ‘ νŠΈμ—”λ“œκ°€ 쀑심이 λ˜λŠ” 경우, ν΄λΌμ΄μ–ΈνŠΈμ˜ λΈŒλΌμš°μ € μƒμ—μ„œ ν•˜λ‚˜μ˜ 앱이 λŒμ•„κ°€κ³  μžˆλŠ” κ²ƒμ΄λ‚˜ λ§ˆμ°¬κ°€μ§€μ΄κΈ° λ•Œλ¬Έμ— λͺ¨λ“  λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 ν΄λΌμ΄μ–ΈνŠΈμ— μ „μ†‘λœλ‹€. 이 말은 ν΄λΌμ΄μ–ΈνŠΈκ°€ μ–΄λ–€ μš”μ²­μ„ ν•˜κΈ° μœ„ν•΄ API Key λ₯Ό 가지고 μžˆμ–΄μ•Όν•œλ‹€λŠ” 말이닀.

λ§Œμ•½ 이것을 λ…ΈμΆœν•˜μ§€ μ•ŠκΈ° μœ„ν•΄μ„œλŠ” 외버 μ„œλ²„λ₯Ό 포함해 ν΄λΌμ΄μ–ΈνŠΈκ°€ μš”μ²­ν•˜λŠ” λͺ¨λ“  μ„œλ²„κ°€ λ‚΄ 인증 정보λ₯Ό 가지고 μžˆμ–΄μ•Ό ν•œλ‹€λŠ” μ˜λ―Έκ°€ λœλ‹€. λ¬Όλ‘ , ν•œ νšŒμ‚¬κ°€ μ œκ³΅ν•˜λŠ” μ—¬λŸ¬ μ„œλΉ„μŠ€λΌλ©΄ Token 을 μ‚¬μš©ν•˜λ©΄ κ°€λŠ₯은 ν•˜λ‹€. 이λ₯Ό μœ„ν•΄μ„œλŠ” λ°˜λ“œμ‹œ API λ°±μ—”λ“œ μ„œλ²„μ™€ ν”„λ‘ νŠΈμ—”λ“œ μ„œλ²„μ˜ λΆ„λ¦¬κ°œλ°œ 운영이 ν•„μš”ν•˜λ©°, μ™ΈλΆ€ μ„œλ²„μ— μš”μ²­μ„ ν΄λΌμ΄μ–ΈνŠΈκ°€ 직접 ν•  수 μ—†κ³  λ°±μ—”λ“œκ°€ κ°œλ°œμ„ ν•΄μ€˜ λ°±μ—”λ“œ μ„œλ²„λ₯Ό Middleware 둜 μ‚¬μš©ν•΄ ν†΅μ‹ ν•΄μ•Όν•œλ‹€λŠ” 것이닀.

ν΄λΌμ΄μ–ΈνŠΈ μ„±λŠ₯이 μ’‹μ•„μ Έ λ°±μ—”λ“œ μ„œλ²„μ˜ 뢀담을 λΆ„μ‚°μ‹œν‚€κ³ μž ν”„λ‘ νŠΈμ—”λ“œ 개발이 μƒκ²¨λ‚¬λŠ”λ° κ²°κ΅­ 과거에 발이 λ¬Άμ΄λŠ” 꼴이 λ˜λŠ” 것이닀. ν”„λ‘ νŠΈμ—”λ“œ κ°œλ°œμ—μ„œ Serverless λŠ” ν”„λ‘ νŠΈμ—”λ“œ κ°œλ°œμžκ°€ λ°±μ—”λ“œμ— μ’…μ†λ˜μ§€ μ•Šκ³  API μš”μ²­ 처리λ₯Ό ν•  수 μžˆλ„λ‘ ν΄λΌμš°λ“œκ°€ μ œκ³΅ν•˜λŠ” κ°„νŽΈν•œ λ°±μ—”λ“œ ν΄λΌμš°λ“œ μ„œλΉ„μŠ€λ₯Ό μ‚¬μš©ν•΄ API Key λ₯Ό κ°μΆ”λŠ” 것이 κ°€μž₯ 핡심이 λœλ‹€.

즉, λ‚΄κ°€ 직접 λ°±μ—”λ“œ API λ₯Ό ꡬ좕해 Middleware 둜 μ‚¬μš©ν•˜λŠ” λŒ€μ‹ , ν΄λΌμš°λ“œκ°€ μ œκ³΅ν•˜λŠ” Middleware λ₯Ό λ¦¬μ†ŒμŠ€ λΉ„μš©λ§Œ λ‚΄κ³  μ €λ ΄ν•˜κ²Œ μ‚¬μš©ν•˜κ²Œ ν•΄μ€„κ²Œ, λŒ€μ‹  λ‚΄κ°€ μ •μ˜ν•œ κ·œμΉ™λŒ€λ‘œ λ„ˆκ°€ μ‚¬μš©ν•  ν•¨μˆ˜λ§Œ λ§Œλ“€μ–΄κ°€ Serverless Functions 의 핡심이닀.


2. Quick Start πŸ‘©β€πŸ’»

1. Install Vercel CLI

npm i -D vercel@latest
# or
yarn add vercel@latest

μžμ„Έν•œ μ„€μ • ν™˜κ²½μ€ Vercel Functions Quickstart νŽ˜μ΄μ§€λ₯Ό μ°Έκ³ ν•œλ‹€. Vercel 은 직접 TypeScript λ₯Ό μ§€μ›ν•˜κΈ° λ•Œλ¬Έμ— d.tsλ₯Ό μ„€μΉ˜ν•  ν•„μš”κ°€ μ—†κ³  λ‘œμ»¬μ—μ„œ 싀행을 μœ„ν•΄ Vercel CLI 만 μ„€μΉ˜ν•˜λ©΄ λœλ‹€.

  • package.json
{
  "scripts": {
    "vercel": "vercel dev"
  }
}

그리고 root κ²½λ‘œμ— vercel.json νŒŒμΌμ„ μƒμ„±ν•œλ‹€.

{
  "devCommand": "npm run dev",
  "buildCommand": "npm run build"
}

yarn을 μ‚¬μš©ν•  경우 μœ„ λͺ…λ Ήμ–΄λŠ” npm이 μ•„λ‹Œ yarn을 μ‚¬μš©ν•œλ‹€.

2. Create API Functions

root κ²½λ‘œμ— apiλΌλŠ” 디렉토리λ₯Ό μƒμ„±ν•˜κ³ , ν•¨μˆ˜λ₯Ό μž‘μ„±ν•΄μ•Όν•˜λŠ”λ° ν•¨μˆ˜λŠ” MSW와 μœ μ‚¬ν•˜κ²Œ μž‘μ„±ν•˜λ©΄ λœλ‹€.

  • /api/user.ts
import type { VercelRequest, VercelResponse } from "@vercel/node";

const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];

export default function handler(request: VercelRequest, response: VercelResponse) {
  const method = ALLOWED_METHODS.find((method) => method === request.method);
  if (method === undefined) return;
  user[method](request, response);
}

const user: Record<string, Function> = {
  GET: getUser,
  POST: postUser,
  PUT: putUser,
  PATCH: patchUser,
  DELETE: deleteUser,
};

function getUser(request: VercelRequest, response: VercelResponse): VercelResponse {
  return response.status(200).json({
    name: "Hogwarts",
    age: 32,
    favorite: ["Movie", "Music", "Book", "Beer"],
  });
}

function postUser(request: VercelRequest, response: VercelResponse): VercelResponse {
  return response.status(200).json({});
}

function putUser(request: VercelRequest, response: VercelResponse): VercelResponse {
  return response.status(200).json({});
}

function patchUser(request: VercelRequest, response: VercelResponse): VercelResponse {
  return response.status(200).json({});
}

function deleteUser(request: VercelRequest, response: VercelResponse): VercelResponse {
  return response.status(200).json({});
}

npm run vercelλͺ…령을 μž…λ ₯ν•˜λ©΄ μœ„μ— μž‘μ„±ν•œ Vercel λͺ…λ Ήμ–΄κ°€ μ‹€ν–‰λœλ‹€. Vercel 에 둜그인 ν›„ ν”„λ‘œμ νŠΈ 섀정이 λ‚˜μ˜¬ λ•Œ μ„€λͺ…을 읽고 적절히 Y/N 을 섀택해주면 λœλ‹€. λŒ€λΆ€λΆ„μ˜ 경우 선택해야 ν•  κ²ƒμœΌλ‘œ νŒλ‹¨λ˜λŠ” 것이 λŒ€λ¬Έμžλ‘œ ν‘œκΈ°(Y/n 이면 Y λ₯Ό 선택할 κ²ƒμœΌλ‘œ μ˜ˆμƒ, y/N 이면 N 을 선택할 κ²ƒμœΌλ‘œ μ˜ˆμƒν•˜κ³  터미널 λ©”μ‹œμ§€κ°€ 좜λ ₯)λ˜λ‹ˆ μ°Έκ³ ν•˜λ©΄ λœλ‹€.

Vercel 을 μ‚¬μš©ν•΄ μ„œλ²„κ°€ 싀행돠면 μœ„ κ²½λ‘œμ— API μš”μ²­μ„ λ³΄λ‚΄λ³΄μž. ν˜Ήμ‹œλΌλ„ Hash Router λ₯Ό μ‚¬μš©ν•˜κ³  μžˆλ‹€λ©΄ #λŠ” μ§€μ›Œμ•Ό ν•œλ‹€. λΌμš°νŒ… JavaScript 둜 μž‘μ„±ν•œ κΈ°λŠ₯을 μ‚¬μš©ν•˜λŠ” 것이 μ•„λ‹ˆκ³  μœ„μ—μ„œ μž‘μ„±ν•œ ν•¨μˆ˜λŠ” 또 λ‹€λ₯Έ Middleware Server 둜 μž‘λ™ν•˜λŠ” 것이라 보면 되기 λ•Œλ¬Έμ΄λ‹€. 즉, express λ₯Ό μ‚¬μš©ν•œ Node μ„œλ²„λ₯Ό λ„μš°κ±°λ‚˜ MSW λ₯Ό μ‚¬μš©ν•΄ HTTP API μš”μ²­μ— μ‘λ‹΅ν•˜λŠ” μ„œλ²„λ₯Ό μ‚¬μš©ν•˜λŠ” 것과 κ°™λ‹€κ³  보면 λœλ‹€. 이것이 κ°€λŠ₯ν•œ μ΄μœ κ°€ Vercel 을 μ„€μΉ˜ν•˜λ©΄ dependencies 둜 https-proxy-agent, make-dir, node-fetch와 같은 것듀이 ν•¨κ»˜ μ„€μΉ˜λ˜κΈ° λ•Œλ¬Έμ΄λ‹€.

이제 npm run vercel둜 μ„œλ²„λ₯Ό λ„μš΄ λ‹€μŒ μ•„λž˜ μ£Όμ†Œλ‘œ GET μš”μ²­μ„ λ³΄λ‚΄λ³΄μž.

http://localhost:3000/api/user

Postman 을 μ‚¬μš©ν•΄λ„ μ’‹κ³ , ν„°λ―Έλ„λ‘œ Curl, Wget 등을 μ‚¬μš©ν•΄λ„ μ’‹λ‹€. μ•„λ‹ˆλ©΄ ν”„λ‘ νŠΈμ—”λ“œ μ„œλ²„μ—μ„œ λ°”λ‘œ fetch μš”μ²­μ„ λ‚ λ €λ³΄λŠ” 것도 μ’‹λ‹€. μš°λ¦¬λŠ” 결과둜 Status Code 200 κ³Ό λ‹€μŒ JSON 데이터λ₯Ό 얻을 수 μžˆλ‹€.

{
  "name": "Hogwarts",
  "age": 32,
  "favorite": [ "Movie", "Music", "Book", "Beer" ]
}


λ‹€μ‹œ 말해, ν”„λ‘ νŠΈμ—”λ“œ μ½”λ“œμ—μ„œ /api/user 경둜둜 μš”μ²­μ„ 보내면 Vercel 이 λ„μš΄ Middleware Server κ°€ HTTP API μš”μ²­μ„ μ²˜λ¦¬ν•˜λŠ” 것이닀.

;(async () => {
  const response = await fetch("/api/user");
  const data = await response.json();
  console.table(data);
})();


ν”„λ‘ νŠΈμ—”λ“œ μ„œλ²„λ₯Ό λ„μš΄λ‹€λŠ” 것은 ν”„λ‘ νŠΈμ—”λ“œλ₯Ό μ„œλΉ„μŠ€ν•˜κΈ° μœ„ν•œ HTML, CSS, JavaScript λ₯Ό ν˜ΈμŠ€νŒ…ν•˜λŠ” μ„œλ²„λ₯Ό λ„μš°λŠ” 것을 μ˜λ―Έν•œλ‹€. μœ„μ™€ 같은 API μš”μ²­μ„ ν•˜λ €λ©΄ API μš”μ²­ μ²˜λ¦¬κ°€ κ°€λŠ₯ν•œ λ°±μ—”λ“œ μ„œλ²„κ°€ ν•„μš”ν•˜λ‹€.

import express from "express";
import router from "./router/index";
import * as ejs from "ejs";

const app = express();
const port = 3000;

app.listen(port, () => {
    console.log(`Playground app listening at http://localhost:${port}`)
})

app.use(express.static("public"));
app.use(express.static("router"));
app.use("/scripts", express.static("node_modules"));
app.use("/js", express.static("dist"));
app.disable("etag");
app.set("views", "/views");
app.set("view engine", "ejs");
app.engine("ejs", ejs.renderFile);
app.use(router);
import express, {response} from "express";
import * as path from "path";

const router = express.Router();
const __dirname = path.resolve();

export default router;

router.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "main.html"))
})

router.get("/product/vegetable", (req, res) => {
  res.render(path.join(__dirname, "views", "product/vegetable.ejs"))
})

router.get("/join", (req, res) => {
  res.render(path.join(__dirname, "views", "user/join.ejs"))
})

router.get("/promotion", (req, res) => {
  const { type } = req
  const promotion = // Request data to database...
  res.json(promotion)
})

μ΄λŸ°μ‹μœΌλ‘œ λ°±μ—”λ“œ μ„œλ²„λ₯Ό 직접 λ„μ›Œμ•Όν•˜λŠ”λ° Vercel 을 μ„€μΉ˜ν•˜λ©΄ MSWλ₯Ό μ‚¬μš©ν•˜λ“― λ‹¨μˆœν•˜κ²Œ 경둜λ₯Ό μƒμ„±ν•˜κ³  ν•„μš”ν•œ ν•¨μˆ˜λ§Œ λ§Œλ“€λ©΄ λ‚˜λ¨Έμ§€ μ„œλ²„ ꡬ좕을 μ‚¬μš©μžκ°€ 직접 ν•˜μ§€ μ•Šμ•„λ„ 되기 λ•Œλ¬Έμ— Serverless Functions 라 ν•˜λŠ” 것이닀. μ„œλ²„κ°€ μ—†μ–΄μ„œκ°€ μ•„λ‹ˆκ³  μ„œλ²„λ₯Ό 직접 λ§Œλ“€μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— Serverless λΌλŠ” 것에 μœ μ˜ν•΄μ•Όν•œλ‹€.

3. Dynamic Routes

Next.js 와 λ§ˆμ°¬κ°€μ§€λ‘œ 동적 λΌμš°νŒ…μ΄ κ°€λŠ₯ν•˜λ‹€.

dynamic Routes 1

λ˜λŠ”

dynamic Routes 2

와 같은 ꡬ쑰둜 디렉토리와 νŒŒμΌμ„ μƒμ„±ν•˜λ©΄

/api/user와 /api/user/uuid005435λ₯Ό ꡬ뢄할 수 μžˆλ‹€. uuid005435λ₯Ό URL Parameters 둜 μ‚¬μš©ν•˜λŠ” μ‹œμŠ€ν…œμ΄ λ˜λŠ” 것이닀. μ΄λ•Œ μ „λ‹¬λœ URL νŒŒλΌλ―Έν„°λŠ” request.queryλ‘œλΆ€ν„° κΊΌλ‚Ό 수 μžˆλŠ”λ°, [id]κ°€ νŒŒλΌλ―Έν„° 이름이 λ˜μ–΄ {id: value} ν˜•νƒœλ‘œ λ‹΄κ²¨μžˆλ‹€.

  • /api/user/[id].ts
import type { VercelRequest, VercelResponse } from "@vercel/node";

const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];

export default function handler(request: VercelRequest, response: VercelResponse) {
  const method = ALLOWED_METHODS.find((method) => method === request.method);
  if (method === undefined) return;
  user[method](request, response);
}

const user: Record<string, Function> = {
  GET: getUser,
  POST: postUser,
  PUT: putUser,
  PATCH: patchUser,
  DELETE: deleteUser,
};

function getUser(request: VercelRequest, response: VercelResponse) {
  const { id } = request.query;
  return response.status(200).json({
    message: `${id} μ‚¬μš©μž 정보에 λŒ€ν•œ μš”μ²­`,
  });
}

// ...

λ”°λΌμ„œ API μš”μ²­μ€ λ‹€μŒκ³Ό 같이 URL Parameters λ₯Ό ꡬ뢄할 수 있게 λœλ‹€.

  • GET /api/user/ μš”μ²­μ— λŒ€ν•œ 응닡
{
  "name": "Hogwarts",
  "age": 32,
  "favorite": [ "Movie", "Music", "Book", "Beer" ]
}
  • GET /api/user/uuid005435 μš”μ²­μ— λŒ€ν•œ 응닡
{
    "message": "uuid005435 μ‚¬μš©μž 정보에 λŒ€ν•œ μš”μ²­"
}

λ¬Όλ‘ , URL Parameters λ₯Ό μΆ”μΆœν•  λ•ŒλŠ” request.queryμ—μ„œ μΆ”μΆœν•˜μ§€λ§Œ, Body 에 μ‹€λ € 보낸 μ •λ³΄λŠ” request.bodyλ‘œλΆ€ν„° μΆ”μΆœν•œλ‹€.




Reference

  1. λ°•μ˜μ›…, β€œν”„λ‘ νŠΈμ—”λ“œ μ›Ή 개발의 λͺ¨λ“  것 초격차 νŒ¨ν‚€μ§€ Online.” fastcampus.co.kr. last modified unknown, Fast Campus.
  2. β€œConfiguring Projects with vercel.json.” Vercel. Feb. 21, 2023, accessed May. 04, 2024, Vercel - Project Configuration.