Zustand์ ์ฟ ํค๋ฅผ ํ์ฉํ ์์ ํ ๋ก๊ทธ์ธ ์ํ ๊ด๋ฆฌ
โ๏ธ ๋ฌธ์ ์ : ์๋ก ๊ณ ์นจ ์ ๋ก๊ทธ์ธ ์ํ ์ ์ง ์๋จ
ํ์ฌ ๋ก๊ทธ์ธ ํ ๋ฐ์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ Zustand์ userStore์ ์ ์ฅํ์ฌ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ ์์ต๋๋ค.
ํ์ง๋ง ์๋ก ๊ณ ์นจ ์ ์ํ๊ฐ ์ด๊ธฐํ๋์ด ๋ก๊ทธ์ธ ์ํ๊ฐ ์ ์ง๋์ง ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
ํ ํฐ์ด๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ก์ปฌ ์คํ ๋ฆฌ์ง๋ ์ธ์ ์คํ ๋ฆฌ์ง์ ์ ์ฅํ๋ ๊ฒ์ด ํ์ํ ์ง ๊ณ ๋ฏผ์ด ๋์์ต๋๋ค.
๐ ๊ณ ๋ฏผ
- ๋ก๊ทธ์ธ ์ํ ์ ์ง๋ฅผ ์ํด ์ด๋ค ์ ์ฅ ๋ฐฉ์์ด ๊ฐ์ฅ ์ ํฉํ ๊น?
- zustand๋ฅผ ์ฌ์ฉํ ๋ ๋ก์ปฌ ์คํ ๋ฆฌ์ง๋ ์ธ์ ์คํ ๋ฆฌ์ง๋ฅผ ์ถ๊ฐ๋ก ํ์ฉํด์ผ ํ ๊น, ์๋๋ฉด ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ๊ณ ๋ คํด์ผ ํ ๊น?
- ์ฟ ํค๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ์ด๋ป๊ฒ ์์ ํ๊ฒ ํ ํฐ์ ์ ์ฅํ๊ณ ์ฌ์ฉํ ์ ์์๊น?
์ํ ๊ด๋ฆฌ์ ๊ธฐ๋ณธ ๋ฌธ์
โ๐ป ์ฌ๊ธฐ์ ์ ๊น
zustand๋ ๋ฉ๋ชจ๋ฆฌ ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, ํ์ด์ง๊ฐ ์๋ก๊ณ ์นจ๋๋ฉด ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ๋ ๋ชจ๋ ์ํ๊ฐ ์ด๊ธฐํ๋ฉ๋๋ค.
์๋ก๊ณ ์นจ์ ํ๋ฉด ๋ธ๋ผ์ฐ์ ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฒ์๋ถํฐ ๋ค์ ๋ก๋ํ๊ธฐ ๋๋ฌธ์, ๋ฉ๋ชจ๋ฆฌ์ ์๋ ์ํ๋ ์ฌ๋ผ์ง๊ณ ์๋ก ์ด๊ธฐํ๋ฉ๋๋ค.
์ฆ, ์๋ฌด๋ฐ ์ถ๊ฐ ์ค์ ์ ํ์ง ์์ ์ํ์ zustand๋ ํ์ด์ง๊ฐ ์๋ก๊ณ ์นจ๋๋ฉด ์ ์ ์ ๋ณด๊ฐ ์ด๊ธฐํ๋๋ ๊ฒ์ด๋ค.
์๋ก๊ณ ์นจ์๋ ์ด๊ธฐํ๋์ง ์์ผ๋ ค๋ฉด ์ง์์ ์ธ ์ ์ฅ์์ ์ ์ฅํด์ผํ๋๋ฐ Zustand์ persist๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
๐ ํด๊ฒฐ๋ฒ 1 - zustand์ persist ๋ฏธ๋ค์จ์ด๋ฅผ ํ์ฉ
Zustand์ persist ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ๋ฉด ์ํ๋ฅผ localStorage๋ sessionStorage์ ์ ์ฅํ์ฌ ์๋ก ๊ณ ์นจ ํ์๋ ์ ์งํ ์ ์์ต๋๋ค.
ํ์ง๋ง localStorage ๋๋ sessionStorage์ ์ ์ฅ๋ ์ ๋ณด๋ ๋ธ๋ผ์ฐ์ ์์ JavaScript๋ฅผ ํตํด ์ ๊ทผ ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์, XSS(๊ต์ฐจ ์ฌ์ดํธ ์คํฌ๋ฆฝํ ) ๊ณต๊ฒฉ์ ๋ ธ์ถ๋ ์ํ์ด ์๊ณ , accessToken ๊ฐ์ ๋ฏผ๊ฐํ ์ธ์ฆ ์ ๋ณด๋ฅผ ํด๋ผ์ด์ธํธ ์ธก์ ๋ด๋ ๊ฒ์ ๋ณด์์ ์ข์ง ์๋ค๊ณ ํ๋จํ๋ค.
- ๋ณด์ ์ทจ์ฝ์ : localStorage์ sessionStorage๋ ๋ธ๋ผ์ฐ์ ์์ JavaScript๋ก ์ ๊ทผ ๊ฐ๋ฅํ๋ฏ๋ก, XSS(๊ต์ฐจ ์ฌ์ดํธ ์คํฌ๋ฆฝํ ) ๊ณต๊ฒฉ์ ์ทจ์ฝํฉ๋๋ค.
- ๋ฏผ๊ฐํ ์ ๋ณด ๊ด๋ฆฌ: accessToken๊ณผ ๊ฐ์ ๋ฏผ๊ฐํ ์ธ์ฆ ์ ๋ณด๋ฅผ ํด๋ผ์ด์ธํธ ์ธก์ ์ ์ฅํ๋ ๊ฒ์ ๋ณด์์ ์ ์ ํ์ง ์์ต๋๋ค.
๐ ๋ด๊ฐ ์ ํํ ํด๊ฒฐ์ฑ - ์ฟ ํค๋ฅผ ํ์ฉํ ์ํ ๊ด๋ฆฌ
1. Access Token์ ํด๋ผ์ด์ธํธ ์ฟ ํค์ ์ ์ฅ
๋ฐฑ์๋์์ httpOnly ์ฟ ํค๋ฅผ ์ง์ํ์ง ์๊ธฐ ๋๋ฌธ์, ํด๋ผ์ด์ธํธ์์ js-cookie ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ Access Token์ ์ฟ ํค์ ์ ์ฅํ๋ ๋ฐฉ์์ผ๋ก ๋ณด์์ ๊ด๋ฆฌํ์ต๋๋ค.
js-cookie๋ฅผ ์ฌ์ฉํด ์ฟ ํค์ accessToken์ ์ ์ฅํ๋ฉด์ secure์ SameSite ์ค์ ์ ์ฌ์ฉํ๋ฉด, localStorage๋ sessionStorage์ ๋นํด XSS ๋ฐ CSRF ๊ณต๊ฒฉ์ ์ํ์ ์๋นํ ์ค์ผ ์ ์์ต๋๋ค. ๋ฌผ๋ก , httpOnly ์ฟ ํค๋งํผ ๊ฐ๋ ฅํ ๋ณด์์ ์ ๊ณตํ์ง ์์ง๋ง, ๋ฐฑ์๋์์ ์ด๋ฅผ ์ ๊ณตํ์ง ์๋ ์ํฉ์์๋ ์ ํฉํ ๋์์ด๋ผ๊ณ ํ๋จํ์ต๋๋ค.
- httpOnly ์ฟ ํค๋ ํด๋ผ์ด์ธํธ ์คํฌ๋ฆฝํธ์์ ์ ๊ทผํ ์ ์์ด์ ๋ณด์์ ๋งค์ฐ ์ ๋ฆฌํ์ง๋ง, ๋ฐฑ์๋์์ ์ด๋ฅผ ์ ๊ณตํ์ง ์๋ ์ํฉ์์ ํด๋ผ์ด์ธํธ์์๋ js-cookie๋ฅผ ์ฌ์ฉํ์ฌ ํ ํฐ์ ์ฟ ํค์ ์ ์ฅํ๊ฒ ๋์์ต๋๋ค.
- ์ฟ ํค์๋ ๋ณด์์ ๊ฐํํ๊ธฐ ์ํด Secure์ SameSite ์ต์ ์ ์ค์ ํ์ต๋๋ค.
์ฝ๋ ์์
import Cookies from "js-cookie";
// Access Token ์ ์ฅ ํจ์
export const setAccessToken = (token) => {
Cookies.set("accessToken", token, {
expires: 1, // ์ ํจ ๊ธฐ๊ฐ: 1์ผ
secure: true, // HTTPS์์๋ง ์ ์ก
sameSite: "Strict", // CSRF ๊ณต๊ฒฉ ๋ฐฉ์ง
});
};
// Access Token ๊ฐ์ ธ์ค๊ธฐ
export const getAccessToken = () => {
return Cookies.get("accessToken") || null;
};
// Access Token ์ญ์
export const removeAccessToken = () => {
Cookies.remove("accessToken");
};
2. Authorization ํค๋๋ก Access Token ์ ๋ฌ
ํด๋ผ์ด์ธํธ์์ API ์์ฒญ ์ Access Token์ Authorization ํค๋๋ก ์ ๋ฌํฉ๋๋ค. ์ด๋ ์๋ฒ์์ ์ฌ์ฉ์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค.
Axios ์ค์
import Axios from "axios";
import Cookies from "js-cookie";
// Axios ์ธ์คํด์ค ์์ฑ
const axiosInstance = Axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000",
timeout: 5000,
withCredentials: false, // ์๋ฒ์ ์ฟ ํค๋ฅผ ์๋์ผ๋ก ํฌํจํ์ง ์์
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
// ์์ฒญ ์ธํฐ์
ํฐ๋ฅผ ํตํด Authorization ํค๋ ์ถ๊ฐ
axiosInstance.interceptors.request.use(
(config) => {
const token = Cookies.get("accessToken");
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export default axiosInstance;
3. Zustand๋ฅผ ์ฌ์ฉํ ์ฌ์ฉ์ ์ํ ๊ด๋ฆฌ
์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ํ๋ก ๊ด๋ฆฌํ์ฌ ํด๋ผ์ด์ธํธ์ ๋ก๊ทธ์ธ ์ํ๋ฅผ ์ ์งํฉ๋๋ค. ๊ทธ๋ฌ๋ ์ฟ ํค์ Access Token์ ์ ์ฅํ๊ณ zustand๋ ์ฌ์ฉ์ ์ ๋ณด๋ง ์ ์ฅํ๋๋ก ํ์ฌ ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ๋ถ๋ฆฌํฉ๋๋ค.
Zustand Store
import create from "zustand";
export const useUserStore = create((set) => ({
user: null,
isAuthenticated: false,
login: async (loginData) => {
try {
const response = await getLogin(loginData);
const { user, accessToken } = response;
setAccessToken(accessToken); // Access Token ์ฟ ํค ์ ์ฅ
set({ user, isAuthenticated: true });
} catch (error) {
set({ user: null, isAuthenticated: false });
throw error;
}
},
logout: () => {
removeAccessToken(); // Access Token ์ญ์
set({ user: null, isAuthenticated: false });
},
}));
๋์์ง ์
- ์ฟ ํค๋ฅผ ์ฌ์ฉํ์ฌ Access Token์ ๊ด๋ฆฌํจ์ผ๋ก์จ ํด๋ผ์ด์ธํธ ์ ์ฅ์์ ๋ณด์ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ์ต๋๋ค.
- ์ฌ์ฉ์ ์ ๋ณด๋ Zustand๋ก ๊ด๋ฆฌํ์ฌ ์๋ก ๊ณ ์นจ ํ์๋ ๋ก๊ทธ์ธ ์ํ๋ฅผ ์ ์งํ ์ ์๋๋ก ํ์ต๋๋ค.
- ๋ฏผ๊ฐํ ์ธ์ฆ ์ ๋ณด๋ ํด๋ผ์ด์ธํธ ์คํฌ๋ฆฝํธ๋ก ์ ๊ทผํ ์ ์๊ฒ ๋ถ๋ฆฌํ์ฌ ๋ณด์์ฑ์ ๊ฐํํ์ต๋๋ค.
๊ฒฐ๋ก
js-cookie๋ฅผ ์ฌ์ฉํ ๋ฐฉ์์ผ๋ก ๋ก๊ทธ์ธ ์ํ ๊ด๋ฆฌ์ ๋ณด์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์์ต๋๋ค.
๋ฐฑ์๋์์ httpOnly ์ฟ ํค๋ฅผ ์ ๊ณตํ์ง ์๋ ํ๊ฒฝ์์๋ ํด๋ผ์ด์ธํธ ์ธก์์ ํ ํฐ์ ์์ ํ๊ฒ ์ ์ฅํ๊ณ , zustand๋ก ์ํ๋ฅผ ๊ด๋ฆฌํ์ฌ ์๋ก ๊ณ ์นจ ์ ๋ก๊ทธ์ธ ์ํ๋ฅผ ์ ์งํ ์ ์์์ต๋๋ค.
- ์ฟ ํค๋ฅผ ํ์ฉํด Access Token์ ์ ์ฅํ๊ณ , js-cookie๋ก ํด๋ผ์ด์ธํธ์์ ์ง์ ๊ด๋ฆฌ.
- Secure, SameSite ์ต์ ์ผ๋ก ๋ณด์์ฑ์ ๊ฐํ.
- zustand๋ก ์ฌ์ฉ์ ์ํ ๊ด๋ฆฌํ์ฌ ๋ก๊ทธ์ธ ์ํ ์ ์ง.
๊ฒฐ๊ณผ์ ์ผ๋ก, ์ฌ์ฉ์์ ํธ๋ฆฌํจ์ ์ ๊ณตํ๋ฉด์ ๋ณด์์ ํฌ๊ฒ ๊ฐํํ ์ ์๊ฒ๋์๋ค :)