
๊ฐ๋ฐ ํ๊ฒฝ
- Next.js 14 (page router)
- TailwindCSS
- Axios
๋ฌธ์ ์ํฉ
BE์์ API๋ฅผ ํตํด JWT์ ๋ณด๋ด์ฃผ์ด AccessToken์ ํตํด ์ฌ์ฉ์ ์ธ์ฆ์ ํ๋ ๋ฐฉ์์ ์ฌ์ฉํ๊ณ ์์ผ๋ฉฐ, ์ฝ๋์ ๋ถํธ์บ ํ ๋ด์์ ํ๋ ํ๋ก์ ํธ์ด๊ธฐ์ BE API๋ฅผ ๋ณ๊ฒฝํ ์ ์์๊ณ , RefreshToken ์์ด AccessToken๋ง์ ์ฌ์ฉํด ์ธ์ฆ์ ํ๊ณ , AccessToken์ ์ฃผ๊ธฐ๊ฐ ์งง์ ์์ฃผ ๋ก๊ทธ์ธ์ ํด์ผํ์ต๋๋ค.
๋ํ, ํ์ฌ ์น์คํ ๋ฆฌ์ง(LocalStorage)๋ฅผ ์ฌ์ฉํ์ฌ SSR(์๋ฒ์ฌ์ด๋ ๋ ๋๋ง) ์ ๋ก๊ทธ์ธ(์ฌ์ฉ์ ์ธ์ฆ)์ด ํ์ํ ์๋น์ค(API)๋ ์ฌ์ฉํ ์ ์์์ต๋๋ค.
- ๋ฌธ์ ์ ์์ฝ
- ์งง์ ํ ํฐ ์๋ช ๋๋ฌธ์ API ํต์ ์ ์์ด 401(UnAuthorized)์๋ฌ๊ฐ ์์ฃผ ๋ฐ์
- SSR ๋ฐฉ์์ ์ฌ์ฉํ๊ธฐ ์ํด ์๋ฒ์ ํด๋ผ์ด์ธํธ์์ ๋ชจ๋ ์ ๊ทผํ ์ ์๋ ๊ณณ์ ํ ํฐ์ ์ ์ฅํ์ฌ ์ ๊ทผํด์ผํจ
- ํ๋ก์ ํธ ์งํ ์ค์ด๊ธฐ์, ๋ค๋ฅธ ํ์๋ค์ ์ฝ๋ ๋ณ๊ฒฝ ์์ด ์คํ ๋ฆฌ์ง ๋ณ๊ฒฝ
๋ฌธ์ ํด๊ฒฐ์ ์ํ ๊ณ ๋ฏผ
1. ํ ํฐ ๋ด์ ์ ์ฅ๋์ด ์๋ ์๋ช ์ ๋ณด๊ณ ํ ํฐ์ด ๋ง๋ฃ๋๋ฉด ๋ก๊ทธ์์์ ํ์!
- ํ ํฐ ๋ง๋ฃ์๊ฐ์ด ๋๋ฉด ์๋์ผ๋ก ์ญ์ ํด์ฃผ๋ ์ฟ ํค๋ฅผ ์ฌ์ฉํ์ฌ ํ ํฐ์ ๊ด๋ฆฌํ๋ฉด, ์๋ฒ์ ํด๋ผ์ด์ธํธ์์ ๋ชจ๋ ์ ๊ทผ ๊ฐ๋ฅํ์ฌ ํ์ํ ๊ณณ์์ ์ฌ์ฉํ ์ ์๋ค.
- ํ ํฐ์ด ์ฟ ํค์ ์ ์ฅ๋์ด ์๋์ง ํ์ธ๋งํ๋ฉด ํ์ฌ ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ(์ฌ์ฉ์ ์ธ์ฆ)์ด ๋์ด์๋์ง ํ์ธํ ์ ์์ด ์ถ๊ฐ ๋ก์ง์ด ํ์ํ์ง ์๋ค.
2. ์ฟ ํค ๋ง๊ณ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ์์๊น?
- ๊ฐ๋ณ ๋ฐ์ดํฐ ๋ฒ ์ด์ค๋ฅผ ๋์ด ํ ํฐ์ ๊ด๋ฆฌํ์. : ํ ํฐ๋ง์ ์ํ ์ถ๊ฐ์ ์ธ ์๋ฒ๊ฐ ํ์ํ๋ฉฐ, ์ฉ๋์ด ์ปค์ง๋ฉด ์ถ๊ฐ์ ์ธ ๋น์ฉ ์ง๋ถ์ด ํ์ํ๋ค.(์ถ๊ฐ์ ์ผ๋ก ๋ฐ์ดํฐ ๋ฒ ์ด์ค์ ์ ์ฅํ ๋ฐ์ดํฐ๊ฐ ์์)
- ์น์คํ ๋ฆฌ์ง(๋ก์ปฌ, ์ธ์ )์ ์ ์ฅํ๋ฉด ์๋ฒ์์ ํ ํฐ์ ์ ๊ทผํ ์ ์์ด ์ ํ์ด ๋๋ค.
3. ์ฟ ํค๋ฅผ ๋ง๋ค ์ ์์๊น?
- ํด๋ผ์ด์ธํธ์์ ์ฟ ํค๋ฅผ ๋ง๋ค ์ ์์ผ๋ฉฐ, ๊ด๋ จ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(react-cookie, cookie ๋ฑ..)๊ฐ ๋ง์์ ๋ง๋๋๋ฐ ๋ฌธ์ ๊ฐ ์๋ค.
- Next.js์ API routes๋ฅผ ํ์ฉํ์ฌ ์๋ฒ์์ httponly ๋ฑ์ ์๋ฒ์์๋ง ์ง์ ๊ฐ๋ฅํ ์ค์ ๋ ๋ง์ง ์ ์๋ค!
4. httponly๋ฅผ ์ธ ์ ์์๊น?
- ํด๋ผ์ด์ธํธ์์ ์ ๊ทผํ์ง ๋ชปํ๋๋ก ๋ง์์ ๋ณด์์ ๋์ ์ ์์
- Next.js์ API routes๋ฅผ ํ์ฉํด์ ํ ํฐ์ ๊ด๋ฆฌํ๋ API๋ฅผ ๋ง๋ค์ด ์ฌ์ฉํ์.
๊ตฌํ ์ฝ๋
ํ ํฐ ์ ์ฅ API
// src/pages/api/save-token.ts
const expireOffeset = 7200; // 2์๊ฐ
const saveTokenInCookie = async (req: NextApiRequest, res: NextApiResponse) => {
// ...
const accessToken = req.body.accessToken; // accessToken์ request์์ ๊ฐ์ ธ์ด
if (accessToken !== req.cookies.accessToken) {
// ํค๋์ Set-Cookie์ค์ ์ ๋ฃ์ด ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ฟ ํค๋ฅผ ์ ์ฅํ ์ ์๋๋กํจ
res.setHeader('Set-Cookie', [
`accessToken=${accessToken}; max-age=${expireOffeset}; path=/; httponly; secure; sameSite=lax;`,
]);
// ...
ํ ํฐ ์ญ์ API
// src/pages/api/delete-token.ts
const deleteTokenFromCookie = async (
req: NextApiRequest,
res: NextApiResponse,
) => {
// ...
// ์ฟ ํค์ ์๋ช
์ 0์ผ๋ก ์ค์ ํ์ฌ ์ญ์
res.setHeader('Set-Cookie', ['accessToken=deleted; Max-Age=0; path=/']);
// ...
ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ API
// src/pages/api/get-token.ts
const getTokenFromCookie = async (
req: NextApiRequest,
res: NextApiResponse,
) => {
const accessToken = req.cookies.accessToken;
if (accessToken) {
res.status(200).json({ accessToken });
} else {
res.status(401).json({ message: '๋ก๊ทธ์ธ์ด ํ์ํ ์๋น์ค ์
๋๋ค.' });
}
// ...
};
export default getTokenFromCookie;
Axios interceptor ์์ ๋ก์ง ์ฝ์
// axios instance
const onRequest = async (config: InternalAxiosRequestConfig) => {
if (process.env.NODE_ENV !== 'production') {
console.log(config);
}
if (!config.url?.startsWith('/auth')) {
try {
const data = await axios.get('/api/get-token/cookie');
if (data.data.accessToken) {
const accessToken = data.data.accessToken;
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
}
} catch {}
}
// ...
const onResponse = async (value: AxiosResponse) => {
if (value.data?.accessToken) {
try {
const data = await axios.post(
'/api/save-token/cookie',
{
accessToken: value.data.accessToken,
},
{
headers: {
'Content-Type': 'application/json',
},
},
);
// ...
return value;
};
// ...
instance.interceptors.request.use(onRequest, onError);
instance.interceptors.response.use(onResponse, onError);
์๋ ํ ์คํธ
๋ก๊ทธ์ธ
(๋ก๊ทธ์ธ API ์๋ต -> axios interceptor response -> cookie ์ ์ฅ )

๋ก๊ทธ์์
(๋ก๊ทธ์์ -> ์ฟ ํค ์ญ์ api route ํธ์ถ)

๊ฒ์๊ธ ์กฐํ
(ํด๋์ ๋งํฌ ์กฐํ API ํธ์ถ-> Axios interceptor request -> ์ฟ ํค ๊ฐ์ ธ์์ header์ ํ์ฑ -> API ํธ์ถ ๊ณ์)


๋ง๋ฌด๋ฆฌ
์ ํ๋ ์ํฉ์์ ๋ด๊ฐ ํ ์ ์๋ ๊ฒ๋ค์ ๋ฌด์์ธ๊ฐ ํ์ธํ๊ณ , FE/BE๋ฅผ ๊ตฌ๋ถํ์ง ์๊ณ ๋ ์ข์ ์๋น์ค๋ฅผ ์ํด ๊ณ ๋ฏผํ ๊ฒฝํ์ด์๊ณ ,
"์ ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ๋ฐฉ์์ผ๋ก ์ธ์ ๊ณผ ์ฟ ํค ๋ฐฉ์์ด ์๋ค" ๋ผ๊ณ ๊ณต๋ถ๋ ํ์์ง๋ง, ๋ ๋ฐฉ์์ ์ฅ๋จ์ ์ ๋น๊ตํ๋ฉด์ ์์ฃผ ์ฌ์ฉํด๋ณผ ๊ธฐํ๊ฐ ์์๋, ๋ ์ง์ ๋ง๋ค์ด๋ณธ ๊ฒฝํ์ด ์์๋ ์ฟ ํค๋ฅผ ๋ค๋ฃจ์ด ๋ณผ ์ ์๋ ๊ฒฝํ์ด ๋์ด ์์ฐผ์ต๋๋ค.
(+ ์ถ๊ฐ ํด๊ฒฐํด๋ณผ ๋ฌธ์ )
๋ก๊ทธ์ธ์ ํ ํ์, ์ฌ์ฉ์๊ฐ ํ์ด์ง์์ ๋ ๋ ํ ๋ค์ ๋์์์ ๋ ์ฟ ํค์ ๋ง๋ฃ ์๊ฐ ๋ด๋ฉด ์๋ ๋ก๊ทธ์ธ์ด ๋์ด์ผํ๋๋ฐ
๋ฉ์ธ ํ์ด์ง์ ์๋ Navigation Bar ์ ๋ก๊ทธ์ธ ๋ฒํผ์ด ๊น๋นก๊ฑฐ๋ฆฌ๋ ๋ฒ๊ทธ๋ฅผ ์์ ํด๋ณผ ์๊ฐ์ ๋๋ค.
์ ์ฒด์ฝ๋