์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ ์ต์ ํํ๋ ๊ฒ์ ํ๋ ์น ๊ฐ๋ฐ์์ ๊ฐ์ฅ ์ค์ํ ๊ณผ์ ์ค ํ๋์ ๋๋ค. ํนํ Server-Side Rendering(SSR)์ ์ฌ์ฉํ๋ Next.js ์ ํ๋ฆฌ์ผ์ด์ ์์ ์บ์ฑ์ ์ฑ๋ฅ ํฅ์์ ํต์ฌ ์์์ ๋๋ค. ํ์ง๋ง ์บ์ฑ๊ณผ ๋ฐ์ดํฐ ์ ์ ๋ ์ฌ์ด์ ๊ท ํ์ ๋ง์ถ๋ ๊ฒ์ ์ฝ์ง ์์ ์ผ์ ๋๋ค.
์ด ๊ธ์์๋ Next.js์์ ์ ์ฒด ํ์ด์ง ์บ์ฑ์ ๊ตฌํํ๋ฉด์๋ ๋ฐ์ดํฐ์ ์ ์ ๋๋ฅผ ์ ์งํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์ธํ ์์๋ณด๊ฒ ์ต๋๋ค.
์ ์ฒด ํ์ด์ง ์บ์ฑ์ด๋?
์ ์ฒด ํ์ด์ง ์บ์ฑ(Full Page Caching)์ ์๋ฒ์์ ๋ ๋๋ง๋ ์์ ํ HTML ํ์ด์ง๋ฅผ ๋ฉ๋ชจ๋ฆฌ๋ ๋์คํฌ์ ์ ์ฅํ์ฌ, ๋์ผํ ์์ฒญ์ด ๋ค์ด์ฌ ๋ ๋ค์ ๋ ๋๋ง ๊ณผ์ ์ ๊ฑฐ์น์ง ์๊ณ ์บ์๋ ํ์ด์ง๋ฅผ ์ฆ์ ๋ฐํํ๋ ๊ธฐ์ ์ ๋๋ค.
์ ์ฒด ํ์ด์ง ์บ์ฑ์ ์ฅ์
- ๋น ๋ฅธ ์๋ต ์๊ฐ: ์ด๋ฏธ ๋ ๋๋ง๋ ํ์ด์ง๋ฅผ ๋ฐ๋ก ๋ฐํํ๋ฏ๋ก ์๋ต ์๋๊ฐ ํฌ๊ฒ ํฅ์๋ฉ๋๋ค.
- ์๋ฒ ๋ฆฌ์์ค ์ ์ฝ: CPU์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์ค์ผ ์ ์์ต๋๋ค.
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ถํ ๊ฐ์: ๋ฐ๋ณต์ ์ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ๋ฅผ ์ค์ผ ์ ์์ต๋๋ค.
- ํ์ฅ์ฑ ํฅ์: ๋ ๋ง์ ๋์ ์ฌ์ฉ์๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
์ ์ฒด ํ์ด์ง ์บ์ฑ์ ๋จ์
- ๋ฐ์ดํฐ ์ ์ ๋ ๋ฌธ์ : ์บ์๋ ๋ฐ์ดํฐ๊ฐ ์ค๋๋ ์ ์์ต๋๋ค.
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ฆ๊ฐ: ํ์ด์ง๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅํด์ผ ํฉ๋๋ค.
- ๋ณต์กํ ๋ฌดํจํ ๋ก์ง: ์ธ์ ์บ์๋ฅผ ๊ฐฑ์ ํ ์ง ๊ฒฐ์ ํ๊ธฐ ์ด๋ ต์ต๋๋ค.
Next.js์์์ ์บ์ฑ ์ต์
1. Static Generation (SSG)
// pages/products/[id].js
export async function getStaticProps({ params }) {
const product = await fetchProduct(params.id);
return {
props: {
product,
},
// 60์ด๋ง๋ค ์ฌ์์ฑ
revalidate: 60,
};
}
export async function getStaticPaths() {
const products = await fetchAllProducts();
const paths = products.map((product) => ({
params: { id: product.id.toString() },
}));
return {
paths,
fallback: 'blocking',
};
}
2. Incremental Static Regeneration (ISR)
ISR์ SSG์ ์ฅ์ ์ ์ ์งํ๋ฉด์ ๋ฐ์ดํฐ ์ ์ ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ๋๋ค.
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
const post = await fetchBlogPost(params.slug);
return {
props: {
post,
},
// 10๋ถ๋ง๋ค ์ฌ์์ฑ ์๋
revalidate: 600,
};
}
3. Server-Side Rendering with Caching
SSR๊ณผ ์บ์ฑ์ ๊ฒฐํฉํ ๋ฐฉ๋ฒ์ ๋๋ค.
// pages/api/products.js
import { NextApiRequest, NextApiResponse } from 'next';
const cache = new Map();
export default async function handler(req, res) {
const cacheKey = `products_${JSON.stringify(req.query)}`;
// ์บ์ ํ์ธ
if (cache.has(cacheKey)) {
const { data, timestamp } = cache.get(cacheKey);
// 5๋ถ ์ด๋ด์ ์บ์๋ ์ ํจ
if (Date.now() - timestamp < 5 * 60 * 1000) {
res.setHeader('X-Cache', 'HIT');
return res.json(data);
}
}
// ์๋ก์ด ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
const products = await fetchProducts(req.query);
// ์บ์์ ์ ์ฅ
cache.set(cacheKey, {
data: products,
timestamp: Date.now(),
});
res.setHeader('X-Cache', 'MISS');
res.json(products);
}
Redis๋ฅผ ํ์ฉํ ์ ์ฒด ํ์ด์ง ์บ์ฑ
๋ ๊ฐ๋ ฅํ ์บ์ฑ ์๋ฃจ์ ์ ์ํด Redis๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
Redis ์บ์ฑ ๋ฏธ๋ค์จ์ด ๊ตฌํ
// lib/cache.js
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
export class PageCache {
constructor(defaultTTL = 300) { // 5๋ถ ๊ธฐ๋ณธ TTL
this.defaultTTL = defaultTTL;
}
generateKey(req) {
const { pathname, query } = req;
const sortedQuery = Object.keys(query)
.sort()
.reduce((result, key) => {
result[key] = query[key];
return result;
}, {});
return `page:${pathname}:${JSON.stringify(sortedQuery)}`;
}
async get(req) {
const key = this.generateKey(req);
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
return null;
}
async set(req, data, ttl = null) {
const key = this.generateKey(req);
const cacheData = {
html: data,
timestamp: Date.now(),
ttl: ttl || this.defaultTTL,
};
await redis.setex(key, ttl || this.defaultTTL, JSON.stringify(cacheData));
}
async invalidate(pattern) {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
}
}
์บ์ฑ ๋ฏธ๋ค์จ์ด ์ ์ฉ
// pages/products/index.js
import { GetServerSideProps } from 'next';
import { PageCache } from '../../lib/cache';
const cache = new PageCache(600); // 10๋ถ TTL
export default function ProductsPage({ products, fromCache }) {
return (
<div>
<h1>์ํ ๋ชฉ๋ก {fromCache && <span>(์บ์๋จ)</span>}</h1>
<div>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.price}</p>
</div>
))}
</div>
</div>
);
}
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
// ์บ์ ํ์ธ
const cachedData = await cache.get(req);
if (cachedData) {
// ์บ์ ํํธ
res.setHeader('X-Cache', 'HIT');
return {
props: {
products: cachedData.products,
fromCache: true,
},
};
}
// ์บ์ ๋ฏธ์ค - ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
const products = await fetchProducts();
// ์บ์์ ์ ์ฅ
await cache.set(req, { products });
res.setHeader('X-Cache', 'MISS');
return {
props: {
products,
fromCache: false,
},
};
};
์ค๋งํธ ์บ์ ๋ฌดํจํ ์ ๋ต
1. ํ๊ทธ ๊ธฐ๋ฐ ๋ฌดํจํ
// lib/smartCache.js
export class SmartCache extends PageCache {
async setWithTags(req, data, tags = [], ttl = null) {
const key = this.generateKey(req);
const cacheData = {
html: data,
timestamp: Date.now(),
tags,
ttl: ttl || this.defaultTTL,
};
// ๋ฉ์ธ ์บ์ ์ ์ฅ
await redis.setex(key, ttl || this.defaultTTL, JSON.stringify(cacheData));
// ํ๊ทธ๋ณ ํค ๋งคํ ์ ์ฅ
for (const tag of tags) {
await redis.sadd(`tag:${tag}`, key);
await redis.expire(`tag:${tag}`, (ttl || this.defaultTTL) + 60);
}
}
async invalidateByTag(tag) {
const keys = await redis.smembers(`tag:${tag}`);
if (keys.length > 0) {
await redis.del(...keys);
await redis.del(`tag:${tag}`);
}
}
}
2. ์๊ฐ ๊ธฐ๋ฐ ๋ฌดํจํ
// lib/timeBasedCache.js
export class TimeBasedCache extends PageCache {
constructor(defaultTTL = 300) {
super(defaultTTL);
this.scheduleCleanup();
}
scheduleCleanup() {
setInterval(async () => {
await this.cleanupExpired();
}, 60000); // 1๋ถ๋ง๋ค ์คํ
}
async cleanupExpired() {
const pattern = 'page:*';
const keys = await redis.keys(pattern);
for (const key of keys) {
const data = await redis.get(key);
if (data) {
const parsed = JSON.parse(data);
const age = (Date.now() - parsed.timestamp) / 1000;
if (age > parsed.ttl) {
await redis.del(key);
}
}
}
}
}
์กฐ๊ฑด๋ถ ์บ์ฑ ๊ตฌํ
๋ชจ๋ ํ์ด์ง๊ฐ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์บ์ฑ๋์ด์ผ ํ๋ ๊ฒ์ ์๋๋๋ค. ์กฐ๊ฑด๋ถ ์บ์ฑ์ ํตํด ๋ ์ ๊ตํ ์บ์ฑ ์ ๋ต์ ๊ตฌํํ ์ ์์ต๋๋ค.
// lib/conditionalCache.js
export class ConditionalCache extends PageCache {
constructor(options = {}) {
super(options.defaultTTL);
this.rules = options.rules || [];
}
shouldCache(req, res) {
// ๊ธฐ๋ณธ ์กฐ๊ฑด๋ค
if (req.method !== 'GET') return false;
if (req.headers.cookie?.includes('admin=true')) return false;
if (res.statusCode !== 200) return false;
// ์ฌ์ฉ์ ์ ์ ๊ท์น ์ ์ฉ
for (const rule of this.rules) {
if (!rule(req, res)) return false;
}
return true;
}
getTTL(req) {
const { pathname } = req;
// ๊ฒฝ๋ก๋ณ TTL ์ค์
if (pathname.startsWith('/api/')) return 60; // API๋ 1๋ถ
if (pathname.startsWith('/blog/')) return 3600; // ๋ธ๋ก๊ทธ๋ 1์๊ฐ
if (pathname.startsWith('/products/')) return 600; // ์ํ์ 10๋ถ
return this.defaultTTL;
}
async get(req, res) {
if (!this.shouldCache(req, res)) return null;
return super.get(req);
}
async set(req, res, data) {
if (!this.shouldCache(req, res)) return;
const ttl = this.getTTL(req);
return super.set(req, data, ttl);
}
}
์ค์ ์ฌ์ฉ ์์
์ ์ฒด ์บ์ฑ ์์คํ ํตํฉ
// lib/cacheManager.js
import { ConditionalCache } from './conditionalCache';
import { SmartCache } from './smartCache';
export class CacheManager {
constructor() {
this.cache = new ConditionalCache({
defaultTTL: 300,
rules: [
// ์ธ์ฆ๋ ์ฌ์ฉ์์ ๊ฐ์ธ ํ์ด์ง๋ ์บ์ฑํ์ง ์์
(req) => !req.url.includes('/my-account'),
// ๊ฒ์ ์ฟผ๋ฆฌ๊ฐ ์๋ ํ์ด์ง๋ ๋ ์งง์ TTL
(req) => {
if (req.query.search) {
req._customTTL = 60;
}
return true;
},
],
});
}
async middleware(req, res, next) {
// ์บ์ ํ์ธ
const cached = await this.cache.get(req, res);
if (cached) {
res.setHeader('X-Cache', 'HIT');
res.setHeader('X-Cache-Time', new Date(cached.timestamp).toISOString());
return res.send(cached.html);
}
// ์๋ณธ res.send ๋ฉ์๋ ๋ฐฑ์
const originalSend = res.send.bind(res);
// res.send ๋ฉ์๋ ๋ํ
res.send = async (body) => {
// ์บ์์ ์ ์ฅ
if (res.statusCode === 200 && typeof body === 'string') {
const ttl = req._customTTL || this.cache.getTTL(req);
await this.cache.set(req, res, body, ttl);
res.setHeader('X-Cache', 'MISS');
}
return originalSend(body);
};
next();
}
}
Next.js API ๋ฏธ๋ค์จ์ด๋ก ์ ์ฉ
// middleware.js
import { NextResponse } from 'next/server';
import { CacheManager } from './lib/cacheManager';
const cacheManager = new CacheManager();
export async function middleware(request) {
// ์ ์ ํ์ผ์ ๊ฑด๋๋ฐ๊ธฐ
if (request.nextUrl.pathname.startsWith('/_next/')) {
return NextResponse.next();
}
// ์บ์ ํ์ธ
const cached = await cacheManager.cache.get(request);
if (cached) {
const response = new NextResponse(cached.html);
response.headers.set('X-Cache', 'HIT');
response.headers.set('Content-Type', 'text/html');
return response;
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
์ฑ๋ฅ ๋ชจ๋ํฐ๋ง ๋ฐ ์ต์ ํ
์บ์ ์ฑ๋ฅ ๋ฉํธ๋ฆญ
// lib/cacheMetrics.js
export class CacheMetrics {
constructor() {
this.stats = {
hits: 0,
misses: 0,
errors: 0,
totalRequests: 0,
};
}
recordHit() {
this.stats.hits++;
this.stats.totalRequests++;
}
recordMiss() {
this.stats.misses++;
this.stats.totalRequests++;
}
recordError() {
this.stats.errors++;
}
getHitRate() {
if (this.stats.totalRequests === 0) return 0;
return (this.stats.hits / this.stats.totalRequests) * 100;
}
getStats() {
return {
...this.stats,
hitRate: this.getHitRate(),
};
}
reset() {
this.stats = {
hits: 0,
misses: 0,
errors: 0,
totalRequests: 0,
};
}
}
์บ์ ์๋ฐ
// scripts/warmCache.js
import { CacheManager } from '../lib/cacheManager';
const cacheManager = new CacheManager();
const POPULAR_PAGES = [
'/',
'/products',
'/about',
'/blog',
];
async function warmCache() {
console.log('์บ์ ์๋ฐ ์์...');
for (const page of POPULAR_PAGES) {
try {
const response = await fetch(`${process.env.SITE_URL}${page}`);
if (response.ok) {
console.log(`โ
${page} ์บ์ ์๋ฃ`);
} else {
console.log(`โ ${page} ์คํจ: ${response.status}`);
}
} catch (error) {
console.error(`โ ${page} ์ค๋ฅ:`, error.message);
}
}
console.log('์บ์ ์๋ฐ ์๋ฃ!');
}
warmCache();
๋ฒ ์คํธ ํ๋ํฐ์ค
1. ์ ์ ํ TTL ์ค์
- ์ ์ ์ฝํ ์ธ : 1์๊ฐ ์ด์
- ๋์ ์ฝํ ์ธ : 5-15๋ถ
- ์ค์๊ฐ ๋ฐ์ดํฐ: 30์ด-2๋ถ
- API ์๋ต: 1-5๋ถ
2. ์บ์ ํค ์ ๋ต
// ์ข์ ์บ์ ํค ์์
function generateCacheKey(req) {
const { pathname, query } = req;
const { page, sort, filter } = query;
// ์ ๋ ฌ๋ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ง ํฌํจ
const relevantParams = { page, sort, filter };
const queryString = new URLSearchParams(relevantParams).toString();
return `${pathname}${queryString ? `?${queryString}` : ''}`;
}
3. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ
// ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ชจ๋ํฐ๋ง
function monitorMemoryUsage() {
const used = process.memoryUsage();
console.log('๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋:');
for (let key in used) {
console.log(`${key}: ${Math.round(used[key] / 1024 / 1024 * 100) / 100} MB`);
}
}
setInterval(monitorMemoryUsage, 60000); // 1๋ถ๋ง๋ค ์ฒดํฌ
4. ์บ์ ๋ฌดํจํ ์ ๋ต
// ์ด๋ฒคํธ ๊ธฐ๋ฐ ๋ฌดํจํ
import { EventEmitter } from 'events';
class CacheInvalidator extends EventEmitter {
constructor(cache) {
super();
this.cache = cache;
this.setupEventListeners();
}
setupEventListeners() {
this.on('product:updated', (productId) => {
this.cache.invalidate(`page:/products/${productId}:*`);
this.cache.invalidate('page:/products:*');
});
this.on('user:updated', (userId) => {
this.cache.invalidate(`page:/users/${userId}:*`);
});
}
}
App Router์์์ ์บ์ฑ
Next.js 13+ App Router์์๋ ๋ ๊ฐ๋ ฅํ ์บ์ฑ ์ต์ ์ ์ ๊ณตํฉ๋๋ค.
1. ํ์ด์ง ์บ์ฑ with App Router
// app/products/page.tsx
import { unstable_cache } from 'next/cache';
const getCachedProducts = unstable_cache(
async () => {
const products = await fetchProducts();
return products;
},
['products'],
{
revalidate: 3600, // 1์๊ฐ
tags: ['products']
}
);
export default async function ProductsPage() {
const products = await getCachedProducts();
return (
<div>
<h1>์ํ ๋ชฉ๋ก</h1>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.price}</p>
</div>
))}
</div>
);
}
2. ๋ฐ์ดํฐ ์บ์ ๋ฌดํจํ
// app/admin/actions.ts
import { revalidateTag } from 'next/cache';
export async function updateProduct(productId: string, data: any) {
await updateProductInDB(productId, data);
// ํน์ ํ๊ทธ ๋ฌดํจํ
revalidateTag('products');
revalidateTag(`product-${productId}`);
}
3. ๋ผ์ฐํธ ์บ์ ์ค์
// app/api/products/route.ts
export const dynamic = 'force-dynamic'; // ๋๋ 'force-static'
export const revalidate = 3600; // 1์๊ฐ
export async function GET() {
const products = await fetchProducts();
return Response.json(products);
}
๊ณ ๊ธ ์บ์ฑ ํจํด
1. ๊ณ์ธตํ ์บ์ฑ
// lib/hierarchicalCache.js
export class HierarchicalCache {
constructor() {
this.l1Cache = new Map(); // ๋ฉ๋ชจ๋ฆฌ ์บ์
this.l2Cache = redis; // Redis ์บ์
}
async get(key) {
// L1 ์บ์ ํ์ธ
if (this.l1Cache.has(key)) {
const data = this.l1Cache.get(key);
if (this.isValid(data)) {
return data.value;
}
}
// L2 ์บ์ ํ์ธ
const l2Data = await this.l2Cache.get(key);
if (l2Data) {
const parsed = JSON.parse(l2Data);
// L1์ ๋ณต์ฌ
this.l1Cache.set(key, {
value: parsed.value,
timestamp: parsed.timestamp,
ttl: Math.min(300, parsed.ttl), // L1์ ์ต๋ 5๋ถ
});
return parsed.value;
}
return null;
}
async set(key, value, ttl = 3600) {
const data = {
value,
timestamp: Date.now(),
ttl,
};
// L1์ ์ ์ฅ (์งง์ TTL)
this.l1Cache.set(key, {
...data,
ttl: Math.min(300, ttl),
});
// L2์ ์ ์ฅ
await this.l2Cache.setex(key, ttl, JSON.stringify(data));
}
isValid(data) {
return (Date.now() - data.timestamp) / 1000 < data.ttl;
}
}
2. ์ ์ํ ์บ์ฑ
// lib/adaptiveCache.js
export class AdaptiveCache extends PageCache {
constructor() {
super();
this.hitRates = new Map();
this.accessCounts = new Map();
}
async get(key) {
this.incrementAccess(key);
return super.get(key);
}
incrementAccess(key) {
const count = this.accessCounts.get(key) || 0;
this.accessCounts.set(key, count + 1);
}
adaptTTL(key, baseTTL) {
const accessCount = this.accessCounts.get(key) || 0;
const hitRate = this.hitRates.get(key) || 0;
// ์์ฃผ ์ ๊ทผ๋๊ณ ํํธ์จ์ด ๋์ ํ์ด์ง๋ ๋ ์ค๋ ์บ์
if (accessCount > 100 && hitRate > 0.8) {
return baseTTL * 2;
}
// ๊ฑฐ์ ์ ๊ทผ๋์ง ์๋ ํ์ด์ง๋ ์งง๊ฒ ์บ์
if (accessCount < 10) {
return baseTTL / 2;
}
return baseTTL;
}
}
์ค์ ํ๋ก๋์ ๊ณ ๋ ค์ฌํญ
1. ์บ์ ํฌ๊ธฐ ์ ํ
// lib/boundedCache.js
export class BoundedCache extends PageCache {
constructor(maxSize = 1000, defaultTTL = 300) {
super(defaultTTL);
this.maxSize = maxSize;
this.accessOrder = [];
}
async set(key, data, ttl) {
await super.set(key, data, ttl);
// LRU ์ ์ฑ
์ผ๋ก ํฌ๊ธฐ ๊ด๋ฆฌ
this.updateAccessOrder(key);
const currentSize = await this.getSize();
if (currentSize > this.maxSize) {
await this.evictLRU();
}
}
updateAccessOrder(key) {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
this.accessOrder.push(key);
}
async evictLRU() {
const lruKey = this.accessOrder.shift();
if (lruKey) {
await redis.del(lruKey);
}
}
async getSize() {
const keys = await redis.keys('page:*');
return keys.length;
}
}
2. ์บ์ ์์ถ
// lib/compressedCache.js
import { gzip, gunzip } from 'zlib';
import { promisify } from 'util';
const gzipAsync = promisify(gzip);
const gunzipAsync = promisify(gunzip);
export class CompressedCache extends PageCache {
async set(key, data, ttl) {
const compressed = await gzipAsync(JSON.stringify(data));
const cacheData = {
compressed: compressed.toString('base64'),
timestamp: Date.now(),
ttl: ttl || this.defaultTTL,
};
await redis.setex(key, ttl || this.defaultTTL, JSON.stringify(cacheData));
}
async get(key) {
const cached = await redis.get(key);
if (cached) {
const data = JSON.parse(cached);
const compressed = Buffer.from(data.compressed, 'base64');
const decompressed = await gunzipAsync(compressed);
return JSON.parse(decompressed.toString());
}
return null;
}
}
๋ง์น๋ฉฐ
Next.js์์ ์ ์ฒด ํ์ด์ง ์บ์ฑ์ ๊ตฌํํ ๋๋ ์ฑ๋ฅ ํฅ์๊ณผ ๋ฐ์ดํฐ ์ ์ ๋ ์ฌ์ด์ ๊ท ํ์ ์ ๋ง์ถ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
- ์ ์ ํ ์บ์ฑ ์ ๋ต ์ ํ: ISR, SSR+์บ์ฑ, ์กฐ๊ฑด๋ถ ์บ์ฑ ๋ฑ
- ์ค๋งํธํ ๋ฌดํจํ ๋ก์ง: ํ๊ทธ ๊ธฐ๋ฐ, ์๊ฐ ๊ธฐ๋ฐ ๋ฌดํจํ
- ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง: ์บ์ ํํธ์จ, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ถ์
- ๋ฒ ์คํธ ํ๋ํฐ์ค ์ ์ฉ: TTL ์ค์ , ์บ์ ํค ์ ๋ต, ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ
์ด๋ฌํ ์์๋ค์ ์ข ํฉ์ ์ผ๋ก ๊ณ ๋ คํ์ฌ ๊ตฌํํ๋ฉด, ์ฌ์ฉ์ ๊ฒฝํ์ ํฌ๊ฒ ๊ฐ์ ํ๋ฉด์๋ ์๋ฒ ๋ฆฌ์์ค๋ฅผ ํจ์จ์ ์ผ๋ก ์ฌ์ฉํ ์ ์๋ ๊ณ ์ฑ๋ฅ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค ์ ์์ต๋๋ค.
์บ์ฑ์ ๊ฐ๋ ฅํ ์ต์ ํ ๋๊ตฌ์ด์ง๋ง, ์๋ชป ๊ตฌํํ๋ฉด ์คํ๋ ค ๋ฌธ์ ๊ฐ ๋ ์ ์๊ธฐ ๋๋ฌธ์ ์ถฉ๋ถํ ํ ์คํธ์ ๋ชจ๋ํฐ๋ง์ ํตํด ์ ์ง์ ์ผ๋ก ๊ฐ์ ํด๋๊ฐ๋ ๊ฒ์ด ์ข์ต๋๋ค.
ํ๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์บ์ฑ์ ์ ํ์ด ์๋ ํ์ ์ด๊ธฐ์ Next.js์ ๊ฐ๋ ฅํ ์บ์ฑ ๊ธฐ๋ฅ๋ค์ ์ ์ ํ ํ์ฉํ์ฌ ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.