Next.js + Nest.js JWT 인증 구조 전체 흐름 정리

김민석·2026년 3월 22일
post-thumbnail

프론트엔드와 백엔드가 통신할 때 가장 중요한 부분 중 하나는 Authorization (인증/인가) 이다.이번 글에서는
이번 글에서는 실제 프로젝트에서 사용한
Server Component API 호출,Client Component API 호출
cookies-next,React Query,JWT 인증, Nest.js 인증 구조를
전체 흐름 중심으로 정리해보려고 한다.

전체 흐름

Next.js Server Component Or Client Component
        ↓
cookies-next
        ↓
Auth.js session token
        ↓
fetchApi
        ↓
Nest.js API
        ↓
JWT 검증
        ↓
User 정보 반환

Next.js App Router 환경에서 Server Component와 Client Component 모두에서 쿠키를 일관되게 관리하기 위해 cookies-next를 사용하였다. 이를 통해 Auth.js session token을 쉽게 가져와 Authorization 헤더에 추가하고 Nest.js API와 안전하게 통신할 수 있도록 구성하였다.

설치 : pnpm add cookies-next@latest

cookies-next의 장점

  • 서버와 클라이언트 모두에서 사용 가능하다

    • Next.js에서 쿠키를 다룰 때 가장 큰 문제는
      서버와 클라이언트에서 사용하는 방식이 다르다는 것이다.
  • 기본 Next.js 방식

서버
import { cookies } from "next/headers";
클라이언트
document.cookie

이렇게 서버와 클라이언트 코드가 완전히 다르다.
아래는 cookies-next 사용한 것

cookies-next 사용
import { getCookie } from "cookies-next/server";
import { getCookie } from "cookies-next/client";

서버와 클라이언트에서 쉽게 사용 가능하다.

장점

  • 하나의 API로 쿠키를 관리할 수 있다
  • 코드 통일
  • 유지보수 쉬움
  • 인증 구조 관리 쉬움
  • Server Component에서도 쉽게 사용 가능

Next.js App Router에서는

기본 방식
const cookieStore = cookies();
const token = cookieStore.get("token");

조금 번거롭다. 아래는 cookies-next 를 사용한 코드

cookies-next
getCookie("token", { cookies })

간단하게 사용 가능하다.

Auth.js와 호환이 좋다

Auth.js를 사용하면 쿠키 구조가 생긴다.

proudction 인지에 따라 아래와 같은 토큰이 생긴다.
__Secure-authjs.session-token or authjs.session-token

const AUTH_COOKIE_NAME =
  process.env.NODE_ENV === "production"
    ? "__Secure-authjs.session-token"
    : "authjs.session-token";
cookies-next
getCookie(AUTH_COOKIE_NAME, { cookies })

보안 구조에 유리하다

쿠키 기반 인증은 httpOnly,secure,sameSite설정 가능하다.
cookies-next는 쿠키를 쉽게 설정할 수 있다.

setCookie("token", token, {
  httpOnly: true,
  secure: true,
  sameSite: "strict",
});

cookies-next는 Next.js에서 서버와 클라이언트 모두에서 쿠키를 동일한 방식으로 관리하고 Auth.js 및 JWT 기반 API Authorization 구조를 쉽게 구현할 수 있게 해주는 라이브러리이다.

API Authorization 구조 만들기

Next.js Server/Client 어디서든 백엔드 API를 쉽게 호출하고 Authorization을 자동으로 처리하기 위해 공통 API를 설계해보기

요약

토큰 가져오기
→ Authorization 자동 추가
→ fetch 공통 처리
→ API 재사용

전체코드

"use server";

import { cookies } from "next/headers";
import { getCookie } from "cookies-next/server";

const AUTH_COOKIE_NAME =
  process.env.NODE_ENV === "production"
    ? "__Secure-authjs.session-token"
    : "authjs.session-token";

const API_URL = process.env.API_URL || "http://localhost:8000";

async function fetchApi<T>(
  endpoint: string,
  options: RequestInit = {},
  token?: string
) {
  const headers = {
    "Content-Type": "application/json",
    ...(options.headers || {}),
  } as Record<string, string>;

  if (token) {
    headers["Authorization"] = `Bearer ${token}`;
  }

  const config: RequestInit = {
    ...options,
    headers,
    cache: "no-store",
  };

  if (options.body && typeof options.body !== "string") {
    config.body = JSON.stringify(options.body);
  }

  const response = await fetch(`${API_URL}${endpoint}`, config);

  if (!response.ok) {
    throw new Error(`API 요청 실패: ${response.status}`);
  }

  if (response.status === 204) {
    return {} as T;
  }

  const contentType = response.headers.get("Content-Type");
  if (contentType && contentType.includes("application/json")) {
    return response.json() as Promise<T>;
  } else {
    return response.text() as Promise<T>;
  }
}

