Communitygithub.com

rbikbov/frontend-starter

Next.js(App Router) 프론트엔드의 인증 및 세션 관리: 토큰 저장 위치, HttpOnly 쿠키를 사용하는 BFF, 경쟁 상태 없는 리프레시 토큰, '인증되지 않음' 판별(GraphQL 200 응답 포함), proxy.ts 및 Data Access Layer에서의 검증, 안전한 로그인 후 리디렉션, 이전 사용자 데이터 누출 없는 로그아웃, 비밀번호 재설정 링크 내 토큰, 로그인 및 복구 폼 구현 시 사용합니다.

frontend-starter란 무엇인가요?

이 스킬은 Next.js(App Router)를 기반으로 한 프론트엔드 애플리케이션에서 안전한 인증 및 세션 관리를 구현하기 위한 포괄적인 가이드를 제공합니다. 백엔드에서 받은 토큰 또는 세션 ID를 프론트엔드가 저장, 갱신 및 검증해야 하는 상황에 초점을 맞춥니다. BFF(Backend for Frontend) 패턴을 기본으로 하여 HttpOnly 쿠키에 토큰을 저장하며, Auth.js, Better Auth, Clerk과 같은 라이브러리를 사용하는 프로젝트에도 세션 관리 메커니즘과 관련된 여러 규칙(예: 안전한 리디렉션, 데이터 누출 방지 로그아웃)이 적용됩니다. 이 스킬의 핵심은 12개 섹션으로 구성된 상세한 규칙 세트입니다. 주요 내용으로는 세션 모델 선택(BFF, 백엔드 직접 쿠키 설정, 라이브러리 등), HttpOnly, Secure, SameSite 속성을 포함한 쿠키 설정의 필수 규칙, route handler를 통한 BFF 구현, proxy.ts(middleware.ts)와 Data Access Layer(DAL)에서의 세션 검증 책임 분리, 단일 비행(single-flight) 패턴을 사용한 경쟁 상태 없는 리프레시 토큰 처리, 안전한 callbackUrl 검증(getSafeRedirect), 그리고 로그아웃 시 클라이언트 측 캐시와 스토어를 완전히 정리하는 방법을 다룹니다. 이 스킬은 특히 멱등성, 보안, 그리고 사용자 경험을 중시합니다. GraphQL의 200 응답 내에서 인증 오류를 식별하는 방법, 비밀번호 재설정 토큰을 URL에서 즉시 제거하는 방법, 서버 장애 시 세션을 유지하면서도 인증 실패 시 쿠키를 삭제하는 차별화된 오류 처리를 강조합니다. 또한 선택한 모델과 프록시 설정을 ADR(Architecture Decision Record)에 문서화할 것을 권장합니다. 이 지침은 Claude Code, Cursor, Codex와 같은 AI 에이전트와 직접 협업하여 Next.js 프로젝트의 인증 시스템을 설계, 구현 및 검토할 때 유용합니다. 개발자는 이 스킬의 규칙을 참고하여 안전하고 신뢰할 수 있는 프론트엔드 인증 아키텍처를 구축할 수 있습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/rbikbov/frontend-starter/tree/HEAD/.claude/skills/frontend-auth-session

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Next.js 인증 및 세션

백엔드로부터 세션(토큰 또는 세션 ID)을 받아 저장, 갱신 및 검증해야 하는 프론트엔드에 관한 문서입니다. 프로젝트가 라이브러리나 제공자(Auth.js, Better Auth, Clerk 등)를 사용하는 경우 세션 메커니즘은 해당 라이브러리가 처리하지만, 섹션 2, 6~10의 규칙은 여전히 적용됩니다. 코드는 references/implementation.md를 참조하세요.

규칙 우선순위. 사용자 지침과 프로젝트의 agent 파일이 더 중요합니다. 백엔드가 다른 방식을 강제하는 경우(예: 자체적으로 쿠키를 설정하는 경우) 백엔드를 따르고 그 차이점을 README 또는 ADR에 기록하세요.

