서버리스 REST API 구축
AWS Lambda, DynamoDB, API Gateway, Route 53을 활용하여 서버 없이 확장 가능한 REST API를 구축합니다.
- DynamoDB 테이블 (NoSQL 데이터베이스)
- Lambda 함수 (CRUD 로직)
- API Gateway (REST 엔드포인트)
- 커스텀 도메인 연결 (선택)
- 프론트엔드 연동 예제
Lambda
월 100만 요청 무료
DynamoDB
25GB 스토리지, 월 2억 요청 무료
API Gateway
월 100만 요청 무료 (12개월)
Route 53
호스팅 영역당 $0.50/월 (선택)
전체 아키텍처
이 워크샵에서 구축할 서버리스 API 아키텍처입니다.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client │────▶│ Route 53 │────▶│ API Gateway │────▶│ Lambda │
│ (Browser) │ │ (Domain) │ │ (REST API) │ │ (Function) │
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ DynamoDB │
│ (Table) │
└─────────────┘
# 비용 (프리티어 기준):
# - Lambda: 월 100만 요청 무료
# - DynamoDB: 25GB 스토리지, 월 2억 요청 무료
# - API Gateway: 월 100만 요청 무료
# - Route 53: 호스팅 영역당 $0.50/월 (선택사항)사전 준비
워크샵을 시작하기 전에 필요한 것들입니다.
# 필수 준비물:
# 1. AWS 계정 (프리티어 가능)
# 2. AWS CLI 설치 및 설정
# 3. Node.js 18+ 설치
# AWS CLI 설치 확인
aws --version
# AWS 자격 증명 설정
aws configure
# AWS Access Key ID: [YOUR_ACCESS_KEY]
# AWS Secret Access Key: [YOUR_SECRET_KEY]
# Default region name: ap-northeast-2
# Default output format: jsonAWS Console에서 테이블 생성
DynamoDB 콘솔에서 테이블을 생성합니다.
# 1. AWS Console > DynamoDB > Tables > Create table
# 테이블 설정:
# Table name: TodoItems
# Partition key: id (String)
# Table settings: Default settings (On-demand 선택 권장)
# Create table 클릭AWS CLI로 테이블 생성 (대안)
CLI를 사용하여 테이블을 생성할 수도 있습니다.
aws dynamodb create-table \
--table-name TodoItems \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region ap-northeast-2
# 테이블 생성 확인
aws dynamodb describe-table --table-name TodoItems테스트 데이터 삽입
테이블이 정상적으로 작동하는지 테스트합니다.
aws dynamodb put-item \
--table-name TodoItems \
--item '{
"id": {"S": "1"},
"title": {"S": "Learn AWS Lambda"},
"completed": {"BOOL": false},
"createdAt": {"S": "2024-01-01T00:00:00Z"}
}'
# 데이터 조회
aws dynamodb scan --table-name TodoItemsIAM 역할 생성
Lambda가 DynamoDB에 접근할 수 있는 IAM 역할을 생성합니다.
# 1. AWS Console > IAM > Roles > Create role
# Trusted entity type: AWS service
# Use case: Lambda
# Permissions policies 추가:
# - AWSLambdaBasicExecutionRole (CloudWatch Logs 권한)
# - AmazonDynamoDBFullAccess (DynamoDB 권한)
# Role name: TodoApiLambdaRole
# Create role 클릭Lambda 함수 코드 작성
로컬에서 Lambda 함수 코드를 작성합니다.
// index.mjs
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
GetCommand,
PutCommand,
ScanCommand,
DeleteCommand,
} from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";
const client = new DynamoDBClient({ region: "ap-northeast-2" });
const docClient = DynamoDBDocumentClient.from(client);
const TABLE_NAME = "TodoItems";
export const handler = async (event) => {
const { httpMethod, pathParameters, body } = event;
const headers = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
try {
let response;
switch (httpMethod) {
case "GET":
if (pathParameters?.id) {
response = await getItem(pathParameters.id);
} else {
response = await getAllItems();
}
break;
case "POST":
response = await createItem(JSON.parse(body));
break;
case "PUT":
response = await updateItem(pathParameters.id, JSON.parse(body));
break;
case "DELETE":
response = await deleteItem(pathParameters.id);
break;
case "OPTIONS":
return { statusCode: 200, headers, body: "" };
default:
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Unsupported method" }),
};
}
return {
statusCode: 200,
headers,
body: JSON.stringify(response),
};
} catch (error) {
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: error.message }),
};
}
};
async function getAllItems() {
const command = new ScanCommand({ TableName: TABLE_NAME });
const result = await docClient.send(command);
return result.Items;
}
async function getItem(id) {
const command = new GetCommand({
TableName: TABLE_NAME,
Key: { id },
});
const result = await docClient.send(command);
return result.Item;
}
async function createItem(data) {
const item = {
id: randomUUID(),
...data,
createdAt: new Date().toISOString(),
};
const command = new PutCommand({
TableName: TABLE_NAME,
Item: item,
});
await docClient.send(command);
return item;
}
async function updateItem(id, data) {
const item = { id, ...data, updatedAt: new Date().toISOString() };
const command = new PutCommand({
TableName: TABLE_NAME,
Item: item,
});
await docClient.send(command);
return item;
}
async function deleteItem(id) {
const command = new DeleteCommand({
TableName: TABLE_NAME,
Key: { id },
});
await docClient.send(command);
return { deleted: true };
}Lambda 함수 배포
AWS Console에서 Lambda 함수를 생성하고 코드를 배포합니다.
# 1. AWS Console > Lambda > Create function
# Function name: TodoApiFunction
# Runtime: Node.js 20.x
# Architecture: arm64 (비용 절감)
# Execution role: Use an existing role > TodoApiLambdaRole
# Create function 클릭
# 2. Code source에 위의 코드를 붙여넣기
# 3. Deploy 클릭
# 또는 CLI로 배포:
# zip -r function.zip index.mjs
# aws lambda create-function \
# --function-name TodoApiFunction \
# --runtime nodejs20.x \
# --handler index.handler \
# --role arn:aws:iam::YOUR_ACCOUNT_ID:role/TodoApiLambdaRole \
# --zip-file fileb://function.zipLambda 테스트
Lambda 함수가 정상 작동하는지 테스트합니다.
# AWS Console > Lambda > TodoApiFunction > Test
# Test event 생성:
{
"httpMethod": "GET",
"pathParameters": null,
"body": null
}
# Test 클릭하여 결과 확인REST API 생성
API Gateway에서 새 REST API를 생성합니다.
# 1. AWS Console > API Gateway > Create API
# 2. REST API > Build 선택
# API details:
# API name: TodoApi
# API endpoint type: Regional
# Create API 클릭리소스 및 메서드 생성
API 리소스와 HTTP 메서드를 설정합니다.
# /todos 리소스 생성
# 1. Resources > Create resource
# Resource name: todos
# Resource path: /todos
# CORS 활성화 체크
# Create resource 클릭
# GET /todos 메서드 생성
# 1. /todos 선택 > Create method
# Method type: GET
# Integration type: Lambda Function
# Lambda function: TodoApiFunction
# Create method 클릭
# POST /todos 메서드 생성
# 동일하게 POST 메서드 추가
# /todos/{id} 리소스 생성
# 1. /todos 선택 > Create resource
# Resource name: {id}
# Resource path: {id}
# Create resource 클릭
# GET, PUT, DELETE /todos/{id} 메서드 생성
# 각각 Lambda 함수와 연결CORS 설정
브라우저에서 API를 호출할 수 있도록 CORS를 설정합니다.
# 각 리소스에서 CORS 활성화
# 1. /todos 선택 > Enable CORS
# 2. Access-Control-Allow-Origin: *
# 3. Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
# 4. Save 클릭
# /todos/{id}에도 동일하게 CORS 설정API 배포
API를 스테이지에 배포합니다.
# 1. Deploy API 클릭
# 2. Stage: *New Stage*
# Stage name: prod
# Deploy 클릭
# 배포 완료 후 Invoke URL 확인:
# https://xxxxxxxxxx.execute-api.ap-northeast-2.amazonaws.com/prodAPI 테스트
배포된 API를 테스트합니다.
# 전체 조회
curl https://YOUR_API_ID.execute-api.ap-northeast-2.amazonaws.com/prod/todos
# 새 항목 생성
curl -X POST \
https://YOUR_API_ID.execute-api.ap-northeast-2.amazonaws.com/prod/todos \
-H "Content-Type: application/json" \
-d '{"title": "Learn Serverless", "completed": false}'
# 특정 항목 조회
curl https://YOUR_API_ID.execute-api.ap-northeast-2.amazonaws.com/prod/todos/ITEM_ID
# 항목 수정
curl -X PUT \
https://YOUR_API_ID.execute-api.ap-northeast-2.amazonaws.com/prod/todos/ITEM_ID \
-H "Content-Type: application/json" \
-d '{"title": "Learn Serverless", "completed": true}'
# 항목 삭제
curl -X DELETE \
https://YOUR_API_ID.execute-api.ap-northeast-2.amazonaws.com/prod/todos/ITEM_IDACM 인증서 발급
커스텀 도메인용 SSL 인증서를 발급받습니다.
# 1. AWS Console > Certificate Manager (us-east-1 리전!)
# 2. Request certificate > Request a public certificate
# Domain names: api.yourdomain.com
# Validation method: DNS validation
# Request 클릭
# 3. 인증서 상세 > Create records in Route 53
# (Route 53에서 도메인을 관리하는 경우 자동 검증)API Gateway 커스텀 도메인 생성
API Gateway에서 커스텀 도메인을 설정합니다.
# 1. API Gateway > Custom domain names > Create
# Domain name: api.yourdomain.com
# API endpoint type: Regional
# ACM certificate: 위에서 발급받은 인증서 선택
# Create domain name 클릭
# 2. API mappings 탭 > Configure API mappings
# API: TodoApi
# Stage: prod
# Path: (비워두기)
# Save 클릭Route 53 레코드 생성
Route 53에서 API Gateway를 가리키는 레코드를 생성합니다.
# 1. Route 53 > Hosted zones > yourdomain.com
# 2. Create record
# Record name: api
# Record type: A
# Alias: Yes
# Route traffic to:
# - Alias to API Gateway API
# - Region: ap-northeast-2
# - API Gateway domain name 선택
# Create records 클릭
# 3. 전파 대기 (최대 몇 분 소요)
# 4. 테스트
curl https://api.yourdomain.com/todosAPI 호출 예제
프론트엔드에서 서버리스 API를 호출합니다.
// lib/api.ts
const API_URL = "https://YOUR_API_ID.execute-api.ap-northeast-2.amazonaws.com/prod";
export async function getTodos() {
const res = await fetch(`${API_URL}/todos`);
return res.json();
}
export async function createTodo(title: string) {
const res = await fetch(`${API_URL}/todos`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, completed: false }),
});
return res.json();
}
export async function updateTodo(id: string, data: any) {
const res = await fetch(`${API_URL}/todos/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return res.json();
}
export async function deleteTodo(id: string) {
const res = await fetch(`${API_URL}/todos/${id}`, {
method: "DELETE",
});
return res.json();
}React 컴포넌트 예제
Todo 리스트 컴포넌트 예제입니다.
// components/TodoList.tsx
"use client";
import { useEffect, useState } from "react";
import { getTodos, createTodo, deleteTodo } from "@/lib/api";
export default function TodoList() {
const [todos, setTodos] = useState([]);
const [newTitle, setNewTitle] = useState("");
useEffect(() => {
loadTodos();
}, []);
async function loadTodos() {
const data = await getTodos();
setTodos(data);
}
async function handleCreate() {
if (!newTitle.trim()) return;
await createTodo(newTitle);
setNewTitle("");
loadTodos();
}
async function handleDelete(id: string) {
await deleteTodo(id);
loadTodos();
}
return (
<div>
<div className="flex gap-2 mb-4">
<input
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="New todo..."
className="border px-3 py-2 rounded"
/>
<button onClick={handleCreate} className="bg-blue-500 text-white px-4 py-2 rounded">
Add
</button>
</div>
<ul className="space-y-2">
{todos.map((todo: any) => (
<li key={todo.id} className="flex justify-between items-center border p-3 rounded">
<span>{todo.title}</span>
<button onClick={() => handleDelete(todo.id)} className="text-red-500">
Delete
</button>
</li>
))}
</ul>
</div>
);
}리소스 삭제 순서
비용 발생을 방지하기 위해 리소스를 삭제합니다.
# 1. API Gateway 삭제
# API Gateway > APIs > TodoApi > Delete
# 2. Lambda 함수 삭제
# Lambda > Functions > TodoApiFunction > Delete
# 3. DynamoDB 테이블 삭제
# DynamoDB > Tables > TodoItems > Delete
# 4. IAM 역할 삭제
# IAM > Roles > TodoApiLambdaRole > Delete
# 5. (선택) Route 53 레코드 삭제
# Route 53 > Hosted zones > 레코드 삭제
# 6. (선택) ACM 인증서 삭제
# Certificate Manager > 인증서 삭제
# 7. (선택) API Gateway 커스텀 도메인 삭제
# API Gateway > Custom domain names > 삭제축하합니다! 서버리스 REST API가 완성되었습니다. 이제 서버 관리 없이 자동으로 확장되는 API를 운영할 수 있습니다.
Next Steps:
- • API 인증 추가 (Cognito, API Key)
- • Lambda Layer로 공통 코드 분리
- • CloudWatch로 모니터링 설정
- • SAM/Serverless Framework로 IaC 전환
- • CI/CD 파이프라인 구축