본문으로 바로가기 본문 바로가기
WIKI 기술 지식 베이스

e-커머스 웹사이트에 D1 읽기 복제(Read Replication) 사용하기

원문 보기 위키 갱신

D1 읽기 복제(Read Replication)은 D1 데이터베이스를 여러 리전에 복제할 수 있게 해 주는 기능이에요. 이 기능은 읽기 지연 시간을 줄이고 읽기 처리량을 높이기 때문에 e-커머스 웹사이트에 유용해요. 이 튜토리얼에서는 e-커머스 웹사이트에 D1 읽기 복제를 사용하는 방법을 배워 볼게요.

이 튜토리얼은 가상의 e-커머스 웹사이트를 사용하지만, 이 원리는 뉴스 웹사이트, 소셜 미디어 플랫폼, 마케팅 웹사이트처럼 낮은 읽기 지연 시간과 읽기 확장이 필요한 어떤 사용 사례에도 적용될 수 있어요.

출처: 문서

본문

빠른 시작 (Quick start)

단계를 건너뛰고 빠르게 시작하고 싶다면 아래 버튼을 클릭하세요.

Deploy to Cloudflare

이렇게 하면 GitHub 계정에 저장소가 만들어지고 애플리케이션이 Cloudflare Workers에 배포돼요. 또한 D1 데이터베이스를 만들고 바인딩하며, 필요한 테이블을 만들고 샘플 데이터도 추가해 줘요. 배포 과정에서 Enable read replication 박스를 체크하면 읽기 복제가 활성화돼요.

그런 다음 배포된 애플리케이션을 방문하면 돼요.

사전 준비 (Prerequisites)

  1. Cloudflare 계정 ↗︎에 가입하세요.
  2. Node.js ↗︎를 설치하세요.

Node.js 버전 관리자

Volta ↗︎나 nvm ↗︎ 같은 Node 버전 관리자를 사용하면 권한 문제를 피하고 Node.js 버전을 쉽게 바꿀 수 있어요. 이 가이드에서 뒤에 설명할 Wrangler는 Node 16.17.0 이상 버전을 요구해요.

1단계: Workers 프로젝트 만들기

다음 명령을 실행해 새 Workers 프로젝트를 만드세요.

npm create cloudflare@latest -- fast-commerce
yarn create cloudflare fast-commerce
pnpm create cloudflare@latest fast-commerce

설정 단계에서 다음 옵션을 선택하세요.

  • What would you like to start with? 는 Hello World example을 선택하세요.
  • Which template would you like to use? 는 SSR / full-stack app을 선택하세요.
  • Which language do you want to use? 는 TypeScript를 선택하세요.
  • Do you want to use git for version control? 은 Yes를 선택하세요.
  • Do you want to deploy your application? 은 No를 선택하세요 (배포 전에 몇 가지를 변경할 거예요).

API 라우트를 만들려면 Hono ↗︎를 사용할 거예요. 다음 명령을 실행해 Hono를 설치하세요.

npm i hono
yarn add hono
pnpm add hono
bun add hono

2단계: 프런트엔드 업데이트하기

위 단계는 기본 프런트엔드가 있는 새 Workers 프로젝트를 만들고 Hono를 설치해요. 제품을 나열하도록 프런트엔드를 업데이트할 거예요. 또한 단일 제품을 표시하는 새 페이지를 프런트엔드에 추가할 거예요.

새로 만든 Worker 프로젝트 폴더로 이동하세요.

cd fast-commerce

제품을 나열하도록 public/index.html 파일을 업데이트하세요. 아래 코드를 참고로 사용하세요.

