๐ŸŒฑ <groot/>
SPA ์ƒˆ๋กœ๊ณ ์นจ์—๋„ ํ„ฐ๋„์ด ์•ˆ ํ’€๋ฆฌ๊ฒŒ โ€” URLยท์ฟ ํ‚คยท๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋กœ ์„ธ์…˜ ์ถ”์ ํ•˜๊ธฐ | Groot's Tech Blog
๐ŸŒWeb

SPA ์ƒˆ๋กœ๊ณ ์นจ์—๋„ ํ„ฐ๋„์ด ์•ˆ ํ’€๋ฆฌ๊ฒŒ โ€” URLยท์ฟ ํ‚คยท๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋กœ ์„ธ์…˜ ์ถ”์ ํ•˜๊ธฐ

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

ํ„ฐ๋„์—์„œ ์€๊ทผํžˆ ๊ณจ์น˜ ์•„ํŒ ๋˜ ๋ฌธ์ œ. ๊ณต๊ฐœ URL์€ https://server/abc12345/page ์ฒ˜๋Ÿผ ์•ž์— ํ„ฐ๋„ ID(abc12345) ๊ฐ€ ๋ถ™๋Š”๋‹ค. ์„œ๋ฒ„๋Š” ์ด ID๋กœ "์–ด๋А ๋กœ์ปฌ๋กœ ๋ณด๋‚ผ์ง€"๋ฅผ ์•ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐโ€”

  • ํŽ˜์ด์ง€ ์•ˆ์˜ ๋ฆฌ์†Œ์Šค๋Š” /main.js, /logo.png์ฒ˜๋Ÿผ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋กœ ์š”์ฒญ๋œ๋‹ค. ์—ฌ๊ธฐ์—” ํ„ฐ๋„ ID๊ฐ€ ์—†๋‹ค. ์–ด๋А ๋กœ์ปฌ๋กœ ๋ณด๋‚ด์•ผ ํ•˜์ง€?
  • React Router ๊ฐ™์€ SPA์—์„œ /abc12345/about์„ ๋ณด๋‹ค๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €๋Š” /about์œผ๋กœ ์š”์ฒญํ•œ๋‹ค. ์—ญ์‹œ ID๊ฐ€ ์—†๋‹ค.

์ฆ‰ "ํ„ฐ๋„ ID๋ฅผ URL์— ๊ณ„์† ๋ถ™์ด๊ณ  ๋‹ค๋‹ ์ˆ˜ ์—†๋‹ค"๋Š” ๊ฒŒ ๋ฌธ์ œ์˜€๋‹ค.

ํ•ด๊ฒฐ: URL๋กœ ํ•œ ๋ฒˆ ์‹ฌ๊ณ  โ†’ ์ฟ ํ‚ค๋กœ ๊ธฐ์–ต โ†’ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋กœ ์ง€์šด๋‹ค

ํ•ต์‹ฌ ์•„์ด๋””์–ด๋Š” ํ„ฐ๋„ ID๋ฅผ URL์—์„œ ์ฟ ํ‚ค๋กœ ์˜ฎ๊ธฐ๋Š” ๊ฒƒ์ด๋‹ค.

1๋‹จ๊ณ„. URL ๋งจ ์•ž์— ํ„ฐ๋„ ID๊ฐ€ ์žˆ์œผ๋ฉด, ๊ทธ๊ฑธ ์ฟ ํ‚ค์— ์ €์žฅํ•˜๊ณ  ID๋ฅผ ๋—€ ๊ฒฝ๋กœ๋กœ 302 ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•œ๋‹ค.

const pathMatch = req.path.match(/^\/([a-f0-9]{8})(\/.*)?$/);
if (pathMatch) {
  const tunnelId = pathMatch[1];
  const pathWithoutTunnelId = pathMatch[2] || "/";

  // ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์€ ๋ฒ„๋ฆฌ์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ๋ณด์กดํ•œ๋‹ค
  const queryString = req.url.includes("?")
    ? req.url.substring(req.url.indexOf("?"))
    : "";
  const fullPath = pathWithoutTunnelId + queryString;

  res.cookie("tunnelId", tunnelId, {
    path: "/",
    sameSite: isProduction ? "none" : "lax",
    secure: isProduction,          // ํ”„๋กœ๋•์…˜์€ HTTPS์—์„œ๋งŒ
    maxAge: 24 * 60 * 60 * 1000,   // 24์‹œ๊ฐ„
  });

  return res.redirect(302, fullPath); // /abc12345/about?tab=1 โ†’ /about?tab=1
}