표기: MUST — 위반할 수 없음; SHOULD — 기본적으로 따라야 하며, 예외는 이유가 필요함.


1. 모델 선택

모델사용 시기
BFF: Next route handler가 백엔드로부터 토큰을 받아 HttpOnly 쿠키에 저장백엔드가 응답 본문에 토큰을 반환하는 경우. Next의 기본 옵션
백엔드가 직접 HttpOnly 쿠키 설정프론트엔드와 API가 같은 사이트에 있고, 백엔드를 제어할 수 있는 경우
라이브러리 또는 제공자OAuth, MFA, 사용자 관리가 필요한 경우 — 직접 구현하지 마세요
Access는 메모리에, refresh는 HttpOnly 쿠키에BFF가 없는 SPA
토큰을 localStorage/sessionStorage에 저장금지: XSS 공격 하나로 세션이 탈취됨

두 번째 결정 사항 — BFF가 프록시할 내용:

옵션장점단점
login / refresh / logout만 프록시, 나머지 요청은 API에 직접 쿠키와 함께 전송BFF가 병목이 아님; 코드 감소API가 쿠키에서 토큰을 읽고 CORS를 구성해야 함; 쿠키가 API 도메인에 도달해야 함
모든 요청을 BFF를 통해 전송API가 쿠키와 CORS를 몰라도 됨; 단일 origin모든 요청에 추가 홉 발생; BFF 확장 필요

선택 사항은 ADR에 기록합니다.


2. 쿠키 (MUST)

  • 접근 권한을 부여하는 모든 것(access, refresh, 세션 ID)에 대해 HttpOnly.
  • 프로덕션에서는 Secure. 로컬에서 브라우저는 http://localhost에서 Secure를 허용하지만, 다른 http 호스트에서는 허용하지 않습니다.
  • 기본적으로 SameSite=Lax. Strict는 CSRF에 대해 더 안전하지만, 이메일이나 다른 사이트의 링크를 통해 방문한 사용자가 첫 번째 탐색에서는 인증되지 않은 것으로 보입니다 — 의식적으로 선택하고 그 이유를 기록하세요.
  • Path=/; 수명은 토큰의 exp 또는 백엔드 응답을 기준으로 하며, '대충' 설정하지 마세요.
  • SHOULD: 이름에 __Host- 접두사 사용(Secure, Path=/, Domain 없음 필요) — 서브도메인에서 쿠키를 변조할 수 없습니다.
  • 쿠키는 서버에서만 설정 및 삭제됩니다(route handler, Server Action, proxy.ts). Server Component는 쿠키를 설정할 수 없습니다.
  • JS에서 읽을 수 없는 쿠키가 유일한 세션 전달자입니다. is_authenticated와 같은 HttpOnly가 없는 플래그는 UI를 위한 힌트로만 허용되며, 접근 결정에 절대 사용되지 않습니다.

3. BFF: route handlers (MUST)

  • 로그인은 스키마(zod)로 유효성 검사; 잘못된 형식의 본문은 400.
  • 변경하는 핸들러는 Origin을 애플리케이션의 origin과 비교: SameSite=Lax는 같은 사이트의 서브도메인으로부터의 요청을 보호하지 않습니다. Server Action은 자체적으로 Origin과 Host를 비교합니다.
  • 클라이언트 컨텍스트를 백엔드에 전달: X-Forwarded-For, User-Agent — 그렇지 않으면 백엔드의 rate limit이 서버 IP만 볼 수 있습니다.
  • 백엔드 오류는 kind와 상태 코드(401, 403, 422, 429 ( Retry-After 포함), 503)가 포함된 응답으로 변환되며, 백엔드 텍스트는 외부로 유출되지 않습니다.
  • Refresh: 백엔드가 refresh 토큰을 거부한 경우 — 쿠키를 삭제하고 401 응답. 백엔드를 사용할 수 없는 경우(네트워크, 5xx) — 쿠키를 그대로 두고 503 응답: 일시적인 장애로 인해 로그아웃되면 안 됩니다.
  • Logout: 백엔드에서 토큰을 폐기(오류는 로깅하되 표시하지 않음), 어떤 경우든 쿠키를 삭제합니다.