public/index.html

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>E-commerce Store</title>
		<style>
			* {
				margin: 0;
				padding: 0;
				box-sizing: border-box;
				font-family: Arial, sans-serif;
			}

    		body {
    			background-color: #f9fafb;
    			min-height: 100vh;
    			display: flex;
    			flex-direction: column;
    		}

    		header {
    			background-color: white;
    			padding: 1rem 2rem;
    			display: flex;
    			justify-content: space-between;
    			align-items: center;
    			border-bottom: 1px solid #e5e7eb;
    		}

    		.store-title {
    			font-weight: bold;
    			font-size: 1.25rem;
    		}

    		.cart-button {
    			padding: 0.5rem 1rem;
    			cursor: pointer;
    			background: none;
    			border: none;
    		}

    		.products-grid {
    			display: grid;
    			grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    			gap: 1.5rem;
    			padding: 2rem;
    		}

    		.product-card {
    			background-color: white;
    			border-radius: 0.5rem;
    			overflow: hidden;
    			box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    		}

    		.product-info {
    			padding: 1rem;
    		}

    		.product-title {
    			font-size: 1.125rem;
    			font-weight: 600;
    			margin-bottom: 0.5rem;
    		}

    		.product-description {
    			color: #4b5563;
    			font-size: 0.875rem;
    			margin-bottom: 1rem;
    		}

    		.product-price {
    			font-size: 1.25rem;
    			font-weight: bold;
    			margin-bottom: 0.5rem;
    		}

    		.product-stock {
    			color: #4b5563;
    			font-size: 0.875rem;
    			margin-bottom: 1rem;
    		}

    		.view-details-btn {
    			display: block;
    			width: 100%;
    			padding: 0.5rem 0;
    			background-color: #2563eb;
    			color: white;
    			border: none;
    			border-radius: 0.375rem;
    			cursor: pointer;
    			text-align: center;
    			text-decoration: none;
    			font-size: 0.875rem;
    		}

    		.view-details-btn:hover {
    			background-color: #1d4ed8;
    		}

    		footer {
    			background-color: white;
    			padding: 1rem 2rem;
    			text-align: center;
    			border-top: 1px solid #e5e7eb;
    			color: #4b5563;
    			font-size: 0.875rem;
    		}

    		/* Basic Responsiveness */
    		@media (max-width: 768px) {
    			.products-grid {
    				grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    			}
    		}

    		@media (max-width: 480px) {
    			.products-grid {
    				grid-template-columns: 1fr;
    			}
    		}
    	</style>
    </head>
    <body>
    	<header>
    		<h1 class="store-title">E-commerce Store</h1>
    		<button class="cart-button">Cart</button>
    	</header>

    	<main class="products-grid" id="products-container">
    		<!-- Products will be loaded here by JavaScript -->
    	</main>

    	<footer>
    		<p>© 2025 E-commerce Store. All rights reserved.</p>
    	</footer>

    	<script>
    		document.addEventListener('DOMContentLoaded', () => {
    			let products = [];
    			let d1Duration,
    				queryDuration = 0;
    			let dbLocation;
    			let isPrimary = true;

    			// Function to create product HTML
    			function createProductCard(product) {
    				return `
                <div class="product-card" data-category="${product.category}">
                    <div class="product-info">
                        <h3 class="product-title">${product.name}</h3>
                        <p class="product-description">${product.description}</p>
                        <p class="product-price">$${product.price.toFixed(2)}</p>
                        <p class="product-stock">${product.inventory} in stock</p>
                        <a href="product-details.html?id=${product.id}" class="view-details-btn">View Details</a>
                    </div>
                </div>
            `;
    			}

    			// Function to render content
    			function renderContent() {
    				try {
    					const productsContainer = document.getElementById('products-container');
    					if (!productsContainer) return;
    					productsContainer.innerHTML = '';

    					products.forEach((product) => {
    						productsContainer.innerHTML += createProductCard(product);
    					});
    				} catch (error) {
    					console.error('Error rendering content:', error);
    				}
    			}

    			// Fetch products
    			fetch('/api/products')
    				.then((response) => response.json())
    				.then((data) => {
    					products = data;
    					renderContent();
    				})
    				.catch((error) => console.error('Error fetching products:', error));
    		});
    	</script>
    </body>

</html>

단일 제품을 표시하는 새 public/product-details.html 파일을 만드세요.

