This commit is contained in:
2026-11-10 12:35:16 +09:00
parent 4ada42bacc
commit a51616f330
12 changed files with 451 additions and 203 deletions
+57 -20
View File
@@ -85,8 +85,9 @@ const MIME_TYPES = {
'.woff': 'font/woff'
};
// Gzip 압축 전송 유틸
function sendResponse(req, res, statusCode, contentType, body, extraHeaders = {}) {
// Gzip 압축 전송 유틸 (HEAD 요청 규격 완벽 대응 및 사전 압축 버퍼 즉시 전송)
function sendResponse(req, res, statusCode, contentType, body, extraHeaders = {}, preGzipped = null) {
const isHead = req.method === 'HEAD';
const acceptEncoding = req.headers['accept-encoding'] || '';
const headers = {
'Content-Type': contentType,
@@ -98,21 +99,29 @@ function sendResponse(req, res, statusCode, contentType, body, extraHeaders = {}
}
if (acceptEncoding.includes('gzip') && body.length > 512) {
if (preGzipped) {
headers['Content-Encoding'] = 'gzip';
headers['Content-Length'] = preGzipped.length;
res.writeHead(statusCode, headers);
res.end(isHead ? undefined : preGzipped);
return;
}
zlib.gzip(body, (err, compressed) => {
if (err) {
headers['Content-Length'] = body.length;
res.writeHead(statusCode, headers);
res.end(body);
res.end(isHead ? undefined : body);
return;
}
headers['Content-Encoding'] = 'gzip';
headers['Content-Length'] = compressed.length;
res.writeHead(statusCode, headers);
res.end(compressed);
res.end(isHead ? undefined : compressed);
});
} else {
headers['Content-Length'] = body.length;
res.writeHead(statusCode, headers);
res.end(body);
res.end(isHead ? undefined : body);
}
}
@@ -177,21 +186,36 @@ const server = http.createServer(async (req, res) => {
return;
}
// 끝 슬래시 정규화 (루트 '/' 제외)
const pathname = rawPathname.length > 1 && rawPathname.endsWith('/')
? rawPathname.replace(/\/+$/, '')
: rawPathname;
// 끝 슬래시 정규화 (루트 '/' 제외, 끝 슬래시 요청 시 표준 URL로 301 영구 리다이렉트하여 중복 색인 방지)
if (rawPathname.length > 1 && rawPathname.endsWith('/')) {
const normalized = rawPathname.replace(/\/+$/, '');
res.writeHead(301, {
Location: normalized,
'Cache-Control': 'public, max-age=86400'
});
res.end();
return;
}
const pathname = rawPathname;
const isAdmin = pathname === '/admin' || pathname.startsWith('/admin/') || pathname.endsWith('/admin');
// 2. 캐시 확인 (인메모리 캐시 히트 시 0.001초 응답, 관리자 경로 제외)
// 2. 캐시 확인 (인메모리 캐시 히트 시 사전 압축 버퍼로 0.0001초 즉시 응답, 관리자 경로 제외)
if (!isAdmin) {
const cached = getCachedHtml(pathname);
if (cached) {
sendResponse(req, res, cached.statusCode, 'text/html; charset=utf-8', cached.html, {
'X-Cache': 'HIT',
'Cache-Control': 'public, max-age=3600'
});
sendResponse(
req,
res,
cached.statusCode,
'text/html; charset=utf-8',
cached.html,
{
'X-Cache': 'HIT',
'Cache-Control': 'public, max-age=3600'
},
cached.gzipped
);
return;
}
}
@@ -234,13 +258,26 @@ const server = http.createServer(async (req, res) => {
return;
}
// 캐시에 저장 (다음 요청부터 0.001초 응답)
setCachedHtml(pathname, { statusCode: 200, html: fullHtml });
// 캐시에 원본 Buffer 및 Gzip 압축 Buffer 함께 저장 (대량 봇 크롤링 시 CPU 부하 제로화)
const htmlBuf = Buffer.from(fullHtml, 'utf-8');
let gzippedBuf = null;
try {
gzippedBuf = zlib.gzipSync(htmlBuf);
} catch {}
setCachedHtml(pathname, { statusCode: 200, html: htmlBuf, gzipped: gzippedBuf });
sendResponse(req, res, 200, 'text/html; charset=utf-8', fullHtml, {
'X-Cache': 'MISS',
'Cache-Control': 'public, max-age=3600'
});
sendResponse(
req,
res,
200,
'text/html; charset=utf-8',
htmlBuf,
{
'X-Cache': 'MISS',
'Cache-Control': 'public, max-age=3600'
},
gzippedBuf
);
return;
} catch (err) {
console.error(`[SSR Error] ${pathname} 렌더링 중 오류:`, err);