4. 세션 확인 위치 (MUST)

위치하는 일하지 않는 일
proxy.ts (Next 16+, 이전에는 middleware.ts)쿠키 존재 여부에 따른 낙관적 확인: 로그인 페이지로 리디렉션 및 로그인 페이지에서 리디렉션DB나 API에 접근하지 않음; 유일한 보호 수단이 아님
Data Access Layer (server-only 모듈의 verifySession())데이터 근처에서 확인: 서버 컴포넌트, route handler, Server Action—
레이아웃아무것도 하지 않음: 레이아웃은 탐색 시 다시 렌더링되지 않음접근을 확인하지 않음
백엔드진실 공급원: 모든 요청에서 토큰 확인—
  • 모든 route handler와 Server Action은 공개 진입점이며 자체적으로 verifySession()을 호출합니다.
  • proxy.ts는 refresh 쿠키(또는 세션 ID)를 확인합니다. access는 만료되었을 수 있지만 세션은 유효할 수 있습니다.
  • 경로 일치는 세그먼트 기준: /app 및 /app/…와 일치하지만 /apple과는 일치하지 않습니다.
  • cacheComponents(Next 16.4 프로젝트에서 기본값)를 사용하는 경우, 세션 읽기 및 사용자 데이터 캐싱에는 고유한 규칙이 있습니다. DAL을 작성하기 전에 node_modules/next/dist/docs/01-app/02-guides/authentication-with-cache-components.md를 읽으세요.
  • 권한(역할, 소유권)은 확인된 데이터(서명(JWKS, jose) 또는 백엔드 응답)를 기반으로 결정됩니다. 확인되지 않은 JWT payload는 접근 근거가 될 수 없습니다.
  • Server Component는 쿠키를 업데이트할 수 없습니다. 서버 렌더링 시점에 access가 만료된 경우: proxy.ts가 세션을 갱신하거나(응답에 쿠키를 설정할 수 있음 — 문서 탐색에 대해서만 수행하고 prefetch에는 수행하지 않으며, refresh 토큰 순환을 고려), 페이지가 클라이언트 로딩을 렌더링하고 클라이언트가 refresh를 수행합니다.

5. 클라이언트: 경쟁 상태 없는 refresh (MUST)

  • '인증되지 않음' 판별: HTTP 401, 또는 GraphQL 응답(200)에서 extensions.code === "UNAUTHENTICATED"인 오류가 하나 이상 있는 경우(errors.some, errors[0] 아님). 메시지 텍스트 비교는 백엔드가 코드를 제공하지 않는 경우에만, 단일 함수에서, 주석과 테스트와 함께 수행합니다.
  • 단일 비행(Single-flight): 401을 받은 병렬 요청은 하나의 공통 refresh를 기다렸다가 그 결과를 사용합니다. Promise는 finally에서 재설정됩니다.
  • 원래 요청은 한 번 재시도됩니다. 두 번째 401은 세션이 손실된 것이며, 두 번째 refresh 없이 처리합니다.
  • refresh 요청 자체는 인터셉터를 통과하지 않습니다(그렇지 않으면 재귀 발생).
  • refresh 결과는 세 가지 결과를 구분합니다: 발급됨(true) → 재시도; 거부됨(false) → 세션 만료; 네트워크 또는 5xx(예외) → network 오류, 계정에서 로그아웃되지 않음.
  • 로그아웃 중에는 refresh가 차단됩니다. 그렇지 않으면 로그아웃 시점에 401로 실패한 요청이 즉시 세션을 복원합니다.
  • 만료된 세션: 요청 캐시를 지우고 현재 페이지의 callbackUrl과 함께 로그인 페이지로 이동합니다.
  • 재시도할 요청의 본문은 재시도 가능해야 합니다(문자열, FormData). 일회용 ReadableStream은 안 됩니다.