public/product-details.html

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Product Details - E-commerce Store</title>
		<style>
			* {
				margin: 0;
				padding: 0;
				box-sizing: border-box;
				font-family: Arial, sans-serif;
			}

			body {
				background-color: #f9fafb;
				min-height: 100vh;
				display: flex;
				flex-direction: column;
			}

			header {
				background-color: white;
				padding: 1rem 2rem;
				display: flex;
				justify-content: space-between;
				align-items: center;
				border-bottom: 1px solid #e5e7eb;
			}

			.store-title {
				font-weight: bold;
				font-size: 1.25rem;
				text-decoration: none;
				color: black;
			}

			.cart-button {
				padding: 0.5rem 1rem;
				cursor: pointer;
				background: none;
				border: none;
			}

			.product-container {
				max-width: 800px;
				margin: 2rem auto;
				background-color: white;
				border-radius: 0.5rem;
				box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
				padding: 2rem;
			}

			.product-title {
				font-size: 1.875rem;
				font-weight: bold;
				margin-bottom: 0.5rem;
			}

			.product-description {
				color: #4b5563;
				margin-bottom: 1.5rem;
			}

			.product-price {
				font-size: 1.875rem;
				font-weight: bold;
				margin-bottom: 0.5rem;
			}

			.product-stock {
				font-size: 0.875rem;
				color: #4b5563;
				text-align: right;
			}

			.add-to-cart-btn {
				display: block;
				width: 100%;
				padding: 0.75rem;
				background-color: #2563eb;
				color: white;
				border: none;
				border-radius: 0.375rem;
				cursor: pointer;
				text-align: center;
				font-size: 1rem;
				margin-top: 1.5rem;
			}

			.add-to-cart-btn:hover {
				background-color: #1d4ed8;
			}

			.price-stock-container {
				display: flex;
				justify-content: space-between;
				align-items: center;
				margin-bottom: 1rem;
			}

			footer {
				background-color: white;
				padding: 1rem 2rem;
				text-align: center;
				border-top: 1px solid #e5e7eb;
				color: #4b5563;
				font-size: 0.875rem;
				margin-top: auto;
			}

			/* Back button */
			.back-button {
				display: inline-block;
				margin-bottom: 1.5rem;
				color: #2563eb;
				text-decoration: none;
				font-size: 0.875rem;
			}

			.back-button:hover {
				text-decoration: underline;
			}

			/* Notification */
			.notification {
				position: fixed;
				top: 1rem;
				right: 1rem;
				background-color: #10b981;
				color: white;
				padding: 0.75rem 1rem;
				border-radius: 0.375rem;
				box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
				transform: translateX(150%);
				transition: transform 0.3s ease;
			}

			.notification.show {
				transform: translateX(0);
			}
		</style>
	</head>
	<body>
		<header>
			<a href="index.html" class="store-title">E-commerce Store</a>
			<button class="cart-button">Cart</button>
		</header>

		<main class="product-container">
			<a href="index.html" class="back-button">← Back to products</a>
			<h1 class="product-title" id="product-title">Product Name</h1>
			<p class="product-description" id="product-description">
				Product description goes here.
			</p>

			<div class="price-stock-container">
				<p class="product-price" id="product-price">$0.00</p>
				<p class="product-stock" id="product-stock">0 in stock</p>
			</div>

			<button class="add-to-cart-btn" id="add-to-cart">Add to Cart</button>
		</main>

		<div class="notification" id="notification">Added to cart!</div>

		<footer>
			<p>© 2025 E-commerce Store. All rights reserved.</p>
		</footer>

		<script>
			// Get query parameter from URL
			const url = new URL(window.location.href);
			const searchParams = new URLSearchParams(url.search);
			const productId = searchParams.get("id");

			// Fetch product details
			fetch(`/api/products/${productId}`)
				.then((response) => response.json())
				.then((product) => displayContent(product))
				.catch((error) =>
					console.error("Error fetching product details:", error),
				);

			// Function to display product details
			function displayContent(product) {
				document.title = `${product[0].name} - E-commerce Store`;
				document.getElementById("product-title").textContent = product[0].name;
				document.getElementById("product-description").textContent =
					product[0].description;
				document.getElementById("product-price").textContent =
					`$${product[0].price.toFixed(2)}`;
				document.getElementById("product-stock").textContent =
					`${product[0].inventory} in stock`;
			}
		</script>
	</body>
</html>