์ด์ œ ๋ธŒ๋ผ์šฐ์ € ์ฃผ์†Œ์ฐฝ์€ ๊น”๋”ํ•œ /about์ด ๋˜๊ณ , ๋ธŒ๋ผ์šฐ์ €๋Š” tunnelId ์ฟ ํ‚ค๋ฅผ ๊ฐ–๊ฒŒ ๋œ๋‹ค.

2๋‹จ๊ณ„. ์ดํ›„ ๋ชจ๋“  ์š”์ฒญ(/main.js, /api/data, ์ƒˆ๋กœ๊ณ ์นจํ•œ /about)์€ ์ฟ ํ‚ค๋ฅผ ๋‹ฌ๊ณ  ์˜จ๋‹ค. ์„œ๋ฒ„๋Š” ์ฟ ํ‚ค์—์„œ ํ„ฐ๋„ ID๋ฅผ ์ฝ์–ด ํ•ด๋‹น ๋กœ์ปฌ๋กœ ๋ณด๋‚ธ๋‹ค.

else if (req.cookies.tunnelId) {
  tunnelId = req.cookies.tunnelId;
  fullPath = req.url; // ๊ฒฝ๋กœ ๊ทธ๋Œ€๋กœ ๋กœ์ปฌ๋กœ ์ „๋‹ฌ
}

3๋‹จ๊ณ„(ํด๋ฐฑ). ์ฟ ํ‚ค๊ฐ€ ์•ˆ ์‹ค๋ ค ์˜ค๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๋‹ค(์ผ๋ถ€ ๋ฆฌ์†Œ์Šค, ์ดˆ๊ธฐ ๋กœ๋“œ ํƒ€์ด๋ฐ). ๋จผ์ € query parameter(?tunnelId=...)๋ฅผ ํ™•์ธํ•˜๊ณ , ์žˆ์œผ๋ฉด ๊ทธ๊ฑธ ์“ฐ๋ฉด์„œ ์ฟ ํ‚ค๋„ ๋‹ค์‹œ ์‹ฌ์–ด ๋‹ค์Œ ์š”์ฒญ๋ถ€ํ„ฐ๋Š” ์ฟ ํ‚ค๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.

else if (req.query.tunnelId) {
  tunnelId = req.query.tunnelId;
  res.cookie("tunnelId", tunnelId, cookieOptions); // ๋‹ค์Œ๋ถ€ํ„ด ์ฟ ํ‚ค๋กœ
}

4๋‹จ๊ณ„(์ตœํ›„ ํด๋ฐฑ). ๊ทธ๊ฒƒ๋„ ์—†์œผ๋ฉด Referer ํ—ค๋”์—์„œ ID๋ฅผ ๊ธ์–ด๋‚ธ๋‹ค. manifest.json์ฒ˜๋Ÿผ ์ฟ ํ‚ค๊ฐ€ ์ „๋‹ฌ๋˜์ง€ ์•Š๋Š” ์š”์ฒญ์„ ์œ„ํ•ด์„œ๋‹ค.

else if (req.headers.referer) {
  const m = req.headers.referer.match(/\/([a-f0-9]{8})(\/|$)/);
  if (m) tunnelId = m[1];
}

URL โ†’ ์ฟ ํ‚ค โ†’ query parameter โ†’ Referer, ๋„ค ๋‹จ๊ณ„ ํด๋ฐฑ์œผ๋กœ ์–ด๋–ค ์š”์ฒญ์ด ์™€๋„ ํ„ฐ๋„ ID๋ฅผ ๋ณต์›ํ•œ๋‹ค. SPA๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„, ์ด๋ฏธ์ง€๊ฐ€ ์ ˆ๋Œ€๊ฒฝ๋กœ๋กœ ์š”์ฒญ๋ผ๋„, ํ„ฐ๋„์ด ์•ˆ ํ’€๋ฆฐ๋‹ค.

SameSite, ์ •ํ™•ํžˆ ๋”ฐ์ ธ๋ณด๋ฉด

