๐ŸŒฑ <groot/>
์ด๋ฏธ์ง€ยทํฐํŠธ๋ฅผ ํ…์ŠคํŠธ ์ฑ„๋„๋กœ ๋‚˜๋ฅด๊ธฐ โ€” ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ WebSocket์œผ๋กœ ํ„ฐ๋„๋งํ•˜๊ธฐ | Groot's Tech Blog
๐ŸŒWeb

์ด๋ฏธ์ง€ยทํฐํŠธ๋ฅผ ํ…์ŠคํŠธ ์ฑ„๋„๋กœ ๋‚˜๋ฅด๊ธฐ โ€” ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ WebSocket์œผ๋กœ ํ„ฐ๋„๋งํ•˜๊ธฐ

2026.07.02ยท3๋ถ„ ์ฝ๊ธฐ

ํ„ฐ๋„์„ ์ฒ˜์Œ ๋งŒ๋“ค์–ด ํ…์ŠคํŠธ ํŽ˜์ด์ง€๋ฅผ ๋„์› ์„ ๋• ์ž˜ ๋๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ด๋ฏธ์ง€๊ฐ€ ๊นจ์ง€๊ณ , ํฐํŠธ๊ฐ€ ์•ˆ ๋‚˜์˜ค๊ณ , CSS๊ฐ€ ์ด์ƒํ•˜๊ฒŒ ์ ์šฉ๋๋‹ค. ์›์ธ์€ ํ•˜๋‚˜์˜€๋‹ค. ๋‚˜๋Š” ์‘๋‹ต์„ ์ „๋ถ€ "๋ฌธ์ž์—ด"๋กœ ๋‹ค๋ฃจ๊ณ  ์žˆ์—ˆ๋‹ค.

๋ฌธ์ œ: WebSocket ๋ฉ”์‹œ์ง€์— ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ๊ทธ๋ƒฅ ์‹ค์œผ๋ฉด ๊นจ์ง„๋‹ค

์›น ํŽ˜์ด์ง€ ํ•˜๋‚˜์—” ํ…์ŠคํŠธ(HTML/CSS/JS)๋งŒ ์žˆ๋Š” ๊ฒŒ ์•„๋‹ˆ๋‹ค. PNG, WOFF ํฐํŠธ, PDF ๊ฐ™์€ ๋ฐ”์ด๋„ˆ๋ฆฌ๊ฐ€ ์ž”๋œฉ์ด๋‹ค. ์ด๊ฑธ ๋ฌธ์ž์—ด๋กœ ์ทจ๊ธ‰ํ•ด toString() ํ•˜๋ฉด ๋ฐ”์ดํŠธ๊ฐ€ ๊นจ์ง„๋‹ค. ๋‚˜๋Š” JSON WebSocket ๋ฉ”์‹œ์ง€๋กœ ์‘๋‹ต์„ ๋‚˜๋ฅด๋Š”๋ฐ, JSON์€ ํ…์ŠคํŠธ๋ผ ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ๊ทธ๋Œ€๋กœ ๋‹ด์„ ์ˆ˜ ์—†๋‹ค.

ํ•ด๊ฒฐ 1: ์ „๋ถ€ ๋ฐ”์ด๋„ˆ๋ฆฌ๋กœ ๋ฐ›์•„์„œ, Base64๋กœ ํฌ์žฅ

๋จผ์ € ๋กœ์ปฌ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์‘๋‹ต์„ ๋ฐ›์„ ๋•Œ ๋ฌด์กฐ๊ฑด arraybuffer(๋ฐ”์ด๋„ˆ๋ฆฌ)๋กœ ๋ฐ›๋Š”๋‹ค. ํ…์ŠคํŠธ๋“  ์ด๋ฏธ์ง€๋“  ์ผ๋‹จ ๋ฐ”์ดํŠธ๋กœ ๋‹ค๋ฃฌ๋‹ค.

const response = await axios({
  url: `${protocol}://localhost:${localPort}${url}`,
  responseType: "arraybuffer", // ์ด๋ฏธ์ง€ยทํฐํŠธยทํ…์ŠคํŠธ ์ „๋ถ€ ๋ฐ”์ด๋„ˆ๋ฆฌ๋กœ
  decompress: true,            // gzip์€ ์ž๋™ ํ•ด์ œ
  validateStatus: () => true,  // 4xxยท5xx๋„ ๊ทธ๋Œ€๋กœ ํ†ต๊ณผ
});

๊ทธ๋ฆฌ๊ณ  ๋ฐ”์ด๋„ˆ๋ฆฌ์ธ ๊ฒƒ๋งŒ Base64๋กœ ์ธ์ฝ”๋”ฉํ•ด์„œ ํ…์ŠคํŠธ JSON์— ์‹ค์„ ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ ๋‹ค. ์–ด๋А ์ชฝ์ธ์ง€ isBase64 ํ”Œ๋ž˜๊ทธ๋กœ ์•Œ๋ฆฐ๋‹ค.

