์ด๋ฏธ์งยทํฐํธ๋ฅผ ํ ์คํธ ์ฑ๋๋ก ๋๋ฅด๊ธฐ โ ๋ฐ์ด๋๋ฆฌ๋ฅผ WebSocket์ผ๋ก ํฐ๋๋งํ๊ธฐ
ํฐ๋์ ์ฒ์ ๋ง๋ค์ด ํ ์คํธ ํ์ด์ง๋ฅผ ๋์ ์ ๋ ์ ๋๋ค. ๊ทธ๋ฐ๋ฐ ์ด๋ฏธ์ง๊ฐ ๊นจ์ง๊ณ , ํฐํธ๊ฐ ์ ๋์ค๊ณ , 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