이제 제품을 나열하고 단일 제품을 표시하는 프런트엔드가 생겼어요. 하지만 프런트엔드는 아직 D1 데이터베이스에 연결되지 않았어요. 지금 개발 서버를 시작하면 제품이 보이지 않을 거예요. 다음 단계에서 D1 데이터베이스를 만들고 제품을 가져와 프런트엔드에 표시하는 API를 만들 거예요.

3단계: D1 데이터베이스를 만들고 읽기 복제 활성화하기

다음 명령을 실행해 새 D1 데이터베이스를 만드세요.

npx wrangler d1 create fast-commerce

터미널에서 반환된 D1 바인딩을 wrangler 파일에 추가하세요.

{
	"d1_databases": [
		{
			"binding": "DB",
			"database_name": "fast-commerce",
			"database_id": "YOUR_DATABASE_ID"
		}
	]
}
[[d1_databases]]
binding = "DB"
database_name = "fast-commerce"
database_id = "YOUR_DATABASE_ID"

worker-configuration.d.ts 파일의 Env 인터페이스를 업데이트하려면 다음 명령을 실행하세요.

npm run cf-typegen

다음으로 D1 데이터베이스에 대해 읽기 복제를 활성화하세요. Workers & Pages > D1 ↗︎로 이동한 다음 기존 데이터베이스를 선택하고 > Settings > Enable Read Replication을 선택하세요.

4단계: API 라우트 만들기

src/index.ts 파일을 업데이트해서 Hono 라이브러리를 가져오고 API 라우트를 만드세요.

import { Hono } from "hono";
// Set db session bookmark in the cookie
import { getCookie, setCookie } from "hono/cookie";

const app = new Hono<{ Bindings: Env }>();

// Get all products
app.get("/api/products", async (c) => {
	return c.json({ message: "get list of products" });
});

// Get a single product
app.get("/api/products/:id", async (c) => {
	return c.json({ message: "get a single product" });
});

// Upsert a product
app.post("/api/product", async (c) => {
	return c.json({ message: "create or update a product" });
});

export default app;

위 코드는 세 개의 API 라우트를 만들어요.

  • GET /api/products: 제품 목록을 반환해요.
  • GET /api/products/:id: 단일 제품을 반환해요.
  • POST /api/product: 제품을 만들거나 업데이트해요.

하지만 아직 API 라우트가 D1 데이터베이스에 연결되지 않았어요. 다음 단계에서 D1 데이터베이스에 products 테이블을 만들고, API 라우트를 D1 데이터베이스에 연결하도록 업데이트할 거예요.

5단계: 로컬 D1 데이터베이스 스키마 만들기

다음 명령을 실행해 D1 데이터베이스에 products 테이블을 만드세요.

npx wrangler d1 execute fast-commerce --command "CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, inventory INTEGER NOT NULL DEFAULT 0, category TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"

다음으로 다음 명령을 실행해 products 테이블에 인덱스를 만드세요.

npx wrangler d1 execute fast-commerce --command "CREATE INDEX IF NOT EXISTS idx_products_id ON products (id)"

개발 목적으로 다음 명령을 실행해 로컬 D1 데이터베이스에 INSERT 구문을 실행할 수도 있어요.

npx wrangler d1 execute fast-commerce --command "INSERT INTO products (id, name, description, price, inventory, category) VALUES (1, 'Fast Ergonomic Chair', 'A comfortable chair for your home or office', 100.00, 10, 'Furniture'), (2, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing'), (3, 'Fast Wooden Desk', 'A wooden desk for your home or office', 150.00, 5, 'Furniture'), (4, 'Fast Leather Sofa', 'A leather sofa for your home or office', 300.00, 3, 'Furniture'), (5, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing')"

6단계: 재시도 로직 추가하기

애플리케이션을 더 탄력적으로 만들기 위해 API 라우트에 재시도 로직을 추가할 수 있어요. src 디렉터리에 retry.ts라는 새 파일을 만드세요.

export interface RetryConfig {
	maxRetries: number;
	initialDelay: number;
	maxDelay: number;
	backoffFactor: number;
}

const shouldRetry = (error: unknown): boolean => {
	const errMsg = error instanceof Error ? error.message : String(error);
	return (
		errMsg.includes("Network connection lost") ||
		errMsg.includes("storage caused object to be reset") ||
		errMsg.includes("reset because its code was updated")
	);
};