์ฒ˜์Œ์—” "๋‹ค๋ฅธ ๋„๋ฉ”์ธ์ด๋‹ˆ ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ๊ฒ ์ง€" ํ•˜๊ณ  ๋„˜๊ฒผ๋Š”๋ฐ, ๋”ฐ์ ธ๋ณด๋ฉด ์•„๋‹ˆ๋‹ค. ๋ฐฉ๋ฌธ์ž๋Š” ํ„ฐ๋„ ๋„๋ฉ”์ธ์— ์ง์ ‘ ์ ‘์†ํ•˜๊ณ (ํผ์ŠคํŠธ ํŒŒํ‹ฐ), ์ฃผ์ž… ์Šคํฌ๋ฆฝํŠธ์˜ fetch /log๋„ ๊ฐ™์€ ์˜ค๋ฆฌ์ง„์œผ๋กœ ๋‚˜๊ฐ„๋‹ค. ์ฆ‰ same-site ์ƒํ™ฉ์ด๋ผ ์›์น™์ ์œผ๋ก  SameSite=Lax๋กœ๋„ ์ถฉ๋ถ„ํžˆ ๋™์ž‘ํ•œ๋‹ค.

  • ์‹ค์ œ ์ฝ”๋“œ๋Š” sameSite: isProduction ? "none" : "lax" โ€” ๋กœ์ปฌ์€ Lax, ํ”„๋กœ๋•์…˜์€ ๋ฐฉ์–ด์ ์œผ๋กœ None + Secure๋ฅผ ๋’€๋‹ค. ๋‚˜์ค‘์— ํŽ˜์ด์ง€๊ฐ€ iframe์— ์ž„๋ฒ ๋“œ๋˜๋Š” ๋“ฑ ์ง„์งœ ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ๊ฐ€ ๋  ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ์—ฌ์œ ๋ถ„์ด์ง€, ์ง€๊ธˆ ๊ตฌ์กฐ์—์„œ ํ•„์ˆ˜๋Š” ์•„๋‹ˆ๋‹ค. (SameSite=None์€ Secure๊ฐ€ ๊ฐ•์ œ๋ผ ํ”„๋กœ๋•์…˜์€ HTTPS ์ „์ œ.)
  • CORS ์‘๋‹ต์— Access-Control-Allow-Credentials: true โ€” ์ž๊ฒฉ์ฆ๋ช…(์ฟ ํ‚ค) ๋™๋ฐ˜ ์š”์ฒญ์„ ํ—ˆ์šฉ.

"์™œ ์ฟ ํ‚ค๊ฐ€ ์•ˆ ์‹ฌ๊ธฐ์ง€?"๋กœ ํ•œ์ฐธ ํ—ค๋งค๋‹ค๊ฐ€, ํผ์ŠคํŠธ ํŒŒํ‹ฐ/์„œ๋“œ ํŒŒํ‹ฐ์™€ SameSite์˜ ์˜๋ฏธ๋ฅผ ์ •ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜๊ฒŒ ๋œ ๋ถ€๋ถ„์ด๋‹ค.

๋ฐฐ์šด ๊ฒƒ

  • ์ƒํƒœ๋ฅผ URL์— ๊ณ„์† ๋“ค๊ณ  ๋‹ค๋‹ ์ˆ˜ ์—†์„ ๋•Œ, ์ฟ ํ‚ค๋กœ ์˜ฎ๊ธฐ๊ณ  URL์€ ๊นจ๋—ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฑด ํ”ํ•œ ์›น ํŒจํ„ด์ด๋‹ค. (302 ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๊ฐ€ ๊ทธ ์ด์‚ฌ ํŠธ๋Ÿญ ์—ญํ• .)

  • ํ•˜๋‚˜์˜ ์†Œ์Šค๋งŒ ๋ฏฟ์ง€ ๋ง๊ณ  URL โ†’ ์ฟ ํ‚ค โ†’ query parameter โ†’ Referer ์ฒ˜๋Ÿผ ํด๋ฐฑ์„ ์Œ“์œผ๋ฉด ๊ฒฌ๊ณ ํ•ด์ง„๋‹ค.

  • ์ฟ ํ‚ค์˜ SameSite๋Š” ๋„๋ฉ”์ธ์ด ์•„๋‹ˆ๋ผ ํผ์ŠคํŠธ ํŒŒํ‹ฐ/์„œ๋“œ ํŒŒํ‹ฐ ๋ฌธ๋งฅ์œผ๋กœ ํŒ๋‹จํ•ด์•ผ ํ•œ๋‹ค. ๋ธŒ๋ผ์šฐ์ € ๋ณด์•ˆ ๋ชจ๋ธ์„ ๋Œ€์ถฉ ์•Œ๋ฉด ์—ฌ๊ธฐ์„œ ๋ง‰ํžŒ๋‹ค.

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

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