SPA ์๋ก๊ณ ์นจ์๋ ํฐ๋์ด ์ ํ๋ฆฌ๊ฒ โ URLยท์ฟ ํคยท๋ฆฌ๋ค์ด๋ ํธ๋ก ์ธ์ ์ถ์ ํ๊ธฐ
ํฐ๋์์ ์๊ทผํ ๊ณจ์น ์ํ ๋ ๋ฌธ์ .
๊ณต๊ฐ 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