// Helper function for sleeping
const sleep = (ms: number): Promise<void> => {
	return new Promise((resolve) => setTimeout(resolve, ms));
};

export const defaultRetryConfig: RetryConfig = {
	maxRetries: 3,
	initialDelay: 100,
	maxDelay: 1000,
	backoffFactor: 2,
};

export async function withRetry<T>(
	operation: () => Promise<T>,
	config: Partial<RetryConfig> = defaultRetryConfig,
): Promise<T> {
	const maxRetries = config.maxRetries ?? defaultRetryConfig.maxRetries;
	const initialDelay = config.initialDelay ?? defaultRetryConfig.initialDelay;
	const maxDelay = config.maxDelay ?? defaultRetryConfig.maxDelay;
	const backoffFactor =
		config.backoffFactor ?? defaultRetryConfig.backoffFactor;

	let lastError: Error | unknown;
	let delay = initialDelay;

	for (let attempt = 0; attempt <= maxRetries; attempt++) {
		try {
			const result = await operation();
			return result;
		} catch (error) {
			lastError = error;

			if (!shouldRetry(error) || attempt === maxRetries) {
				throw error;
			}

			// Add randomness to avoid synchronizing retries
			// Wait for a random delay between delay and delay*2
			await sleep(delay * (1 + Math.random()));

			// Calculate next delay with exponential backoff
			delay = Math.min(delay * backoffFactor, maxDelay);
		}
	}

	throw lastError;
}

withRetry 함수는 주어진 작업을 지수 백오프(exponential backoff)로 재시도하는 유틸리티 함수예요. 재시도 횟수, 초기 지연, 최대 지연, 백오프 계수를 커스터마이즈할 수 있는 설정 객체를 인자로 받아요. 네트워크 연결 손실, 저장소 재설정, 코드 업데이트로 인한 오류일 때만 작업을 재시도해요.

주의

분산 시스템에서 재시도 메커니즘은 특정 위험을 가질 수 있어요. 재시도 메커니즘의 위험과 이를 피하는 방법에 대해 더 배우려면 Retry Strategies in Distributed Systems: Identifying and Addressing Key Pitfalls ↗︎ 문서를 읽어 보세요.

재시도는 때로 데이터 불일치를 초래할 수 있어요. 재시도 로직을 조심스럽게 처리하세요.

다음으로 src/index.ts 파일을 업데이트해서 withRetry 함수를 가져오고 API 라우트에서 사용하세요.

import { withRetry } from "./retry";

7단계: API 라우트 업데이트하기

API 라우트를 D1 데이터베이스에 연결하도록 업데이트하세요.

1. POST /api/product

app.post("/api/product", async (c) => {
	const product = await c.req.json();

	if (!product) {
		return c.json({ message: "No data passed" }, 400);
	}

	const db = c.env.DB;
	const session = db.withSession("first-primary");

	const { id } = product;

	try {
		return await withRetry(async () => {
			// Check if the product exists
			const { results } = await session
				.prepare("SELECT * FROM products where id = ?")
				.bind(id)
				.run();
			if (results.length === 0) {
				const fields = [...Object.keys(product)];
				const values = [...Object.values(product)];
				// Insert the product
				await session
					.prepare(
						`INSERT INTO products (${fields.join(", ")}) VALUES (${fields.map(() => "?").join(", ")})`,
					)
					.bind(...values)
					.run();
				const latestBookmark = session.getBookmark();
				latestBookmark &&
					setCookie(c, "product_bookmark", latestBookmark, {
						maxAge: 60 * 60, // 1 hour
					});
				return c.json({ message: "Product inserted" });
			}

			// Update the product
			const updates = Object.entries(product)
				.filter(([_, value]) => value !== undefined)
				.map(([key, _]) => `${key} = ?`)
				.join(", ");

			if (!updates) {
				throw new Error("No valid fields to update");
			}

			const values = Object.entries(product)
				.filter(([_, value]) => value !== undefined)
				.map(([_, value]) => value);

			await session
				.prepare(`UPDATE products SET ${updates} WHERE id = ?`)
				.bind(...[...values, id])
				.run();
			const latestBookmark = session.getBookmark();
			latestBookmark &&
				setCookie(c, "product_bookmark", latestBookmark, {
					maxAge: 60 * 60, // 1 hour
				});
			return c.json({ message: "Product updated" });
		});
	} catch (e) {
		console.error(e);
		return c.json({ message: "Error upserting product" }, 500);
	}
});

