Skip to main content

Command Palette

Search for a command to run...

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

백엔드 API로 받은 쿠키를 Next.js 서버 컴포넌트에서도 찾을 수 있는 방법에 대해 작성했습니다.

Updated
4 min read

쿠키 이슈

Next.js에서 서버 컴포넌트는 서버에서 실행되고 렌더링되는 컴포넌트이다. 다음과 같은 특징이 있다.

  • 서버에서만 동작하기 때문에 데이터베이스, 파일 시스템에도 접근할 수 있다.

  • 서버에서 미리 렌더링된 UI를 그대로 클라이언트로 전달하기 때문에 자바스크립트 번들 사이즈를 줄일 수 있다.

  • 브라우저에서 실행되지 않기 때문에 useState, useEffect와 같은 리액트 API를 호출할 수 없다.

별도의 백엔드 API가 개발되어있는 상황에서 Next.js로 프론트엔드를 개발할 때 API를 요청하는 케이스는 여러가지가 될 수 있다.

  • Next.js 클라이언트 컴포넌트를 웹 브라우저에서 렌더링할 때 백엔드 API 요청

  • Next.js 클라이언트 컴포넌트를 서버 측 렌더링할 때 백엔드 API 요청

  • Next.js 서버 컴포넌트, 서버 액션에서 백엔드 API 요청

각각의 도메인이 프론트엔드는 frontend.com, 백엔드는 backend.com으로 되어있을 때 백엔드 API에서 쿠키를 발급받았음에도 프론트엔드에서 네트워크 요청할 때 쿠키가 보이지 않는 문제가 있었다.

  1. 로그인 페이지에서 필요한 데이터를 입력 후 백엔드 API로 로그인 요청하면 백엔드에서는 JWT 토큰이 담긴 쿠키를 발급하게 된다.
  1. 웹 브라우저에서는 backend.com의 쿠키를 통해 API를 요청하여 사용자 데이터를 받을 수 있다.

  2. 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.krapp2.co.kr가 같은 Site라고 하기 힘들 수 있다.

임의의 도메인 접미사(Suffix)에 대해 도메인을 등록할 수 있는지 알고리즘 상으로 판단하기 힘들다. 누군가가 도메인을 등록할 수 있는 접미사를 유효 최상위 도메인, eTLD라고 부르며 이 목록을 모아놓은 곳을 공용 접미사 목록(Public Suffix List)라고 부른다.

eTLD+1은 eTLD과 그 다음 부분을 포함하며, eTLD+1 도메인과 그 서브도메인은 전부 Site가 같다고 할 수 있다.

More from this blog

AI 에이전트에서 작업 맥락을 관리하기 위해 사용한 방법

AI 에이전트의 불편함 Codex CLI와 같은 AI 코딩 에이전트를 통해서 프로젝트 개발 속도를 높일 수 있다. 그리고 혼자 작업했을 때는 놓칠 수 있는 부분도 발생할 수 있는데 AI 에이전트를 통해 보완할 수 있었다. 컴포넌트에 대해서 접근성 태그 작성 API 호출하는 비동기 코드 작성할 때 에러 처리 그리고 AI 에이전트를 잘 활용하면 불가능하

Mar 8, 202610 min read

지시사항을 분할하여 Codex로 효율적인 작업하기

이슈 Codex와 같은 AI 기반 코드 에이전트는 출력 토큰의 수가 제한되어 있다. 그래서 다양한 작업을 하나의 프롬프트에 전부 몰아서 작성하면 원하는 결과가 나오지 않을 수 있다. Codex와 같은 LLM 기반 서비스는 출력 토큰 수를 초과하게 되면 일부 단계가 누락되거나 특정 요소를 과하게 요약할 수 있고 이는 전체적인 답변 퀄리티를 낮출 수 있기 때문이다. 그래서 다양한 AI 공식문서는 특정 작업을 더 작은 단위로 분할하는 방식을 강조한다...

Nov 3, 20255 min read

Key를 활용하여 컴포넌트 상태를 초기화하기

개요 컴포넌트의 상태가 업데이트되어도 컴포넌트 내부에 배치된 컴포넌트의 상태는 그대로 유지되는 경우가 있었다. 예전에는 Props 전달 및 내부에서 useEffect를 통해서 상태를 초기 상태로 업데이트하도록 했는데 useEffect를 남용하게 되면 상태 변화 추적이 어려워지는 문제가 있었다. 그런데 useEffect 외에도 Key를 통해서 컴포넌트 상태를 초기화할 수 있다는 사실을 알게 되었다. 리스트 렌더링 React 프론트엔드에서 목록 형...

Sep 21, 20253 min read

내가 AGENTS.md를 작성하는 방법

Codex CLI 요즘 프로젝트 개발에 Codex CLI를 활용하는 이유는 다음과 같다. 특정 프로젝트를 작업할 때 Claude Code는 종종 사용량을 초과한 것과 다르게 Codex CLI는 ChatGPT Plus 요금제만으로도 토큰 사용량 초과 걱정 없이 충분하게 활용할 수 있었다. 타 CLI 기반 AI 개발 도구에 비해서 요구사항을 더 정확하게 구현하고 꼭 필요한 작업만 진행하기 때문에 코드를 검토하는 시간을 줄일 수 있다. 반복적인 코드...

Sep 21, 20254 min read
N

Nowon Lee

22 posts