const isBinary =
  ct.includes("image/") || ct.includes("video/") ||
  ct.includes("font/")  || ct.includes("application/pdf");

let responseBody, isBase64 = false;
if (isBinary || isSvg) {
  responseBody = Buffer.from(response.data).toString("base64"); // ๋ฐ”์ด๋„ˆ๋ฆฌ โ†’ Base64
  isBase64 = true;
} else {
  responseBody = Buffer.from(response.data).toString("utf8");   // ํ…์ŠคํŠธ๋Š” ๊ทธ๋Œ€๋กœ
}

ws.send(JSON.stringify({ type: "response", requestId, statusCode,
                         headers, body: responseBody, isBase64 }));

์„œ๋ฒ„๋Š” ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ณด๊ณ  ๋˜๋Œ๋ฆฐ๋‹ค. Base64๋ฉด ๋‹ค์‹œ ๋ฐ”์ด๋„ˆ๋ฆฌ๋กœ ๋””์ฝ”๋”ฉํ•ด์„œ ์‘๋‹ตํ•œ๋‹ค.

pending.res.end(isBase64 ? Buffer.from(body, "base64") : body);

Base64๋Š” ์šฉ๋Ÿ‰์ด 33% ๋Š˜์ง€๋งŒ, JSON/WebSocket์ด๋ผ๋Š” ํ…์ŠคํŠธ ํ†ต๋กœ๋กœ ์•„๋ฌด ๋ฐ”์ดํŠธ๋‚˜ ์•ˆ์ „ํ•˜๊ฒŒ ๋‚˜๋ฅผ ์ˆ˜ ์žˆ๋‹ค. ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋ฅผ ๋ฐ›์•„๋“ค์ธ ์…ˆ์ด๋‹ค.

ํ•ด๊ฒฐ 2: ๋นŒ๋“œ ๋„๊ตฌ๊ฐ€ ๊ฑฐ์ง“๋งํ•˜๋Š” Content-Type ๊ต์ •

์—ฌ๊ธฐ์„œ ์ง„์งœ ์‚ฝ์งˆ์ด ์‹œ์ž‘๋๋‹ค. ๋กœ์ปฌ ๊ฐœ๋ฐœ ์„œ๋ฒ„(ํŠนํžˆ Vite)๊ฐ€ ์—‰๋šฑํ•œ Content-Type์„ ์ฃผ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์—ˆ๋‹ค. SVG๋ฅผ text/html๋กœ ์ค€๋‹ค๋“ ์ง€, ?importยท?url ์ฟผ๋ฆฌ๊ฐ€ ๋ถ™์€ ๋ชจ๋“ˆ์„ ์ด์ƒํ•˜๊ฒŒ ์ค€๋‹ค๋“ ์ง€. ๊ทธ๋Œ€๋กœ ๋‚˜๋ฅด๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํŒŒ์ผ ์ข…๋ฅ˜๋ฅผ ์˜คํ•ดํ•ด ๊นจ์ง„๋‹ค.

๊ทธ๋ž˜์„œ ํŒŒ์ผ ํ™•์žฅ์ž์™€ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๊ณ  Content-Type์„ ๊ฐ•์ œ๋กœ ๋ฐ”๋กœ์žก์•˜๋‹ค.

let ct = response.headers["content-type"] || "";

// Vite๊ฐ€ ๋ณ€ํ™˜ํ•˜๋Š” ํŠน์ˆ˜ ์ฟผ๋ฆฌ(?import, ?url, ?raw ๋“ฑ)๋Š” JS๋กœ ์ทจ๊ธ‰
if (query && /(import|url|raw|react)/.test(query)) ct = "application/javascript";

// ํ™•์žฅ์ž๋กœ ๊ฐ•์ œ ๊ต์ •
if (urlPath.endsWith(".svg")) ct = "image/svg+xml";
else if (urlPath.endsWith(".png")) ct = "image/png";
else if (urlPath.endsWith(".woff2")) ct = "font/woff2";

ํ•ด๊ฒฐ 3: ์ค‘๊ณ„ํ•˜๋ฉด ์•ˆ ๋˜๋Š” ํ—ค๋” ๊ฑธ๋Ÿฌ๋‚ด๊ธฐ

