요즘 대부분의 서비스들은 로그인을 통한 유저들을 확보한다. 그리고 JWT 토큰을 통한 유저 인증을 진행하고 있다.
그렇다면 프론트엔드에서는 토큰을 어디에 저장하고 어떻게 사용을 할까?
이번 글에서는 Next 관점에서 토큰을 어디에 저장하고 어떻게 사용하면 좋을지에 대해 알아보겠다.
이 글은 Next 15를 기준으로 작성했습니다.
토큰을 저장하는 가장 간단한 방법으로는 브라우저의 스토리지인 localStorage
혹은 sessionStorage
에 저장하는 것이다.
아래는 localStorage
를 통한 간단한 유저 인증 예시이다.
// /app/page.tsx
"use client";
import { useEffect } from "react";
export default function Home() {
...
useEffect(()=>{
const fetchData = async() => {
const token = window.localStorage.getItem("token") || undefined;
const response = await fetch("url", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`}
});
...
}
fetchData();
},[])
return (
...
);
}
위에서 볼 수 있듯 localStorage
에서 토큰을 가져와서 헤더에 넣는 식으로 간단하게 구현이 가능하다. 하지만 이는 아래와 같은 단점이 있다.
브라우저 스토리지를 이용하기 때문에 개발자 모드로 확인할 수 있어서 보안에 취약하다.
window
의 localStorage
에 접근해야 하기 때문에 클라이언트 컴포넌트로 작성해야 한다. 이는 Next의 서버 컴포넌트 이점을 살리지 못하게 된다.
또 다른 방법으로는 cookie
가 있다. Next에서 cookie
는 서버 액션에서만 동작하는 특성을 갖고 있다.
아래는 cookie
를 이용한 유저 인증 예시이다.
// /app/page.tsx
"use client";
import { useEffect } from "react";
export default function Home() {
...
const handleFetchUserData = async () => {
const res = await fetch('/api/user');
...
}
return (
...
<button onClick={handleFetchUserData}>Fetch user data</button>
);
}
// /app/api/user/route.ts
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
const cookieStore = await cookies();
const token = cookieStore.get("token") || undefined;
const response = await fetch("url", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`}
});
return NextResponse.json({ status: 200, token });
}
브라우저 스토리지와 다른 점이 있다면 서버 사이드에서 쿠키에 있는 token을 추출 후 API 요청을 보낸다는 것이다. 이는 브라우저 스토리지에서 단점이라고 언급했던 클라이언트 사이드에서의 실행을 극복했다.
하지만 쿠키는 document
로 접근 및 조작을 할 수 있기에 보안에 관한 문제는 여전히 남아있다.
그렇다면 보안까지 챙기면서 서버 사이드에서 동작할 수 있게 하는 방법은 뭐가 있을까?
HTTP Cookie
는 서버가 브라우저에 작은 데이터 조각을 보내놓는데, 브라우저는 이를 저장해두었다가 동일한 서버에 재요청을 할 때 이전에 저장한 데이터를 함께 전송한다.
이를 통해 서버는 동일한 브라우저에서 요청이 들어왔는지 판별할 수 있고, 사용자의 로그인 상태를 유지하는 데 사용할 수 있다.
HTTPOnly Cookie
는 크로스 사이트 스크립팅(XSS) 공격을 방지하기 위해 사용된다. 이는 JavaScript의 document.cookie API에 접근할 수 없어 보안이 한층 강화된다.
아래는 Next에서 HTTPOnly Cookie
를 사용하는 예제이다.
// /app/page.tsx
"use client";
import { useEffect } from "react";
export default function Home() {
...
const handleFetchUserData = async () => {
const res = await fetch('/api/user');
...
}
return (
...
<button onClick={handleFetchUserData}>Fetch user data</button>
);
}
// /app/api/user/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const body = await req.json();
const response = await fetch("url", {
method: "POST",
body: JSON.stringify(body),
});
if (!response.ok) {
return NextResponse.json({ status: response.status, statusText: response.statusText });
}
const { data } = await response.json();
const res = NextResponse.json({ message: "success", data });
res.cookies.set("token", data.token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
path: "/",
maxAge: 60 * 60 * 24,
});
return res;
}
export async function GET(req: NextRequest) {
const token = req.cookies.get("token");
// token을 이용한 로직 작성
...
return NextResponse.json(...);
}
NextRequest
와 NextResponse
는 응답 혹은 요청의 Set-Cookie
속성을 읽거나 변경할 수 있다. res.cookies
부분에 설정된 옵션들을 보면 아래와 같다.
strict
값은 브라우저가 동일한 사이트 요청에만 쿠키를 전송한다. lax
, none
옵션이 추가로 있다.path: “/docs”
인 경우/docs
, /docs/
, /docs/Web/
, /docs/Web/HTTP
/
, /docsets
, /fr/docs
상황에 맞게 옵션을 설정하여 쿠키 관리를 하면 될 것 같다.
Next에서 제공하는 HTTPOnly Cookie
를 사용하기 위해서는 Next 자체 서버로 요청을 보내야 한다.
결국 백엔드 서버로 요청을 보내기 위해서는 Next 서버를 한 번 거쳐야 한다는 것이다.
따라서 HTTPOnly Cookie
를 효율적으로 사용하기 위한 로직을 고민해 볼 필요가 있을 것 같다.
여태까지 유저 인증이 필요한 통신을 할 때는 브라우저 스토리지를 이용하여 로직을 구현했다.
보안을 생각해 보게 되면서 HTTPOnly Cookie
라는 개념을 알게 되었고, 이를 추후 프로젝트에 적용해 보고자 한다.
하지만 바로 위에서 언급했듯 HTTPOnly Cookie
를 사용하는 것이 무조건 옳은지, 만약 사용하게 된다면 코드를 어떻게 재사용성 있게 작성해야 할지 고민을 많이 해봐야 할 것 같다.