위 코드에서:

  • 요청 본문에서 제품 데이터를 얻어요.
  • 그런 다음 데이터베이스에 제품이 있는지 확인해요.
    • 있으면 제품을 업데이트해요.
    • 없으면 제품을 삽입해요.
  • 그런 다음 쿠키에 북마크(bookmark)를 설정해요.
  • 마지막으로 응답을 반환해요.

최신 데이터로 세션을 시작하고 싶으므로 first-primary 제약 조건을 사용해요. first-unconstrained 제약 조건을 사용하거나 북마크를 전달해도 쓰기 요청은 항상 기본(primary) 데이터베이스로 라우팅돼요.

쿠키에 설정된 북마크는 새 세션이 제공된 북마크만큼 최신인 데이터베이스 버전을 읽도록 보장하는 데 사용할 수 있어요.

외부 플랫폼으로 제품을 관리한다면 이 API를 외부 플랫폼에 연결해서, 외부 플랫폼에서 제품이 생성되거나 업데이트될 때 D1 데이터베이스가 자동으로 제품 세부 정보를 업데이트하도록 할 수 있어요.

2. GET /api/products

app.get("/api/products", async (c) => {
	const db = c.env.DB;

	// Get bookmark from the cookie
	const bookmark = getCookie(c, "product_bookmark") || "first-unconstrained";

	const session = db.withSession(bookmark);

	try {
		return await withRetry(async () => {
			const { results } = await session.prepare("SELECT * FROM products").run();

			const latestBookmark = session.getBookmark();

			// Set the bookmark in the cookie
			latestBookmark &&
				setCookie(c, "product_bookmark", latestBookmark, {
					maxAge: 60 * 60, // 1 hour
				});

			return c.json(results);
		});
	} catch (e) {
		console.error(e);
		return c.json([]);
	}
});

위 코드에서:

  • 쿠키에서 데이터베이스 세션 북마크를 얻어요.
    • 북마크가 설정되지 않았다면 first-unconstrained 제약 조건을 사용해요.
  • 그런 다음 북마크로 데이터베이스 세션을 만들어요.
  • 데이터베이스에서 모든 제품을 가져오고 최신 북마크를 얻어요.
  • 그런 다음 이 북마크를 쿠키에 설정해요.
  • 마지막으로 결과를 반환해요.

3. GET /api/products/:id

app.get("/api/products/:id", async (c) => {
	const id = c.req.param("id");

	if (!id) {
		return c.json({ message: "Invalid id" }, 400);
	}

	const db = c.env.DB;

	// Get bookmark from the cookie
	const bookmark = getCookie(c, "product_bookmark") || "first-unconstrained";

	const session = db.withSession(bookmark);

	try {
		return await withRetry(async () => {
			const { results } = await session
				.prepare("SELECT * FROM products where id = ?")
				.bind(id)
				.run();

			const latestBookmark = session.getBookmark();

			// Set the bookmark in the cookie
			latestBookmark &&
				setCookie(c, "product_bookmark", latestBookmark, {
					maxAge: 60 * 60, // 1 hour
				});

			console.log(results);

			return c.json(results);
		});
	} catch (e) {
		console.error(e);
		return c.json([]);
	}
});

위 코드에서:

  • 요청 매개변수에서 제품 ID를 얻어요.
  • 그런 다음 북마크로 데이터베이스 세션을 만들어요.
  • 데이터베이스에서 제품을 가져오고 최신 북마크를 얻어요.
  • 그런 다음 이 북마크를 쿠키에 설정해요.
  • 마지막으로 결과를 반환해요.

8단계: 애플리케이션 테스트하기

이제 API 라우트를 D1 데이터베이스에 연결하도록 업데이트했어요. 개발 서버를 시작하고 프런트엔드로 이동해서 애플리케이션을 테스트할 수 있어요.

npm run dev