์‘๋‹ต ํ—ค๋”๋ฅผ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌํ•˜๋ฉด ์˜คํžˆ๋ ค ์‚ฌ๊ณ ๊ฐ€ ๋‚œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด:

  • content-encoding: gzip โ€” ์ด๋ฏธ decompress๋กœ ํ’€์—ˆ๋Š”๋ฐ ์ด ํ—ค๋”๊ฐ€ ๋‚จ์•„์žˆ์œผ๋ฉด ๋ธŒ๋ผ์šฐ์ €๊ฐ€ "์••์ถ•๋ผ ์žˆ๋„ค" ํ•˜๊ณ  ๋˜ ํ’€๋ ค๋‹ค ๊นจ์ง„๋‹ค.
  • content-length โ€” ์ธ์ฝ”๋”ฉํ•˜๋ฉฐ ๊ธธ์ด๊ฐ€ ๋ฐ”๋€Œ์—ˆ์œผ๋‹ˆ ํ‹€๋ฆฐ ๊ฐ’์ด๋‹ค.
  • content-security-policy, x-frame-options, strict-transport-security โ€” ํ„ฐ๋„ ๋„๋ฉ”์ธ์—์„œ ํŽ˜์ด์ง€๊ฐ€ ์•ˆ ๋œจ๊ฒŒ ๋ง‰์„ ์ˆ˜ ์žˆ๋‹ค.
delete cleanResponseHeaders["content-encoding"];  // ์ด๋ฏธ ํ’€์—ˆ์Œ
delete cleanResponseHeaders["content-length"];     // ๊ธธ์ด ๋ฐ”๋€œ
delete cleanResponseHeaders["transfer-encoding"];
delete cleanResponseHeaders["content-security-policy"];
delete cleanResponseHeaders["x-frame-options"];

๋ฆฌ๋‹ค์ด๋ ‰ํŠธ(Location) ํ—ค๋”์˜ https://localhost๋„ ์†๋ด์•ผ ํ–ˆ๋‹ค. ๋‹ค๋งŒ ์ง€๊ธˆ ๊ตฌํ˜„์€ http://localhost:8080์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ ์น˜ํ™˜์ด๋ผ ๋กœ์ปฌ ๊ฐœ๋ฐœ ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ์ค€์ด๋‹ค. ๋กœ์ปฌ์—์„œ ํ…Œ์ŠคํŠธํ•  ๋• ๋™์ž‘ํ•˜์ง€๋งŒ, ํ”„๋กœ๋•์…˜์—์„œ ์™ธ๋ถ€ ๋ฐฉ๋ฌธ์ž๊ฐ€ ๋ฐ›์œผ๋ฉด ์ž๊ธฐ ์ปดํ“จํ„ฐ์˜ localhost๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ผ ๊นจ์ง„๋‹ค. ์ œ๋Œ€๋กœ ํ•˜๋ ค๋ฉด ํ„ฐ๋„์˜ ๊ณต๊ฐœ URL๋กœ ์น˜ํ™˜ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์•„์ง ๋‚จ๊ฒจ๋‘” ํ•œ๊ณ„๋‹ค.

๋ฐฐ์šด ๊ฒƒ

  • ํ…์ŠคํŠธ ํ†ต๋กœ๋กœ ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ๋‚˜๋ฅผ ๋• Base64๊ฐ€ ์ •์„์ด๋‹ค. ์šฉ๋Ÿ‰ 33% ์ฆ๊ฐ€๋Š” ๋Œ€๊ฐ€๋กœ ๋ฐ›์•„๋“ค์ธ๋‹ค.

  • Content-Type์„ ๋‚จ์˜ ์„œ๋ฒ„๊ฐ€ ์ •ํ™•ํžˆ ์ค€๋‹ค๊ณ  ๋ฏฟ์œผ๋ฉด ์•ˆ ๋œ๋‹ค. ํŠนํžˆ ๊ฐœ๋ฐœ ์„œ๋ฒ„๋Š” ์ƒํ™ฉ์— ๋”ฐ๋ผ ๊ฑฐ์ง“๋ง์„ ํ•œ๋‹ค. ํ™•์žฅ์žยท์ฟผ๋ฆฌ๋กœ ๋ณด์ •ํ•˜๋Š” ์•ˆ์ „์žฅ์น˜๊ฐ€ ํ•„์š”ํ–ˆ๋‹ค.

  • ํ”„๋ก์‹œ/์ค‘๊ณ„๋ฅผ ๋งŒ๋“ค ๋• "๊ทธ๋Œ€๋กœ ์ „๋‹ฌํ•˜๋ฉด ์•ˆ ๋˜๋Š” ํ—ค๋”" ๊ฐ€ ๋ฐ˜๋“œ์‹œ ์žˆ๋‹ค. ์••์ถ•ยท๊ธธ์ดยท๋ณด์•ˆ ํ—ค๋”๋Š” ์ค‘๊ณ„ ๊ณ„์ธต์—์„œ ์†๋ด์•ผ ํ•œ๋‹ค. HTTP๋ฅผ ๊ฒ‰ํ•ฅ๊ธฐ๋กœ ์•Œ ๋•Œ๋Š” ์•ˆ ๋ณด์ด๋˜ ๋””ํ…Œ์ผ์ด์—ˆ๋‹ค.

  • ๊ด€๋ จ ํ”„๋กœ์ ํŠธ: Custom Tunnel

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ
ยฉ 2026 ยท Groot's Tech Blog. Built with Gatsby ๐ŸŒฑ