Next.js App Router 인증 구조: Proxy 1차 확인과 데이터 접근 계층(DAL)

세션 쿠키 옵션, proxy.ts의 낙관적 확인, verifySession, 레이아웃 검사의 함정

핵심 요약

  1. Proxy는 쿠키만 보는 낙관적 확인용이며 DB 조회를 넣거나 유일한 방어선으로 쓰지 않는다.
  2. 데이터 조회·변경은 verifySession을 거치는 데이터 접근 계층(DAL)에 모은다.
  3. 세션 쿠키는 서버에서 HttpOnly, Secure, SameSite, 만료와 함께 설정한다.
  4. 레이아웃은 내비게이션마다 다시 렌더링되지 않으므로 인증 확인 위치로 부적합하다.

Next.js 앱에 로그인을 붙일 때 가장 흔한 실수는 "보호해야 할 페이지의 레이아웃에서 세션을 확인하면 끝"이라고 생각하는 것이다. Next.js 인증 가이드(16.3 기준)는 인증 확인을 여러 층에 나누고, 대부분의 보안 검사는 데이터 원본에 가장 가까운 곳에서 하라고 권한다. 이 글은 세션 쿠키, Proxy, 데이터 접근 계층(DAL), DTO를 어떤 순서로 구성하는지 정리한다. 서버 액션의 입력 검증과 오류 처리는 서버 액션 에러 핸들링 글에서 다뤘다.

인증 확인을 나누는 층

층확인 방식역할한계
Proxy(proxy.ts)쿠키의 세션만 해독(낙관적 확인)보호 경로 진입 전 로그인 페이지로 이동모든 경로와 프리페치에서 실행되므로 DB 조회 금지, 유일한 방어선이 아님
데이터 접근 계층(DAL)세션 검증 후 필요 시 DB 확인(보안 확인)데이터 조회·변경 직전 권한 확인모든 데이터 접근이 DAL을 거치도록 강제해야 함
Server Actions·Route HandlersDAL의 verifySession() 호출공개 엔드포인트와 같은 수준의 검사각 함수마다 빠짐없이 호출
UI(Server Component)역할에 따라 표시 여부 결정버튼·메뉴 노출 제어보안 경계가 아님

1단계: 세션 쿠키를 안전하게 설정하기

가이드는 세션 쿠키를 서버에서 설정하고 다음 옵션을 권장한다. 세션 페이로드에는 사용자 ID와 역할 같은 최소 정보만 넣고 이메일·전화번호 같은 개인정보는 넣지 않는다.

// app/lib/session.ts
import 'server-only'
import { cookies } from 'next/headers'

export async function createSession(userId: string) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
  const session = await encrypt({ userId, expiresAt })   // jose 등으로 서명·암호화
  const cookieStore = await cookies()
  cookieStore.set('session', session, {
    httpOnly: true,     // 클라이언트 자바스크립트 접근 차단
    secure: true,       // HTTPS로만 전송
    sameSite: 'lax',    // 교차 사이트 요청 제한
    expires: expiresAt,
    path: '/',
  })
}

서명 키는 openssl rand -base64 32 같은 명령으로 만들어 환경 변수에 둔다. 직접 구현보다 인증 라이브러리를 쓰는 것이 더 안전하고 간단하다는 점도 가이드가 강조한다.

2단계: proxy.ts로 낙관적 확인하기

Next.js 16에서 경로 진입 전에 실행되는 파일은 proxy.ts다. Proxy는 Node.js 런타임에서 실행되며, 쿠키의 세션만 해독해 로그인 여부를 빠르게 판단한다.

// proxy.ts
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'

const protectedRoutes = ['/dashboard', '/settings']

