Next.js 웹뷰에서 JWT 인증, Authorization 헤더가 아닌 Cookie로 바꾼 이유

Next.js 앱을 네이티브 WebView에서 띄우는 하이브리드 구조에서, 인증은 대개 네이티브가 담당합니다. 로그인·토큰 갱신·보안 저장소는 앱 쪽에 두고, 웹은 네이티브로부터 토큰을 받아 사용합니다.

여기서 반복되는 질문이 하나 있습니다. 웹에서 JWT를 API·서버에 어떻게 전달할 것인가? 처음에는 흔한 방식대로 Authorization: Bearer 헤더 + 네이티브 브릿지로 토큰을 받아 썼습니다. 시간이 지나며 SSR·RSC·API 호출 전반에 걸친 stateful 인증 레이어가 운영 부담이 되었고, Cookie 주입 방식으로 바꿨습니다.

아래는 그 전환 과정입니다. JWT 서명 알고리즘이나 OAuth 플로우 전체가 아니라, 웹뷰에서 토큰을 전달하는 방식에만 집중합니다.

As-Is — Authorization Header + 네이티브 브릿지

초기 설계는 다음과 같았습니다.

  1. 네이티브가 액세스 토큰을 보유
  2. WebView 로드 후 JS 브릿지로 토큰 문자열을 웹에 전달
  3. 웹은 토큰을 메모리 + localStorage 등에 저장
  4. 모든 API 요청에 Authorization 헤더를 수동·인터셉터로 붙임
sequenceDiagram
  participant Native
  participant Bridge
  participant WebView
  participant Memory as "메모리/Storage"
  participant API

  Native->>Bridge: accessToken 전달
  Bridge->>WebView: JS 콜백으로 토큰 수신
  WebView->>Memory: localStorage + in-memory 저장
  Note over WebView,API: SSR/RSC fetch 시 헤더 주입 불가
  WebView->>API: Authorization Bearer (클라이언트 fetch만)

겉보기엔 일반적인 SPA 인증과 같지만, WebView + Next.js 조합에서는 다섯 가지 문제가 커집니다.

1. 서버 사이드 fetch 불가 — 첫 렌더의 함정

토큰은 브릿지를 통해 클라이언트 JS에만 도달합니다. Server Component, Route Handler, getServerSideProps 등 서버에서 실행되는 fetch에는 토큰을 넣을 방법이 없습니다. SSR 페이지에서 사용자 정보를 미리 가져오거나, RSC에서 API를 호출하는 패턴이 막힙니다.

“서버에서도 Authorization 헤더를 쓰면 되지 않나?”라고 우회할 수는 있습니다. 하지만 첫 렌더 시점의 Next.js 서버는 네이티브 브릿지를 호출할 수 없습니다. 브릿지는 WebView 안의 JS에서만 동작하므로, HTML을 만들 때 서버는 Authorization 정보가 없습니다. 게스트 UI로 렌더한 뒤 hydration에서 토큰을 받아 다시 fetch하는 패턴으로 가게 되고, SSR의 이점이 줄어듭니다.

더 나아가 서버에서 “현재 로그인된 사용자”를 위해 토큰을 새로 발급받는 방식을 쓰면 다른 문제가 생깁니다. 백엔드가 세션·리프레시 정책상 새 액세스 토큰 발급 시 기존 토큰을 무효화하는 경우, 웹 SSR 한 번을 위해 발급한 토큰이 네이티브가 들고 있던 토큰까지 만료시킵니다. 불필요한 서버 요청이 늘어나고, 웹뷰를 다녀온 뒤 네이티브 API 호출이 401로 깨지는 식의 연쇄 장애로 이어질 수 있습니다.

2. 이중 저장 + 동기화

새로고침·탭 전환·WebView 재로드마다 토큰을 다시 받아야 하고, 메모리와 브라우저 저장소를 함께 관리해야 합니다. 어느 한쪽만 갱신되면 “로그인은 됐는데 API는 401” 같은 불일치가 납니다.

3. 헤더 누락 = 전역 장애