export async function getUserTest(token?: string) {
  // 서버 컴포넌트에서 호출된 경우
  if (!token && typeof window === "undefined") {
    token = await getCookie(AUTH_COOKIE_NAME, { cookies });
  }

  return fetchApi<string>("/user-test", {}, token);
}

쿠키 가져오기

const AUTH_COOKIE_NAME =
  process.env.NODE_ENV === "production"
    ? "__Secure-authjs.session-token"
    : "authjs.session-token";

개발 환경과 배포 환경에 따라 auth.js의 token name이 달라진다. 그래서 분기를 주고 쿠키를 가져오는 코드.

서버 URL 가져오기

const API_URL = process.env.API_URL || "http://localhost:8000";

fetchAPI 함수

async function fetchApi<T>(
  endpoint: string,
  options: RequestInit = {},
  token?: string
) {
  const headers = {
    "Content-Type": "application/json",
    ...(options.headers || {}),
  } as Record<string, string>;

  if (token) {
    headers["Authorization"] = `Bearer ${token}`;
  }

  const config: RequestInit = {
    ...options,
    headers,
    cache: "no-store",
  };

  if (options.body && typeof options.body !== "string") {
    config.body = JSON.stringify(options.body);
  }

  const response = await fetch(`${API_URL}${endpoint}`, config);

  if (!response.ok) {
    throw new Error(`API 요청 실패: ${response.status}`);
  }

  if (response.status === 204) {
    return {} as T;
  }

  const contentType = response.headers.get("Content-Type");
  if (contentType && contentType.includes("application/json")) {
    return response.json() as Promise<T>;
  } else {
    return response.text() as Promise<T>;
  }
}

API 응답 타입을 유연하게 받기 위해 제네릭을 사용함. 제네릭을 사용하면 타입 자동 추론 및 코드 안정성이 증가한다.
다음으로 파마리터를 보면 endpoint,options,token이 있다.
endpoint는 요청할 api 경로이며, options 는 method: "GET" method: "POST" headers body 등 다양한 요청을 가능하게 할 수 있다.
다음으로 token은 서버 혹은 클라이언트에서 token을 집어 넣으면

if (token) {
  headers["Authorization"] = `Bearer ${token}`;
}

해당 코드에서 header에 추가해준다.
그 위에 코드인 headers는

const headers = {
    "Content-Type": "application/json",
    ...(options.headers || {}),
  } as Record<string, string>;

...(options.headers || {}), 이 코드로 기존의 헤더는 유지하면서 추가 헤더도 받을 수 있다.

그 다음 config를 생성하는데 options와 headers 그리고 cache 속성이 들어간다 여기서 no-store를 해주는 이유는 next.js는 자동으로 캐시 처리를 한다. 근데 인증 관련한 코드를 캐시하면 다른 사용자 데이터 반환 토큰 문제 발생 보안 위험 등 위험이 생길 수 있다. 그리고 우리는 react-query나 다른곳에서 따로 cache 처리를 할거라 no -store로 해준다.
그 다음 body를 json으로 반환해준 후 해당 api 로 쏴준다


  if (options.body && typeof options.body !== "string") {
    config.body = JSON.stringify(options.body);
  }

  const response = await fetch(`${API_URL}${endpoint}`, config);

API Hook 생성

클라이언트 컴포넌트에서 사용할 hook

"use client";

import { getCookie } from "cookies-next/client";
import * as api from "@/lib/api";

const AUTH_COOKIE_NAME =
  process.env.NODE_ENV === "production"
    ? "__Secure-authjs.session-token"
    : "authjs.session-token";

export function useApi() {
  // 클라이언트에서 쿠키 가져오기
  const token = getCookie(AUTH_COOKIE_NAME) as string;

  return {
    getUserTest: () => api.getUserTest(token),
  };
}

express.d.ts 생성

JWT 인증 후 req.user에 들어가는 타입을 TypeScript가 알 수 있도록 Express 타입을 확장하기 위해 생성

import { Express } from 'express';

type JwtPayload = {
  sub: string;
  email?: string;
  name?: string;
  picture?: null;
  iat?: number;
};

declare global {
  namespace Express {
    interface User extends JwtPayload {}
  }
}

d.ts로 생성하는 이유

타입 설정만 필요한 경우 .d.ts 파일을 사용한다.
.d.ts 파일은 타입 정의만을 위한 파일로 실행되지 않고 TypeScript 컴파일 과정에서 타입 정보만 제공한다.
반면 일반 .ts 파일로 타입을 정의할 경우 해당 파일이 모듈로 인식되어 실행 코드로 포함될 수 있다.

따라서 Express의 전역 타입을 확장하거나, req.user와 같은 타입을 프로젝트 전체에서 사용하기 위해 .d.ts 파일을 사용한다.

profile
나만의 기록장

0개의 댓글