export default async function proxy(req: NextRequest) {
  const path = req.nextUrl.pathname
  const isProtected = protectedRoutes.some((r) => path.startsWith(r))
  const session = await decrypt(req.cookies.get('session')?.value)

  if (isProtected && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }
  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

Proxy는 프리페치 요청을 포함한 모든 경로에서 실행되므로 DB 조회를 넣으면 성능이 크게 떨어진다. 그리고 가이드는 Proxy가 유일한 방어선이 되어서는 안 된다고 분명히 한다.

3단계: 데이터 접근 계층에 verifySession 두기

// app/lib/dal.ts
import 'server-only'
import { cache } from 'react'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { decrypt } from '@/app/lib/session'

export const verifySession = cache(async () => {
  const session = await decrypt((await cookies()).get('session')?.value)
  if (!session?.userId) redirect('/login')
  return { userId: session.userId as string }
})

export const getMyOrders = cache(async () => {
  const { userId } = await verifySession()
  // DTO: 화면에 필요한 필드만 선택
  return db.order.findMany({
    where: { userId },
    select: { id: true, status: true, total: true, createdAt: true },
  })
})

React의 cache로 감싸면 한 번의 렌더링 안에서 여러 컴포넌트가 호출해도 검증이 한 번만 실행된다. 데이터 조회 함수가 모두 DAL을 거치게 하면, 개발자가 권한 확인을 잊는 경로가 사라진다. 반환값은 필요한 컬럼만 고르는 DTO 형태로 만들어 비밀번호 해시 같은 필드가 클라이언트로 넘어가지 않게 한다.

레이아웃에서 확인하면 안 되는 이유

가이드는 부분 렌더링(Partial Rendering) 때문에 레이아웃이 내비게이션마다 다시 렌더링되지 않아, 경로가 바뀔 때마다 세션이 확인되지 않는다고 경고한다. 또 레이아웃이 하위 세그먼트를 숨겨도 그 세그먼트는 라우터가 렌더링하므로 실행을 막지 못한다. 레이아웃에서 return null로 막는 SPA식 패턴은 Server Action 같은 다른 진입점을 막지 못하므로 권장되지 않는다.

구현 후 점검 순서

  1. 로그아웃 상태에서 보호 경로 URL을 직접 입력해 로그인 페이지로 이동하는지 확인한다.
  2. 로그아웃 상태에서 Server Action을 직접 POST로 호출해 거부되는지 확인한다(UI를 거치지 않는 요청).
  3. 다른 사용자의 리소스 ID로 요청해 소유권 확인이 동작하는지 본다.
  4. 응답 JSON과 RSC Payload에 불필요한 개인정보 필드가 없는지 확인한다.
  5. 쿠키에 HttpOnly, Secure, SameSite가 설정됐는지 브라우저 개발자 도구에서 확인한다.

사내 도구라면 접근 로그와 권한 변경 이력까지 남기는 것이 좋다. 점검 항목은 바이브 코딩 사내 도구 보안 글에 정리했다.

초보자가 자주 실수하는 포인트

  • 보호 페이지의 layout.tsx에서만 세션을 확인하는 경우
  • proxy.ts에서 매 요청 DB 세션 조회를 수행하는 경우
  • 세션 페이로드에 이메일과 전화번호를 넣는 경우
  • Server Action 안에서 권한 확인 없이 클라이언트가 보낸 ID로 바로 수정하는 경우

체크리스트

  • 세션 쿠키 옵션(HttpOnly, Secure, SameSite, 만료)이 설정됐는가
  • proxy.ts가 쿠키 해독만 하고 DB를 조회하지 않는가
  • 모든 데이터 함수가 verifySession을 호출하는가
  • Server Actions·Route Handlers에서도 인증·권한을 확인하는가
  • 반환 데이터가 DTO로 필요한 필드만 포함하는가

자주 묻는 질문

middleware.ts와 proxy.ts는 같은 것인가요?

Next.js 16 문서는 경로 진입 전 로직을 Proxy(proxy.ts)로 안내한다. 이전 버전 프로젝트라면 업그레이드 가이드에서 이름 변경 내용을 확인한다.

정적 페이지도 DAL로 보호할 수 있나요?

여러 사용자가 공유하는 정적 경로는 빌드 시점에 데이터를 가져오므로 요청 시 DAL 확인이 실행되지 않는다. 가이드는 이런 경로를 Proxy로 보호하라고 안내한다.

인증 라이브러리를 쓰면 DAL이 필요 없나요?

라이브러리는 로그인과 세션을 대신 처리해 주지만, 어떤 데이터에 누가 접근할 수 있는지는 앱이 정해야 하므로 권한 확인 계층은 여전히 필요하다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.