axios interceptor, fetch wrapper, 개별 fetch 호출 — 인증 헤더를 붙이는 경로가 여러 개입니다. 새 API를 추가할 때 wrapper를 안 쓰거나, 서드파티 라이브러리가 자체 fetch를 쓰면 헤더가 빠집니다. 한 곳의 실수가 서비스 전체 인증 장애로 이어질 수 있습니다.

4. 웹이 토큰을 자체 발급하면 네이티브 토큰이 무효화

브릿지 없이 프론트엔드가 로그인·갱신 API를 직접 호출해 토큰을 발급받을 수도 있습니다. 그러면 네이티브가 보관하던 액세스 토큰과 웹이 새로 받은 토큰이 동시에 유효하지 않게 됩니다. 한쪽에서 refresh·reissue가 일어나면 다른 쪽 토큰은 무효화되는 구조가 흔하기 때문입니다.

그 결과 웹뷰 화면을 다녀온 뒤 네이티브 화면으로 돌아올 때마다, 네이티브는 “혹시 무효화됐을지 모르니” 토큰을 다시 갱신하는 루틴을 태워야 합니다. 웹뷰 진입·이탈마다 불필요한 refresh가 반복되고, 네이티브·웹 어느 쪽이든 토큰 발급 주체가 분산되면 동기화 비용만 커집니다.

// 매 fetch마다, 또는 axios interceptor에 의존
const token = getTokenFromMemoryOrStorage();
await fetch("https://api.example.com/users/me", {
  headers: { Authorization: `Bearer ${token}` },
});
// Server Component / SSR에서는 token을 알 수 없음

팀과 코드베이스가 커질수록 “인증 헤더를 빠뜨리지 않기”가 반복 비용이 됩니다. 인증이 인프라처럼 자동으로 동작해야 하는데, 매번 사람이 신경 써야 하는 구조였습니다.

To-Be — Cookie 주입 + credentials

전환의 핵심은 단순합니다. 브릿지로 토큰 문자열을 웹 JS에 넘기지 않고, 네이티브가 WebView에 쿠키를 직접 주입합니다. 웹 코드는 토큰을 저장·전달하지 않습니다. 브라우저가 알아서 Cookie를 붙입니다.

sequenceDiagram
  participant Native
  participant WebView
  participant NextServer as "Next.js SSR"
  participant API

  Native->>WebView: ".example.com" 도메인에 access_token 쿠키 주입
  WebView->>NextServer: document 요청 (Cookie 자동 포함)
  NextServer->>API: credentials include fetch (Cookie 전달)
  WebView->>API: credentials include fetch (Cookie 자동 전송)

쿠키 설정

웹(app.example.com)과 API(api.example.com)가 같은 루트 도메인을 쓰므로, Cookie Domain.example.com으로 설정합니다. 서브도메인 간 쿠키 전송이 허용됩니다.

속성 이유
Domain .example.com 웹 ↔ API 서브도메인 간 쿠키 공유
HttpOnly true JS에서 토큰 읽기 불가 → XSS 노출면 감소
Secure true HTTPS 전용
SameSite Strict 또는 Lax CSRF 완화. 웹뷰·동일 사이트 API면 Strict 가능
Path / API·웹 전역
Expires / Max-Age 액세스 토큰 TTL 만료 시 네이티브가 재주입

웹 코드 — stateless

클라이언트 fetch는 credentials: "include", axios는 withCredentials: true만 켜면 됩니다. interceptor로 토큰을 붙일 필요가 없습니다.

// fetch — 요청마다 credentials만 포함
await fetch("https://api.example.com/users/me", {
  credentials: "include",
});

// axios — 인스턴스 defaults 한 번 설정
axios.defaults.withCredentials = true;

서버 사이드에서는 요청에 실린 Cookie를 API로 전달합니다.

// Next.js Server Component / Route Handler
import { headers } from "next/headers";

const cookieHeader = (await headers()).get("cookie") ?? "";
const res = await fetch("https://api.example.com/users/me", {
  headers: { cookie: cookieHeader },
});

Next.js 15+에서는 cookies() 헬퍼를 쓸 수도 있습니다. 어느 쪽이든 원리는 같습니다 — 브라우저가 WebView에 실은 Cookie가 SSR 요청에도 포함되고, 서버 fetch가 그걸 API로 넘깁니다.

