Edge 런타임이 점점 더 인기를 끌면서, 많은 사람들이 Auth.js와 next-auth
를 이러한 환경에 배포하려고 시도하고 있습니다. 하지만 현재 전체 생태계에 걸쳐 존재하는 몇 가지 근본적인 호환성 문제에 직면하고 있습니다. 이 문서를 통해 여러분이 현재 어느 정도의 이해와 경험을 가지고 있든지 상관없이, 이러한 문제를 이해하고 선택한 런타임에서 Auth.js를 성공적으로 실행할 수 있도록 도와드리고자 합니다.
시작하기 전에, 몇 가지 배경 지식을 정리해 보겠습니다. 이미 이 내용을 알고 있다면 이 섹션을 건너뛰어도 좋습니다!
정의
이번에는 Auth.js와 오늘날 다양한 프레임워크, 호스팅 프로바이더, 라이브러리 등에서 매우 인기 있는 엣지 런타임과의 관계에 대해 이야기해 보겠습니다.
먼저, 이 맥락에서 **“엣지”**란 무엇일까요? 여기서 엣지는 네트워크 엔지니어링 분야에서 빌려온 용어로, 네트워크의 가장자리에 위치한 컴퓨팅 노드(즉, 서버)를 의미합니다. 이는 사용자와 더 가까운 위치에 있는 서버를 말합니다. 일반적으로 이러한 컴퓨팅 노드는 데이터 센터의 핵심에서 가장 중요한 작업을 실행하는 완전한 서버보다는 낮은 성능을 가집니다. 여기서 코드를 실행하는 데는 사용자 기기까지의 지연 시간이 줄어들고, 확장성이 더 좋으며, 비용 효율적인 컴퓨팅이 가능하다는 장점이 있습니다. 반면, 하드웨어 성능이 낮고 소프트웨어 스택의 호환성이 다를 수 있다는 단점도 있습니다.
따라서 엣지 런타임이라고 할 때, 우리는 Node.js가 아닌 서버 측 자바스크립트 런타임을 의미하며, 이는 엣지 컴퓨팅 노드(서버)에서 실행되도록 최적화되어 있습니다. 이는 일반적으로 코드가 사용자와 더 가까운 위치에서 실행되고, 빠른 시작 시간, 낮은 메모리 사용량 등에 최적화된 낮은 성능의 하드웨어에서 실행된다는 것을 의미합니다.
문제는 이러한 런타임이 종종 Node.js가 제공하는 기능을 지원하지 않으며, 때로는 이 기능들이 여러분이 의존하는 라이브러리와 패키지의 동작에 중요한 역할을 한다는 점입니다. 어떤 패키지가 “엣지 호환” 또는 “엣지 준비 완료”라고 표시할 때, 이는 해당 소프트웨어가 일부 엣지 런타임에서 누락된 Node.js 기능/모듈을 피하도록 설계되어 더 보편적으로 호환된다는 것을 의미합니다. unjs의 호환성 매트릭스를 확인하면 어떤 런타임이 어떤 기능을 지원하는지 알 수 있습니다. Auth.js와 직접적인 관련은 없지만, 자바스크립트 런타임이 API 상호 운용성을 협력할 수 있는 공간을 제공하는 산업 그룹인 WinterCG를 언급할 좋은 기회입니다.
여기서 주목할 점은 이러한 기능/모듈이 종종 누락되는 이유는 실행 환경이 이를 제공하지 않기 때문이라는 것입니다. 예를 들어, 개발자가 아무리 시간을 투자해도, 서버 측 자바스크립트 런타임이 파일 시스템에 접근할 수 없는 샌드박스 운영 체제 환경에서 실행된다면, 아무리 노력해도 fs
모듈을 구현할 수 없습니다.
현재 Node.js와 다른 런타임 간의 상황이 매우 분열적이고 유동적이기 때문에, 많은 라이브러리가 fetch
와 같은 가장 일반적인 기능만 사용하도록 작업을 최적화하고 있습니다. 예를 들어, 데이터베이스 프로바이더라면 클라이언트 라이브러리가 백엔드와 통신하기 위해 HTTP 요청만 하면 되도록 시스템을 설계할 수 있습니다. 그러면 라이브러리를 “엣지 호환”으로 광고하고 사용자가 원하는 어디에서나 실행할 수 있습니다. 이는 예를 들어 Node.js의 원시 TCP 소켓을 사용하여 백엔드와 통신해야 하는 다른 데이터베이스 클라이언트 라이브러리와는 대조적입니다.
Auth.js
Auth.js는 Edge 호환성을 최적화했습니다. 이는 여러분이 선택한 어떤 JavaScript 런타임에서도 Auth.js의 핵심 기능을 실행할 수 있다는 의미입니다. 여기서 중요한 키워드는 핵심 기능입니다. 만약 Auth.js / next-auth
만 사용하고, Auth.js 콜백이나 미들웨어 등에서 다른 라이브러리를 사용하지 않는다면, 어디서든 자유롭게 사용할 수 있습니다!
문제는 Auth.js와 함께 다른 라이브러리를 사용하려고 할 때 발생합니다.
The Problem
데이터베이스 어댑터
Auth.js와 함께 사용하여 포괄적인 인증 시스템을 구현할 때 자주 사용하는 패키지 중 하나가 데이터베이스 클라이언트입니다. 데이터베이스 클라이언트는 종종 TCP 소켓을 사용해 데이터베이스 서버와 직접 통신하기 때문에 문제가 될 수 있습니다. 이러한 방식으로 동작하는 대표적인 데이터베이스가 PostgreSQL입니다.
PostgreSQL은 클라이언트와 서버 간 통신을 위해 TCP(또는 Unix) 소켓을 통해 전송되는 메시지 기반 프로토콜을 사용합니다. Node.js의 기능 중 하나인 Raw TCP 소켓은 일반적으로 엣지 런타임에서 사용할 수 없습니다. 따라서 표면적으로는 엣지 런타임에서 실행되는 JavaScript로 PostgreSQL 데이터베이스와 통신하는 것이 불가능해 보입니다. 이는 다른 많은 데이터베이스와 그들의 통신 프로토콜에도 동일하게 적용됩니다.
하지만 엣지 런타임이 발전하고 더욱 대중화되면서, 사람들은 이 문제를 해결하기 위해 다양한 창의적인 방법을 고안했습니다. 그 중 하나는 데이터베이스 앞에 API 서버를 두는 것입니다. 이 API 서버의 목적은 HTTP를 통해 전송된 데이터베이스 쿼리를 데이터베이스가 이해할 수 있는 프로토콜로 변환하는 것입니다. 이를 통해 클라이언트 측에서는 API 서버에 HTTP 요청만 보내면 되며, 이는 모든 엣지 런타임에서 지원하는 방식입니다.
미들웨어
Next.js와 next-auth
에서는 Next.js 미들웨어를 사용하여 세션이 존재하는지 확인하고 다음에 어디로 라우팅할지 결정함으로써 라우트를 보호할 수 있습니다. 기본적으로 Vercel 및 다른 호스팅 제공자에서는 미들웨어 코드가 항상 엣지 런타임에서 실행됩니다. 이는 우리의 코드가 예를 들어 PostgreSQL 쿼리를 실행하려고 시도할 때, 기본 기능이 사용 불가능한 환경(즉, TCP 소켓)에서 실행된다는 것을 의미합니다. 따라서 명시적으로 “엣지 호환”이 아닌 데이터베이스 어댑터를 사용하려면, 우리가 사용할 수 있는 기능을 활용하여 데이터베이스를 쿼리하는 방법을 찾아야 합니다.
해결 방법
Auth.js는 데이터베이스 세션 전략과 데이터베이스 어댑터를 함께 사용할 때 일반적인 동작 중에 데이터베이스에 여러 번 접근합니다. 어떤 프레임워크를 사용하든, 모든 Auth.js 클라이언트는 현재 활성 세션을 가져올 수 있으며, 이는 사용자의 sessionToken
이 데이터베이스에 존재하고 유효한지(즉, 만료되지 않았는지) 확인하기 위해 데이터베이스를 쿼리하는 방식으로 이루어집니다.
이는 애플리케이션에서 사용자가 인증되었는지 확인하려는 모든 곳에서 데이터베이스 호출이 필요하다는 것을 의미합니다. 실제로 Auth.js는 이 문제를 조금 더 똑똑하게 처리하며 캐싱과 같은 기법을 사용해 불필요한 데이터베이스 요청을 줄입니다. 하지만 모든 auth()
호출이 데이터베이스 쿼리를 트리거한다는 점을 고려하면, 많은 데이터베이스 어댑터와 함께 에지 런타임에서 Auth.js를 사용하기 위해서는 어떤 해결책이 필요합니다!
설정 분리
Next.js와 next-auth
를 고려할 때, Auth.js가 일부 코드를 엣지 런타임에서 실행하면서도 데이터베이스를 사용해 세션을 저장할 수 있도록 하려면 어떻게 해야 할까요? 엣지 환경에서는 데이터베이스 설정이 없는 next-auth
버전이 필요하고, 그 외의 환경에서는 데이터베이스가 포함된 버전이 필요합니다. 이를 위해 Auth.js의 “지연 초기화” 기능을 사용해 미들웨어에서는 어댑터 없이 독립적인 클라이언트를 초기화하고, 다른 곳에서는 데이터베이스가 포함된 클라이언트를 사용할 수 있습니다.
- 먼저, 모든 곳에서 사용할 공통 Auth.js 설정 객체를 만듭니다. 이 객체는 데이터베이스 어댑터를 포함하지 않습니다.
import GitHub from "next-auth/providers/github"
import type { NextAuthConfig } from "next-auth"
// 이 객체는 Auth.js 인스턴스가 아닌 단순한 설정 객체입니다.
export default {
providers: [GitHub],
} satisfies NextAuthConfig
- 다음으로, 이 설정을 가져오지만 어댑터를 추가하고 세션 전략으로
jwt
를 사용하는 초기화된 Auth.js 인스턴스를 만듭니다.
import NextAuth from "next-auth"
import authConfig from "./auth.config"
import { PrismaClient } from "@prisma/client"
import { PrismaAdapter } from "@auth/prisma-adapter"
const prisma = new PrismaClient()
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: "jwt" },
...authConfig,
})
- 미들웨어에서는 데이터베이스 어댑터 없이 설정을 가져와 자체 Auth.js 클라이언트를 초기화합니다.
import NextAuth from "next-auth"
import authConfig from "./auth.config"
export const { auth: middleware } = NextAuth(authConfig)
- 마지막으로, 다른 곳에서는 기본
auth.ts
설정에서 가져와 평소처럼next-auth
를 사용할 수 있습니다. 더 많은 예제는 세션 관리 문서를 참고하세요.
import { auth } from "@/auth"
export default async function Page() {
const session = await auth()
if (!session) {
return <div>Not authenticated</div>
}
return (
<div className="container">
<pre>{JSON.stringify(session, null, 2)}</pre>
</div>
)
}
여기서 중요한 점은 미들웨어에서 next-auth
의 데이터베이스 기능과 지원을 제거했다는 것입니다. 이는 미들웨어에서 코드를 실행할 때 세션을 가져오거나 사용자 계정 정보 등을 조회할 수 없다는 것을 의미합니다. 따라서 위의 /app/protected/page.tsx
파일에서 보여준 것과 같은 검사를 통해 라우트를 보호해야 합니다. 미들웨어는 여전히 세션 쿠키의 만료 시간을 연장하는 등의 용도로 사용할 수 있습니다.