http://localhost:8787 ↗︎로 이동하세요. 제품이 나열된 것을 볼 수 있을 거예요. 제품을 클릭해서 제품 세부 정보를 확인해 보세요.

새 제품을 삽입하려면 다음 명령을 사용하세요 (개발 서버가 실행되는 동안):

curl -X POST http://localhost:8787/api/product \
     -H "Content-Type: application/json" \
     -d '{"id": 6, "name": "Fast Computer", "description": "A computer for your home or office", "price": 1000.00, "inventory": 10, "category": "Electronics"}'

http://localhost:8787/product-details?id=6으로 이동하세요. 새 제품이 보일 거예요.

다음 명령으로 제품을 업데이트하고 http://localhost:8787/product-details?id=6으로 다시 이동하세요. 업데이트된 제품이 보일 거예요.

curl -X POST http://localhost:8787/api/product \
     -H "Content-Type: application/json" \
     -d '{"id": 6, "name": "Fast Computer", "description": "A computer for your home or office", "price": 1050.00, "inventory": 10, "category": "Electronics"}'

참고

읽기 복제는 애플리케이션이 배포됐을 때만 사용돼요. D1은 로컬에서 개발할 때 읽기 복제본을 만들지 않아요.

로컬에서 테스트하려면 D1 바인딩 설정에서 "remote" : true를 설정할 수 있어요. 자세한 내용은 원격 바인딩 문서를 참고하세요.

9단계: 애플리케이션 배포하기

이전 단계에서 사용한 데이터베이스는 로컬이므로, 원격 데이터베이스에 products 테이블을 만들어야 해요. 원격 데이터베이스에 products 테이블을 만들려면 다음 D1 명령을 실행하세요.

npx wrangler d1 execute fast-commerce --remote --command "CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, inventory INTEGER NOT NULL DEFAULT 0, category TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"

다음으로 다음 명령을 실행해 products 테이블에 인덱스를 만드세요.

npx wrangler d1 execute fast-commerce --remote --command "CREATE INDEX IF NOT EXISTS idx_products_id ON products (id)"

필요하다면 다음 명령을 실행해 원격 데이터베이스에 제품을 삽입할 수도 있어요.

npx wrangler d1 execute fast-commerce --remote --command "INSERT INTO products (id, name, description, price, inventory, category) VALUES (1, 'Fast Ergonomic Chair', 'A comfortable chair for your home or office', 100.00, 10, 'Furniture'), (2, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing'), (3, 'Fast Wooden Desk', 'A wooden desk for your home or office', 150.00, 5, 'Furniture'), (4, 'Fast Leather Sofa', 'A leather sofa for your home or office', 300.00, 3, 'Furniture'), (5, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing')"

이제 다음 명령으로 애플리케이션을 배포할 수 있어요.

npm run deploy

이렇게 하면 애플리케이션이 Workers에 배포되고 D1 데이터베이스가 원격 리전들로 복제돼요. 사용자가 어떤 리전에서 애플리케이션을 요청하든, 그 요청은 데이터베이스가 복제된 가장 가까운 리전으로 리디렉션돼요.

결론 (Conclusion)

이 튜토리얼에서는 e-커머스 웹사이트에 D1 읽기 복제를 사용하는 방법을 배웠어요. D1 데이터베이스를 만들고 읽기 복제를 활성화했어요. 그런 다음 데이터베이스에서 제품을 만들고 업데이트하는 API를 만들었어요. 또한 북마크를 사용해 데이터베이스에서 최신 데이터를 가져오는 방법도 배웠어요.

그런 다음 원격 데이터베이스에 products 테이블을 만들고 애플리케이션을 배포했어요.

읽기가 많은 기존 애플리케이션에도 같은 접근 방식을 사용해서 읽기 지연 시간을 줄이고 읽기 처리량을 높일 수 있어요. 콘텐츠를 관리하기 위해 외부 플랫폼을 사용한다면 외부 플랫폼을 D1 데이터베이스에 연결해서 콘텐츠가 데이터베이스에서 자동으로 업데이트되도록 할 수 있어요.

이 튜토리얼의 전체 코드는 GitHub 저장소 ↗︎에서 확인할 수 있어요.

더 알아보기 (Learn more)