토큰 갱신

쿠키에 Expires 또는 Max-Age를 설정하면, 브라우저는 그 시각을 기준으로 만료된 쿠키를 자동으로 제거합니다. JS에서 지울 필요가 없고, HttpOnly와 맞물려 “만료 = 인증 정보 없음” 상태가 됩니다.

따라서 네이티브에 브릿지로 갱신을 요청해야 하는 시점은 두 가지입니다.

웹은 위 두 경우 모두 nativeBridge.requestTokenRefresh() 같은 단일 진입점을 호출하고, 갱신 로직 전체는 네이티브에 위임합니다. 네이티브가 refresh 후 쿠키를 재주입하면, 웹은 토큰 문자열을 다시 저장할 필요 없이 실패했던 요청을 재시도합니다. 토큰 발급 주체가 네이티브 하나로 모이므로, As-Is에서 겪었던 “웹이 발급 → 네이티브 토큰 무효화” 문제도 사라집니다.

As-Is vs To-Be

Authorization Header Cookie 주입
SSR / RSC 불가 가능
클라이언트 fetch interceptor 필수 credentials: "include"
토큰 저장 JS 접근 가능 (XSS 위험) HttpOnly (JS 접근 불가)
누락 시 장애 코드마다 헤더 누락 가능 브라우저가 자동 전송
갱신 웹이 토큰 문자열 재저장 네이티브가 쿠키만 갱신
토큰 발급 주체 웹·네이티브 분산 → 상호 무효화 네이티브 단일

네이티브 역할

웹 코드만으로는 Cookie를 HttpOnly로 설정할 수 없습니다. 네이티브가 WebView Cookie store에 직접 씁니다. iOS는 WKHTTPCookieStore, Android는 CookieManager 등 플랫폼 API를 사용합니다. 역할은 세 가지로 정리됩니다.

브릿지는 “토큰 문자열 전달”이 아니라 “쿠키 갱신 요청” 용도로만 남습니다. 호출 빈도가 크게 줄어듭니다.

트레이드오프·주의사항

CSRF

Cookie 기반 인증은 CSRF를 고려해야 합니다. SameSite=Strict와 API 서버의 Origin 검증을 함께 쓰면 웹뷰·동일 루트 도메인 API 구조에서는 부담이 적습니다. cross-site 요청을 받는 API라면 CSRF 토큰이나 double-submit cookie 등 추가 방어가 필요합니다.

로컬 개발

WebView 없이 브라우저에서만 개발할 때는 네이티브 쿠키 주입이 없습니다. dev proxy에서 Cookie를 설정하거나, 로컬 전용 mock 브릿지를 두는 방식으로 보완합니다.

동일 루트 도메인 전제

이 패턴은 app.example.comapi.example.com처럼 쿠키 도메인을 공유할 수 있을 때 유효합니다. 완전히 다른 도메인의 서드파티 API에는 적용되지 않습니다.

디버깅

HttpOnly 쿠키는 JS document.cookie로 확인할 수 없습니다. DevTools Application 탭에서만 볼 수 있어 디버깅이 다소 불편하지만, 그만큼 XSS로 토큰이 탈취될 위험은 줄어듭니다.

요약

  1. Authorization Header + 브릿지는 SSR 불가, 이중 저장, 헤더 누락 리스크 — 인증이 코드 전반에 퍼져 운영 부담이 커진다.
  2. 첫 렌더 서버는 브릿지를 호출할 수 없고, 서버에서 토큰을 재발급하면 네이티브 토큰까지 무효화될 수 있다. 웹이 자체 발급해도 같은 문제가 난다.
  3. 네이티브가 .example.com에 HttpOnly 쿠키를 주입하면 웹은 stateless — 저장·전달·발급 주체가 네이티브 하나로 모인다.
  4. credentials: "include" / withCredentials만으로 클라이언트·서버 fetch를 통일한다.
  5. 쿠키 Expires로 브라우저가 만료 쿠키를 제거하므로, 갱신 요청은 401(만료)쿠키 없음 두 경우에 브릿지로 네이티브에 위임한다.