Next.js 미들웨어에서 백엔드 API로부터 발급받은 쿠키를 활용하기

Table of contents
쿠키 이슈
Next.js에서 서버 컴포넌트는 서버에서 실행되고 렌더링되는 컴포넌트이다. 다음과 같은 특징이 있다.
서버에서만 동작하기 때문에 데이터베이스, 파일 시스템에도 접근할 수 있다.
서버에서 미리 렌더링된 UI를 그대로 클라이언트로 전달하기 때문에 자바스크립트 번들 사이즈를 줄일 수 있다.
브라우저에서 실행되지 않기 때문에 useState, useEffect와 같은 리액트 API를 호출할 수 없다.
별도의 백엔드 API가 개발되어있는 상황에서 Next.js로 프론트엔드를 개발할 때 API를 요청하는 케이스는 여러가지가 될 수 있다.
Next.js 클라이언트 컴포넌트를 웹 브라우저에서 렌더링할 때 백엔드 API 요청
Next.js 클라이언트 컴포넌트를 서버 측 렌더링할 때 백엔드 API 요청
Next.js 서버 컴포넌트, 서버 액션에서 백엔드 API 요청
각각의 도메인이 프론트엔드는 frontend.com, 백엔드는 backend.com으로 되어있을 때 백엔드 API에서 쿠키를 발급받았음에도 프론트엔드에서 네트워크 요청할 때 쿠키가 보이지 않는 문제가 있었다.
- 로그인 페이지에서 필요한 데이터를 입력 후 백엔드 API로 로그인 요청하면 백엔드에서는 JWT 토큰이 담긴 쿠키를 발급하게 된다.
- 쿠키의 도메인은 backend.com이다.
웹 브라우저에서는 backend.com의 쿠키를 통해 API를 요청하여 사용자 데이터를 받을 수 있다.
Next.js의 서버 렌더링 환경에서는 프론트의 서버 도메인과 쿠키의 도메인과 서로 달라서 쿠키의 값을 읽을 수 없고 사용자 데이터를 요청할 수 없다.
쿠키의 도메인 속성은 도메인을 따로 명시하지 않으면 현재 도메인으로만 쿠키를 전송할 수 있다. 만약 도메인 속성에 특정 도메인을 지정하면 서브도메인에서 쿠키를 전송 가능하도록 할 수 있다.
예를 들어 프로젝트를 진행할 때 프론트엔드를 example.com, 백엔드를 backend.example.com으로 도메인을 설정하고 쿠키의 도메인을 example.com로 지정하면 쿠키를 양 쪽에서 사용하도록 할 수 있다.
다음과 같은 예시에서는 Next.js 렌더링 서버, 미들웨어에서도 쿠키를 받을 수 있고, 백엔드 API에서도 쿠키를 받을 수 있다.
웹 페이지가 렌더링되면 백엔드의 /auth API로 요청을 보내서 쿠키를 발급받는다.
특정 버튼을 클릭하여 백엔드 /admin으로 요청을 보냈을 때 쿠키의 유무에 따라 true, false 응답을 받게 된다.
프론트엔드의 미들웨어에서도 쿠키가 있는지의 여부에 따라서 웹 페이지의 응답 헤더의
Has-Auth
의 값이 결정된다.
개발환경에서 쿠키의 SameSite 속성을 Strict
로 설정했다. backend.example.com, example.com 최상위 도메인이 같으므로 SameSite로 간주될 수 있다. SameSite를 Strict로 설정해야 타 사이트에서 쿠키를 가져가는 것을 막을 수 있다.
// Hono Backend
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { getCookie, setCookie } from "hono/cookie";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { poweredBy } from "hono/powered-by";
const app = new Hono();
app.use(poweredBy());
app.use(logger());
app.use(
"*",
cors({
origin: ["http://example.com:3000", "http://backend.example.com:3003"],
allowHeaders: [
"X-Custom-Header",
"Upgrade-Insecure-Requests",
"Set-Cookie",
],
allowMethods: ["POST", "GET", "PUT", "PATCH", "DELETE", "OPTIONS"],
maxAge: 600,
credentials: true,
})
);
app.get("/", (c) => {
return c.text("Hello Hono!");
});
app.get("/auth", (ctx) => {
const date = new Date();
const expires = new Date(date.getTime() + 24 * 60 * 60 * 1000);
setCookie(ctx, "Authorization", "THIS_IS_PASSKEY", {
path: "/",
secure: false,
httpOnly: false,
sameSite: "Strict",
expires,
domain: "example.com",
});
return ctx.json({ message: "ok" });
});
app.get("/admin", (ctx) => {
ctx.header;
const passKey = getCookie(ctx, "Authorization");
if (passKey && passKey.length > 0) {
return ctx.json({ message: true });
}
return ctx.json({ message: false });
});
const port = 3003;
serve({
fetch: app.fetch,
port,
});
// app/page.tsx
// NEXT_PUBLIC_API_URL = http://backend.example.com
"use client";
import { useEffect } from "react";
export default function Home() {
useEffect(() => {
fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth`, {
credentials: "include",
}).then((response) => {
console.log(response);
});
}, []);
const onClick = () => {
fetch(`${process.env.NEXT_PUBLIC_API_URL}/admin`, {
credentials: "include",
})
.then((resp) => resp.json())
.then((data) => {
console.log(data);
});
};
return (
<div className="w-full h-full">
<h1 className="text-2xl font-semibold">Hello World!</h1>
<button onClick={onClick}>Admin</button>
</div>
);
}
// middleware.ts
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
const cookies = request.cookies;
const auth = cookies.get("Authorization");
const res = NextResponse.next();
res.headers.set("Has-Auth", auth ? "true" : "false");
return res;
}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
],
};
로컬에서 도메인 세팅
로컬에서 프로젝트를 개발할 때 localhost 외에도 특정 도메인을 세팅하여 각 프로젝트에 필요한 도메인을 적용할 수 있다.
/etc/hosts
파일은 PC에서 도메인에 대해 IP 주소를 찾을 때 조사하는 파일이다. 파일을 열어보면 localhost가 127.0.0.1 아이피에 연결되어있는 것을 확인할 수 있다. 127.0.0.1 아이피는 루프백 아이피로 자기 자신을 가리키는 IP이다.
localhost 외에도 다른 도메인을 루프백 IP에 매핑시키면 도메인으로 로컬 프로젝트에 접속하게 할 수 있다.
127.0.0.1 example.com
127.0.0.1 backend.example.com
SameSite에 관하여
모든 서브도메인이 Same Site인 것은 아니다.
.com
또는 .net
과 같은 최상위 도메인의 경우 최상위 도메인과 그 앞 부분의 조합을 Site라고 부를 수 있다. 예를 들어 example.com의 경우 example.com의 서브도메인은 전부 Site가 같다고 할 수 있다.
반면 co.kr
의 경우 최상위 도메인이 .kr
라고 해서 app1.co.kr
와 app2.co.kr
가 같은 Site라고 하기 힘들 수 있다.
임의의 도메인 접미사(Suffix)에 대해 도메인을 등록할 수 있는지 알고리즘 상으로 판단하기 힘들다. 누군가가 도메인을 등록할 수 있는 접미사를 유효 최상위 도메인, eTLD라고 부르며 이 목록을 모아놓은 곳을 공용 접미사 목록(Public Suffix List)라고 부른다.
eTLD+1은 eTLD과 그 다음 부분을 포함하며, eTLD+1 도메인과 그 서브도메인은 전부 Site가 같다고 할 수 있다.
Subscribe to my newsletter
Read articles from Nowon Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