6. 로그인 후 리디렉션 (MUST)

  • callbackUrl (next, returnTo)은 getSafeRedirect(raw, fallback, appOrigin) 함수로 확인합니다: \ 및 제어 문자를 거부하고(브라우저는 /\.evil.com 및 /<tab>/evil.com을 //evil.com으로 변환), new URL(raw, appOrigin)을 구문 분석하고, origin을 비교한 후 경로만 반환합니다. '/로 시작하고 //로 시작하지 않음' 확인은 충분하지 않습니다.
  • 이에 대한 테스트 세트는 references/implementation.md, 섹션 7에 있습니다.
  • 로그인 후 서버 컴포넌트는 새 쿠키를 볼 수 있어야 합니다: 전체 탐색(window.location.assign) 또는 router.replace + router.refresh()를 사용합니다.

7. 로그아웃 (MUST)

  • 서버가 쿠키를 삭제합니다(섹션 3).
  • 클라이언트가 요청 캐시(queryClient.clear())와 모든 클라이언트 측 사용자 데이터 스토어를 지웁니다: 동일한 탭의 다음 사용자는 잠시라도 이전 사용자의 데이터를 볼 수 없습니다.
  • 캐시된 보호 화면으로 '뒤로' 이동할 수 없도록 로그인 페이지로 이동합니다(replace 또는 전체 탐색).
  • 로그아웃 확인은 제품 선택 사항; 요청 중 버튼 차단은 필수.
  • SHOULD: 다른 탭이 로그아웃을 알 수 있어야 함(BroadcastChannel 또는 플래그에 대한 storage 이벤트). 그렇지 않으면 첫 번째 401까지 데이터를 계속 표시합니다.

8. 링크의 토큰 (비밀번호 재설정, 이메일 확인)

  • 토큰은 URL에서 한 번 읽어 상태에 저장한 후, 토큰 없이 주소를 즉시 대체합니다(router.replace(pathname)): 기록에 남지 않고 Referer로 전송되지 않습니다.
  • 이러한 페이지에는 Referrer-Policy: no-referrer를 사용하고 타사 스크립트(분석, 위젯)를 전혀 사용하지 않습니다.
  • 토큰은 로깅되지 않습니다. 일회성 및 수명은 백엔드가 보장합니다. 프론트엔드는 '링크가 만료되었습니다'라는 명확한 화면과 '새 링크 요청' 작업을 표시합니다.

9. 로그인 및 복구 폼

  • 비밀번호 복구는 존재하는 이메일과 존재하지 않는 이메일에 대해 동일하게 응답합니다('계정이 있는 경우 이메일이 전송되었습니다'). 누가 등록되었는지 공개하지 않습니다. 백엔드가 공개하는 경우 제한 사항으로 기록합니다.
  • 전송은 요청 중에 차단됩니다. 재전송은 두 번째 요청을 생성하지 않습니다.
  • 429: Retry-After가 있는 경우 대기 시간 메시지를 표시합니다.
  • 필드: type="email" + autoComplete="email"; 비밀번호는 로그인 시 autoComplete="current-password", 등록 및 변경 시 "new-password"; 붙여넣기를 차단하지 않습니다.
  • '비밀번호 표시' 버튼: type="button", aria-label 및 aria-pressed, 필드에서 포커스를 빼앗지 않습니다.
  • '이메일 또는 비밀번호가 잘못되었습니다' 오류는 무엇이 잘못되었는지 구체적으로 알리지 않습니다.
  • 서버 오류는 코드별로 필드에 매핑됩니다. 일반 오류는 폼 위에 표시됩니다.

10. 데이터 및 로그

  • 사용자별 응답은 공유 캐시("use cache", CDN)에 저장되지 않습니다. 서버의 QueryClient는 요청당 새로 생성됩니다.
  • 로그에는 토큰, 비밀번호, 이메일이 없습니다. 최대 사용자 ID와 오류 kind만 기록합니다.
  • 세션 토큰 payload는 최소화합니다: ID와 역할, 개인 데이터는 포함하지 않습니다.

11. 테스트

  • 단위: getSafeRedirect (references의 전체 세트); '인증되지 않음' 판별 (401, GraphQL 코드, 빈 본문, JSON 대신 HTML).
  • 단일 비행: 401을 받은 N개의 병렬 요청 → 하나의 refresh, N번 재시도; refresh 거부됨 → 재시도 없음, onSessionExpired 한 번 호출; refresh 네트워크 오류 → network 오류, 로그아웃 없음; 성공적인 refresh 후 두 번째 401 → 두 번째 refresh 없음; 로그아웃 중 → refresh 없음.
  • Route handlers: 쿠키 플래그; refresh 거부됨 → 쿠키 삭제; 백엔드 사용 불가 → 쿠키 유지, 503; 잘못된 본문 → 400; 잘못된 Origin → 403.
  • e2e: 로그인 → 보호된 페이지 → 로그아웃 → 보호된 페이지 접근 불가; access 만료(테스트 환경에서 짧은 TTL) → 투명한 refresh; 비밀번호 재설정 링크 → 주소 표시줄에서 토큰 사라짐.

12. 완료 조건(Definition of Done)

  • 어떤 토큰도 JS에서 읽히지 않음; 세션에 localStorage를 사용하지 않음.
  • 쿠키: HttpOnly, 프로덕션에서 Secure, SameSite, Path, 수명은 토큰 기준.
  • Refresh: 백엔드 거부와 백엔드 사용 불가를 구분함.
  • 결과가 있는 단일 비행, 한 번 재시도, 로그아웃 중 차단 — 테스트로 검증됨.
  • proxy.ts — 낙관적 확인만 수행; DAL은 handlers, actions 및 서버 컴포넌트에서 호출됨.
  • 전체 테스트 세트가 포함된 getSafeRedirect.
  • 로그아웃 시 클라이언트 캐시 및 스토어 지움.
  • 링크의 토큰이 주소 표시줄에서 제거됨.
  • 로그에 토큰 및 개인 데이터 없음.
  • 모델 및 프록시 선택이 ADR에 기록됨.

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

rbikbov/frontend-starter

Creates Architecture Decision Records documenting key technical decisions with context, alternatives considered, tradeoffs, consequences, and decision owners. Use when documenting "architecture decisions", "technical choices", "design decisions", or "ADRs".

rbikbov/frontend-starter

Проверка фронтенд-кода и документации на «нейрослоп» перед коммитом, PR или публикацией репозитория: остатки генераторов, мёртвые и недоделанные опции, утверждения в README и комментариях, не подтверждённые кодом, маркетинговый тон и эмодзи, лишние абстракции, хрупкая обработка ошибок, персональные данные в логах, несуществующие классы стилей. Использовать, когда просят проверить код или репозиторий «на слоп», подготовить проект к публикации или показу, сделать ревью кода, написанного с помощью ИИ, или перед тем как закоммитить собственную большую правку.

rbikbov/frontend-starter

Процесс разработки фичи с агентом: PRD с Non-Goals → компактная спецификация → задачи по одной на коммит → read-only исследование → tracer bullet → сборка с тестом сначала → независимое ревью субагентом → проверка в браузере. Использовать, когда просят сделать фичу, экран, пользовательский сценарий или заметное изменение поведения; когда нужно написать PRD, спецификацию или разбить работу на задачи; когда работа больше одного коммита. Не использовать для опечаток и однострочных правок.

rbikbov/frontend-starter

Старт нового фронтенд-проекта на Next.js (App Router) + TypeScript: фундамент, который закладывается до первой фичи. Использовать, когда нужно: создать проект с нуля; решить, нужны ли Next.js, монорепо, BFF, микрофронтенды; разложить FSD поверх роутинга Next; завести строгий TS, линтер границ, env-конфиг, модель ошибок и API-клиент, тесты, git-хуки, CI, каркас README; навести порядок в молодом проекте после генератора.

관련 스킬