paint-brush
최신 Next.js 기능과 함께 Firebase 인증 사용~에 의해@awinogrodzki
23,089 판독값
23,089 판독값

최신 Next.js 기능과 함께 Firebase 인증 사용

~에 의해 Amadeusz Winogrodzki32m2024/04/04
Read on Terminal Reader

너무 오래; 읽다

번들 크기가 0인 `next-firebase-auth-edge` 라이브러리를 사용하여 Firebase 인증을 Next.js와 통합하는 방법에 대한 포괄적인 단계별 가이드입니다. 여기에는 원활한 사용자 경험을 위한 리디렉션 논리와 함께 사용자 등록, 로그인 및 로그아웃 기능을 위한 단계가 포함되어 있습니다. 이 가이드에서는 Firebase 인증을 앱 라우터, 미들웨어, 서버 구성 요소 등의 최신 Next.js 기능과 통합하는 방법을 알아봅니다. Vercel에 앱을 배포하는 방법에 대한 지침으로 마무리되며, Firebase 인증을 통해 Next.js 애플리케이션을 향상하려는 개발자를 위한 라이브러리의 사용 편의성과 미래 지향적인 디자인을 보여줍니다.
featured image - 최신 Next.js 기능과 함께 Firebase 인증 사용
Amadeusz Winogrodzki HackerNoon profile picture
0-item
1-item

next-firebase-auth-edge 소개

기존 또는 새 Next.js 애플리케이션에 Firebase 인증을 추가하는 방법을 검색하는 동안 이 문서를 발견했을 것입니다. 귀하는 앱이 성공할 가능성을 극대화할 수 있는 현명하고 공정하며 미래 지향적인 결정을 내리는 것을 목표로 합니다. next-firebase-auth-edge의 창시자로서 저는 완전히 편견 없는 의견을 제공하는 것이 제 장점이 아니라는 점을 인정해야 합니다. 하지만 적어도 라이브러리를 디자인할 때 제가 취한 접근 방식을 정당화하려고 노력하겠습니다. 이 가이드가 끝날 때쯤에는 이 접근 방식이 간단하고 장기적으로 실행 가능하다는 것을 알게 되기를 바랍니다.


어떻게 시작됐나

긴 소개는 생략하겠습니다. 도서관에 대한 아이디어는 귀하와 비슷한 상황에서 영감을 받았다고 말씀드리고 싶습니다. Next.js가 App Router 의 카나리아 버전을 출시했을 때였습니다. 저는 재작성 및 내부 리디렉션에 크게 의존하는 앱을 작업하고 있었습니다. 이를 위해 우리는 사용자 정의 Node.js express 서버 렌더링 Next.js 앱을 사용했습니다.


우리는 App RouterServer Components 에 대해 정말 기대하고 있었지만 이것이 우리의 맞춤형 서버와 호환되지 않을 것이라는 점을 알고 있었습니다. 미들웨어는 사용자 정의 Express 서버의 필요성을 없애기 위해 활용할 수 있는 강력한 기능처럼 보였고 대신 Next.js의 내장 기능에만 의존하여 사용자를 다른 페이지로 동적으로 리디렉션하고 다시 작성하는 것을 선택했습니다.


당시 우리는 next-firebase-auth 를 사용하고 있었습니다. 우리는 라이브러리가 정말 마음에 들었지만 레거시로 간주될 next.config.js , pages/_app.tsx , pages/api/login.ts , pages/api/logout.ts 파일을 통해 인증 로직을 확장했습니다. 곧. 또한 라이브러리가 미들웨어와 호환되지 않아 URL을 다시 작성하거나 컨텍스트에 따라 사용자를 리디렉션할 수 없었습니다.


그래서 검색을 시작했는데 놀랍게도 미들웨어 내에서 Firebase 인증을 지원하는 라이브러리를 찾을 수 없었습니다. – 왜 그럴 수 있나요? 그것은 불가능! Node.js와 React에서 11년 이상의 상업적 경험을 가진 소프트웨어 엔지니어로서 저는 이 난제를 해결하기 위해 준비하고 있었습니다.


그래서 시작했습니다. 그리고 대답은 분명해졌습니다. 미들웨어는 Edge Runtime 내에서 실행됩니다. Edge Runtime 내에서 사용 가능한 Web Crypto API 와 호환되는 Firebase 라이브러리는 없습니다. 나는 운명을 정했다 . 나는 무력감을 느꼈다. 새롭고 멋진 API를 사용하기 위해 실제로 기다려야 하는 것은 이번이 처음입니까? - 아니요. 지켜보는 냄비는 결코 끓지 않습니다. 나는 재빨리 흐느낌을 멈추고 next-firebase-auth , firebase-admin 및 기타 여러 JWT 인증 라이브러리를 리버스 엔지니어링하여 Edge 런타임에 맞게 조정하기 시작했습니다. 저는 가장 가볍고, 구성하기 쉽고, 미래 지향적인 인증 라이브러리를 만드는 것을 목표로 이전 인증 라이브러리에서 겪었던 모든 문제를 해결할 기회를 잡았습니다.


약 2주 후에 next-firebase-auth-edge 버전 0.0.1 이 탄생했습니다. 이는 확실한 개념 증명이었지만 버전 0.0.1 사용하고 싶지는 않을 것입니다. 날 믿어.


어떻게 지내요

거의 2년 후 , 저는 372개의 커밋 , 110개의 문제 해결 , 전 세계의 훌륭한 개발자들로부터 귀중한 피드백을 받은 후 라이브러리가 제 또 다른 자아가 저에게 승인을 받는 단계에 도달했음을 발표하게 되어 기쁩니다.



나의 또 다른 자아



이 가이드에서는 next-firebase-auth-edge 버전 1.4.1을 사용하여 인증된 Next.js 앱을 처음부터 생성하겠습니다. 새로운 Firebase 프로젝트 및 Next.js 앱 생성부터 시작하여 next-firebase-auth-edgefirebase/auth 라이브러리와의 통합까지 각 단계를 자세히 살펴보겠습니다. 이 튜토리얼이 끝나면 Vercel에 앱을 배포하여 모든 것이 로컬 및 프로덕션 준비 환경에서 모두 작동하는지 확인합니다.


Firebase 설정

이 부분에서는 아직 Firebase 인증을 설정하지 않았다고 가정합니다. 그렇지 않은 경우 다음 부분으로 건너뛰셔도 됩니다.


Firebase 콘솔 로 이동하여 프로젝트를 생성해 보겠습니다.


프로젝트가 생성되면 Firebase 인증을 활성화해 보겠습니다. 콘솔을 열고 빌드 > 인증 > 로그인 방법을 따르고 이메일 및 비밀번호 방법을 활성화하십시오. 이것이 우리 앱에서 지원할 방법입니다.


이메일/비밀번호 로그인 방법 활성화


첫 번째 로그인 방법을 활성화한 후에는 프로젝트에 대해 Firebase 인증이 활성화되어야 하며 프로젝트 설정 에서 웹 API 키를 검색할 수 있습니다.


웹 API 키 검색


API 키를 복사하여 안전하게 보관하세요. 이제 다음 탭인 Cloud Messaging을 열고 Sender ID를 적어 두겠습니다. 나중에 필요할 거예요.


보낸 사람 ID 검색


마지막으로 서비스 계정 자격 증명을 생성해야 합니다. 이를 통해 앱은 Firebase 서비스에 대한 전체 액세스 권한을 얻을 수 있습니다. 프로젝트 설정 > 서비스 계정 으로 이동하여 새 개인 키 생성을 클릭합니다. 그러면 서비스 계정 자격 증명이 포함된 .json 파일이 다운로드됩니다. 이 파일을 알려진 위치에 저장하십시오.



그게 다야! Next.js 앱을 Firebase 인증과 통합할 준비가 되었습니다.

처음부터 Next.js 앱 만들기

이 가이드에서는 Node.jsnpm이 설치되어 있다고 가정합니다. 이 튜토리얼에 사용된 명령은 최신 LTS Node.js v20 에 대해 검증되었습니다. 터미널에서 node -v 실행하여 노드 버전을 확인할 수 있습니다. NVM 과 같은 도구를 사용하여 Node.js 버전 간에 빠르게 전환할 수도 있습니다.

CLI를 사용하여 Next.js 앱 설정

즐겨찾는 터미널을 열고 프로젝트 폴더로 이동하여 실행하세요.

 npx create-next-app@latest


간단하게 유지하기 위해 기본 구성을 사용해 보겠습니다. 이는 TypeScripttailwind 사용한다는 의미입니다.

 ✔ What is your project named? … my-app ✔ Would you like to use TypeScript? … Yes ✔ Would you like to use ESLint? … Yes ✔ Would you like to use Tailwind CSS? … Yes ✔ Would you like to use `src/` directory? … No ✔ Would you like to use App Router? (recommended) … Yes ✔ Would you like to customize the default import alias (@/*)? … No


프로젝트의 루트 디렉토리로 이동하여 모든 종속성이 설치되어 있는지 확인하겠습니다.

 cd my-app npm install


모든 것이 예상대로 작동하는지 확인하기 위해 npm run dev 명령을 사용하여 Next.js 개발 서버를 시작해 보겠습니다. http://localhost:3000 을 열면 다음과 유사한 Next.js 시작 페이지가 표시됩니다.


Next.js 시작 페이지

환경변수 준비

Firebase와의 통합을 시작하기 전에 Firebase 구성을 저장하고 읽을 수 있는 안전한 방법이 필요합니다. 다행히 Next.js에는 dotenv 지원이 내장되어 있습니다.


즐겨 사용하는 코드 편집기를 열고 프로젝트 폴더로 이동하세요.


프로젝트의 루트 디렉터리에 .env.local 파일을 만들고 다음 환경 변수로 채워보겠습니다.


 FIREBASE_ADMIN_CLIENT_EMAIL=... FIREBASE_ADMIN_PRIVATE_KEY=... AUTH_COOKIE_NAME=AuthToken AUTH_COOKIE_SIGNATURE_KEY_CURRENT=secret1 AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS=secret2 USE_SECURE_COOKIES=false NEXT_PUBLIC_FIREBASE_PROJECT_ID=... NEXT_PUBLIC_FIREBASE_API_KEY=AIza... NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=....firebaseapp.com NEXT_PUBLIC_FIREBASE_DATABASE_URL=....firebaseio.com NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=...


NEXT_PUBLIC_ 접두사가 붙은 변수는 클라이언트측 번들에서 사용할 수 있습니다. Firebase 인증 클라이언트 SDK를 설정하려면 이 정보가 필요합니다.


NEXT_PUBLIC_FIREBASE_PROJECT_ID , FIREBASE_ADMIN_CLIENT_EMAILFIREBASE_ADMIN_PRIVATE_KEY 는 서비스 계정 자격 증명을 생성한 후 다운로드한 .json 파일에서 검색할 수 있습니다.


AUTH_COOKIE_NAME 사용자 자격 증명을 저장하는 데 사용되는 쿠키의 이름입니다.

AUTH_COOKIE_SIGNATURE_KEY_CURRENTAUTH_COOKIE_SIGNATURE_KEY_PREVIOUS 는 자격 증명에 서명할 비밀입니다.


NEXT_PUBLIC_FIREBASE_API_KEY프로젝트 설정 일반 페이지에서 검색된 웹 API 키 입니다.

NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN프로젝트 ID .firebaseapp.com입니다.

NEXT_PUBLIC_FIREBASE_DATABASE_URL프로젝트 ID .firebaseio.com입니다.

NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID프로젝트 설정 > 클라우드 메시징 페이지에서 얻을 수 있습니다.


USE_SECURE_COOKIES 로컬 개발에 사용되지 않지만 Vercel에 앱을 배포할 때 유용합니다.

Firebase 인증과 통합

next-firebase-auth-edge 설치 및 초기 구성

npm install next-firebase-auth-edge@^1.4.1 실행하여 프로젝트의 종속성에 라이브러리를 추가합니다.


프로젝트 구성을 캡슐화하기 위해 config.ts 파일을 만들어 보겠습니다. 필수는 아니지만 코드 예제를 더 읽기 쉽게 만듭니다.

이러한 가치를 숙고하는 데 너무 많은 시간을 소비하지 마십시오. 계속 진행하면서 이에 대해 더 자세히 설명하겠습니다.


 export const serverConfig = { cookieName: process.env.AUTH_COOKIE_NAME!, cookieSignatureKeys: [process.env.AUTH_COOKIE_SIGNATURE_KEY_CURRENT!, process.env.AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS!], cookieSerializeOptions: { path: "/", httpOnly: true, secure: process.env.USE_SECURE_COOKIES === "true", sameSite: "lax" as const, maxAge: 12 * 60 * 60 * 24, }, serviceAccount: { projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!, clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL!, privateKey: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, "\n")!, } }; export const clientConfig = { projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!, authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID };


미들웨어 추가

프로젝트 루트에 middleware.ts 파일을 생성하고 다음을 붙여넣습니다.

 import { NextRequest } from "next/server"; import { authMiddleware } from "next-firebase-auth-edge"; import { clientConfig, serverConfig } from "./config"; export async function middleware(request: NextRequest) { return authMiddleware(request, { loginPath: "/api/login", logoutPath: "/api/logout", apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, cookieSerializeOptions: serverConfig.cookieSerializeOptions, serviceAccount: serverConfig.serviceAccount, }); } export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };


믿거나 말거나, 우리는 앱 서버를 Firebase 인증과 통합했습니다. 실제로 사용하기 전에 구성을 조금 설명하겠습니다.


loginPath authMiddleware에 GET /api/login 엔드포인트를 노출하도록 지시합니다. Authorization: Bearer ${idToken} * 헤더를 사용하여 이 엔드포인트를 호출하면 서명된 사용자 지정 및 새로 고침 토큰이 포함된 HTTP(S) 전용 Set-Cookie 헤더로 응답합니다.


* idToken Firebase 클라이언트 SDK 에서 제공되는 getIdToken 함수를 통해 검색됩니다. 이에 대해서는 나중에 자세히 설명합니다.


마찬가지로 logoutPath 미들웨어에 GET /api/logout 노출하도록 지시하지만 추가 헤더는 필요하지 않습니다. 호출되면 브라우저에서 인증 쿠키를 제거합니다.


apiKey 는 웹 API 키입니다. 미들웨어는 이를 사용하여 사용자 정의 토큰을 새로 고치고 자격 증명이 만료된 후 인증 쿠키를 재설정합니다.


cookieName /api/login/api/logout 엔드포인트에 의해 설정되고 제거되는 쿠키의 이름입니다.


cookieSignatureKeys 사용자 자격 증명에 서명되는 비밀 키 목록입니다. 자격 증명은 항상 목록의 첫 번째 키로 서명되므로 최소한 하나의 값을 제공해야 합니다. 키 순환을 수행하기 위해 여러 키를 제공할 수 있습니다.


cookieSerializeOptions Set-Cookie 헤더를 생성할 때 쿠키 에 전달되는 옵션입니다. 자세한 내용은 쿠키 README를 참조하세요.


serviceAccount Firebase 서비스를 사용하도록 라이브러리를 승인합니다.


매처는 Next.js 서버에 /api/login , /api/logout , / 및 파일이나 API 호출이 아닌 다른 경로에 대해 미들웨어를 실행하도록 지시합니다.

 export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };


모든 /api/* 호출 에 대해 미들웨어를 활성화하지 않는 이유가 궁금할 것입니다 . 그럴 수도 있지만 API 경로 처리기 자체 내에서 인증되지 않은 호출을 처리하는 것이 좋습니다. 이는 이 튜토리얼의 범위를 약간 벗어나지만 관심이 있으시면 알려주세요. 몇 가지 예를 준비하겠습니다!



보시다시피 구성은 최소화 되어 있으며 목적이 명확하게 정의되어 있습니다. 이제 /api/login/api/logout 엔드포인트 호출을 시작하겠습니다.


안전한 홈페이지 만들기

최대한 간단하게 만들기 위해 기본 Next.js 홈 페이지를 지우고 일부 개인화된 콘텐츠로 바꾸겠습니다.


./app/page.tsx 열고 다음을 붙여넣습니다.

 import { getTokens } from "next-firebase-auth-edge"; import { cookies } from "next/headers"; import { notFound } from "next/navigation"; import { clientConfig, serverConfig } from "../config"; export default async function Home() { const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, }); if (!tokens) { notFound(); } return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-xl mb-4">Super secure home page</h1> <p> Only <strong>{tokens?.decodedToken.email}</strong> holds the magic key to this kingdom! </p> </main> ); }


이것을 조금씩 분해해 봅시다.


getTokens 기능은 쿠키 에서 사용자 자격 증명을 검증하고 추출하도록 설계되었습니다.

 const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, });


사용자가 인증되지 않았거나 두 가지 속성을 포함하는 객체인 경우 null 로 해결됩니다.


외부 백엔드 서비스에 대한 API 요청을 승인하는 데 사용할 수 있는 idToken stringtoken 입니다. 이는 범위를 약간 벗어나지만 라이브러리가 분산 서비스 아키텍처를 가능하게 한다는 점은 언급할 가치가 있습니다. token 은 호환되며 모든 플랫폼의 모든 공식 Firebase 라이브러리와 함께 사용할 수 있습니다.


이름에서 알 수 있듯이 decodedToken 이메일 주소, 프로필 사진, 사용자 정의 클레임을 포함하여 사용자를 식별하는 데 필요한 모든 정보가 포함된 token 의 디코딩된 버전으로, 역할과 권한에 따라 액세스를 추가로 제한할 수 있습니다.


tokens 얻은 후 next/navigation 에서 notFound 기능을 사용하여 인증된 사용자만 페이지에 액세스할 수 있는지 확인합니다.

 if (!tokens) { notFound(); }


마지막으로 몇 가지 기본적이고 개인화된 사용자 콘텐츠를 렌더링합니다.

 <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-xl mb-4">Super secure home page</h1> <p> Only <strong>{tokens?.decodedToken.email}</strong> holds the magic key to this kingdom!" </p> </main>


실행해 봅시다.

개발 서버를 닫은 경우 npm run dev 실행하세요.


http://localhost:3000/ 에 액세스하려고 하면 404: 이 페이지를 찾을 수 없습니다.가 표시되어야 합니다.


성공! 우리는 엿보는 눈으로부터 우리의 비밀을 안전하게 지켰습니다!


firebase 설치 및 Firebase 클라이언트 SDK 초기화

프로젝트의 루트 디렉터리에서 npm install firebase 실행하세요.


클라이언트 SDK 설치 후 프로젝트 루트 디렉터리에 firebase.ts 파일을 생성하고 다음 내용을 붙여넣습니다.

 import { initializeApp } from 'firebase/app'; import { clientConfig } from './config'; export const app = initializeApp(clientConfig);


그러면 Firebase 클라이언트 SDK가 초기화되고 클라이언트 구성요소에 대한 앱 개체가 노출됩니다.

등록 페이지 만들기

아무도 볼 수 없다면 매우 안전한 홈 페이지가 무슨 의미가 있을까요? 사람들이 우리 앱에 들어갈 수 있도록 간단한 등록 페이지를 만들어 보겠습니다.


./app/register/page.tsx 아래에 새롭고 멋진 페이지를 만들어 보겠습니다.


 "use client"; import { FormEvent, useState } from "react"; import Link from "next/link"; import { getAuth, createUserWithEmailAndPassword } from "firebase/auth"; import { app } from "../../firebase"; import { useRouter } from "next/navigation"; export default function Register() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [error, setError] = useState(""); const router = useRouter(); async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); if (password !== confirmation) { setError("Passwords don't match"); return; } try { await createUserWithEmailAndPassword(getAuth(app), email, password); router.push("/login"); } catch (e) { setError((e as Error).message); } } return ( <main className="flex min-h-screen flex-col items-center justify-center p-8"> <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700"> <div className="p-6 space-y-4 md:space-y-6 sm:p-8"> <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white"> Pray tell, who be this gallant soul seeking entry to mine humble abode? </h1> <form onSubmit={handleSubmit} className="space-y-4 md:space-y-6" action="#" > <div> <label htmlFor="email" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Your email </label> <input type="email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} id="email" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="[email protected]" required /> </div> <div> <label htmlFor="password" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Password </label> <input type="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required /> </div> <div> <label htmlFor="confirm-password" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Confirm password </label> <input type="password" name="confirm-password" value={confirmation} onChange={(e) => setConfirmation(e.target.value)} id="confirm-password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required /> </div> {error && ( <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert" > <span className="block sm:inline">{error}</span> </div> )} <button type="submit" className="w-full text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800" > Create an account </button> <p className="text-sm font-light text-gray-500 dark:text-gray-400"> Already have an account?{" "} <Link href="/login" className="font-medium text-gray-600 hover:underline dark:text-gray-500" > Login here </Link> </p> </form> </div> </div> </main> ); }


알아요. 텍스트가 많지만 양해해 주세요.


우리는 "use client"; 등록 페이지가 클라이언트 측 API를 사용할 것임을 나타냅니다.


 const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [error, setError] = useState("");

그런 다음 양식 상태를 유지하기 위해 몇 가지 변수와 설정자를 정의합니다.


 const router = useRouter(); async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); if (password !== confirmation) { setError("Passwords don't match"); return; } try { await createUserWithEmailAndPassword(getAuth(app), email, password); router.push("/login"); } catch (e) { setError((e as Error).message); } }

여기서는 양식 제출 논리를 정의합니다. 먼저 passwordconfirmation 동일한지 확인하고, 그렇지 않으면 오류 상태를 업데이트합니다. 값이 유효하면 firebase/auth 에서 createUserWithEmailAndPassword 사용하여 사용자 계정을 만듭니다. 이 단계가 실패하면(예: 이메일을 가져옴) 오류를 업데이트하여 사용자에게 알립니다.


모든 것이 순조롭게 진행되면 사용자를 /login 페이지로 리디렉션합니다. 당신은 아마도 지금 혼란스러울 것이고, 그렇게 하는 것이 옳습니다. /login 페이지가 아직 존재하지 않습니다. 우리는 다음에 일어날 일을 준비하고 있습니다.


http://localhost:3000/register 를 방문하면 페이지는 대략 다음과 같아야 합니다.


등록 페이지


로그인 페이지 만들기

이제 사용자가 등록할 수 있으므로 신원을 증명하십시오.


./app/login/page.tsx 아래에 로그인 페이지를 만듭니다.


 "use client"; import { FormEvent, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; import { app } from "../../firebase"; export default function Login() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const router = useRouter(); async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); try { const credential = await signInWithEmailAndPassword( getAuth(app), email, password ); const idToken = await credential.user.getIdToken(); await fetch("/api/login", { headers: { Authorization: `Bearer ${idToken}`, }, }); router.push("/"); } catch (e) { setError((e as Error).message); } } return ( <main className="flex min-h-screen flex-col items-center justify-center p-8"> <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700"> <div className="p-6 space-y-4 md:space-y-6 sm:p-8"> <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white"> Speak thy secret word! </h1> <form onSubmit={handleSubmit} className="space-y-4 md:space-y-6" action="#" > <div> <label htmlFor="email" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Your email </label> <input type="email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} id="email" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="[email protected]" required /> </div> <div> <label htmlFor="password" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Password </label> <input type="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required /> </div> {error && ( <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert" > <span className="block sm:inline">{error}</span> </div> )} <button type="submit" className="w-full text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800" > Enter </button> <p className="text-sm font-light text-gray-500 dark:text-gray-400"> Don&apos;t have an account?{" "} <Link href="/register" className="font-medium text-gray-600 hover:underline dark:text-gray-500" > Register here </Link> </p> </form> </div> </div> </main> ); }


보시다시피 등록 페이지와 매우 유사합니다. 중요한 부분에 집중해 보겠습니다.

 async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); try { const credential = await signInWithEmailAndPassword( getAuth(app), email, password ); const idToken = await credential.user.getIdToken(); await fetch("/api/login", { headers: { Authorization: `Bearer ${idToken}`, }, }); router.push("/"); } catch (e) { setError((e as Error).message); } }


모든 마법이 일어나는 곳입니다. 우리는 firebase/auth 에서 signInEmailAndPassword 사용하여 사용자의 idToken 검색합니다.


그런 다음 미들웨어에 의해 노출된 /api/login 엔드포인트를 호출합니다. 이 엔드포인트는 사용자 자격 증명으로 브라우저 쿠키를 업데이트합니다.


마지막으로, router.push("/");


로그인 페이지는 대략 다음과 같습니다.



테스트해보자!


http://localhost:3000/register 로 이동하여 임의의 이메일 주소와 비밀번호를 입력하여 계정을 만드세요. http://localhost:3000/login 페이지에서 해당 자격 증명을 사용하십시오. Enter를 클릭하면 매우 안전한 홈페이지 로 리디렉션됩니다.


매우 안전한 홈페이지




우리는 마침내 우리 자신의 개인 적이고 매우 안전한 홈 페이지를 보게 되었습니다! 하지만 기다려! 어떻게 나가나요?


영원히(또는 12일) 세상으로부터 자신을 잠그지 않으려면 로그아웃 버튼을 추가해야 합니다.


시작하기 전에 Firebase 클라이언트 SDK를 사용하여 로그아웃할 수 있는 클라이언트 구성요소를 생성해야 합니다.


./app/HomePage.tsx 아래에 새 파일을 생성해 보겠습니다.

 "use client"; import { useRouter } from "next/navigation"; import { getAuth, signOut } from "firebase/auth"; import { app } from "../firebase"; interface HomePageProps { email?: string; } export default function HomePage({ email }: HomePageProps) { const router = useRouter(); async function handleLogout() { await signOut(getAuth(app)); await fetch("/api/logout"); router.push("/login"); } return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-xl mb-4">Super secure home page</h1> <p className="mb-8"> Only <strong>{email}</strong> holds the magic key to this kingdom! </p> <button onClick={handleLogout} className="text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800" > Logout </button> </main> ); }


눈치채셨겠지만 이는 ./app/page.tsx 의 약간 수정된 버전입니다. getTokens 서버 구성 요소API 경로 핸들러 내에서만 작동하고 signOutuseRouter 클라이언트 컨텍스트에서 실행되어야 하기 때문에 별도의 클라이언트 구성 요소를 만들어야 했습니다. 조금 복잡하다는 것은 알지만 실제로는 매우 강력합니다. 나중에 설명하겠습니다.


로그아웃 프로세스에 집중해 보겠습니다.

 const router = useRouter(); async function handleLogout() { await signOut(getAuth(app)); await fetch("/api/logout"); router.push("/login"); }


먼저 Firebase 클라이언트 SDK에서 로그아웃합니다. 그런 다음 미들웨어에 의해 노출된 /api/logout 엔드포인트를 호출합니다. 사용자를 /login 페이지로 리디렉션하여 마무리합니다.


서버 홈 페이지를 업데이트해 보겠습니다. ./app/page.tsx 로 이동하여 다음을 붙여넣습니다.

 import { getTokens } from "next-firebase-auth-edge"; import { cookies } from "next/headers"; import { notFound } from "next/navigation"; import { clientConfig, serverConfig } from "../config"; import HomePage from "./HomePage"; export default async function Home() { const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, }); if (!tokens) { notFound(); } return <HomePage email={tokens?.decodedToken.email} />; }


이제 Home 서버 구성 요소는 사용자 토큰을 가져와 HomePage 클라이언트 구성 요소에 전달하는 역할만 담당합니다. 이것은 실제로 매우 일반적이고 유용한 패턴입니다.


이것을 테스트해보자:


짜잔! 이제 우리는 우리 마음대로 애플리케이션에 로그인하고 로그아웃할 수 있습니다. 저건 완벽 해!

아니면 그럴까요?


인증되지 않은 사용자가 http://localhost:3000/을 열어 홈 페이지에 들어가려고 하면 404: 이 페이지를 찾을 수 없습니다.

또한 인증된 사용자는 로그아웃하지 않고도 http://localhost:3000/registerhttp://localhost:3000/login 페이지에 계속 액세스할 수 있습니다.


우리는 더 잘할 수 있습니다.


일부 리디렉션 논리를 추가해야 할 것 같습니다. 몇 가지 규칙을 정의해 보겠습니다.

  • 인증된 사용자가 /register/login 페이지에 액세스하려고 하면 / 로 리디렉션해야 합니다.
  • 인증되지 않은 사용자가 / 페이지에 액세스하려고 하면 /login 으로 리디렉션해야 합니다.


미들웨어는 Next.js 앱에서 리디렉션을 처리하는 가장 좋은 방법 중 하나입니다. 다행히 authMiddleware 광범위한 리디렉션 시나리오를 처리할 수 있는 다양한 옵션과 도우미 기능을 지원합니다.


middleware.ts 파일을 열고 업데이트된 버전을 붙여넣으세요.

 import { NextRequest, NextResponse } from "next/server"; import { authMiddleware, redirectToHome, redirectToLogin } from "next-firebase-auth-edge"; import { clientConfig, serverConfig } from "./config"; const PUBLIC_PATHS = ['/register', '/login']; export async function middleware(request: NextRequest) { return authMiddleware(request, { loginPath: "/api/login", logoutPath: "/api/logout", apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, cookieSerializeOptions: serverConfig.cookieSerializeOptions, serviceAccount: serverConfig.serviceAccount, handleValidToken: async ({token, decodedToken}, headers) => { if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) { return redirectToHome(request); } return NextResponse.next({ request: { headers } }); }, handleInvalidToken: async (reason) => { console.info('Missing or malformed credentials', {reason}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); }, handleError: async (error) => { console.error('Unhandled authentication error', {error}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); } }); } export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };


그게 다야. 우리는 모든 리디렉션 규칙을 구현했습니다. 이것을 분석해보자.


 const PUBLIC_PATHS = ['/register', '/login'];


 handleValidToken: async ({token, decodedToken}, headers) => { if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) { return redirectToHome(request); } return NextResponse.next({ request: { headers } }); },

handleValidToken 유효한 사용자 자격 증명이 요청에 첨부될 때 호출됩니다. 사용자가 인증되었습니다. tokens 개체를 첫 번째로, 수정된 요청 헤더를 두 번째 인수로 사용하여 호출됩니다. NextResponse 로 해결되어야 합니다.


next-firebase-auth-edgeredirectToHome NextResponse.redirect(new URL(“/“)) 로 단순화할 수 있는 객체를 반환하는 도우미 함수입니다.


PUBLIC_PATHS.includes(request.nextUrl.pathname) 확인하여 인증된 사용자가 /login 또는 /register 페이지에 액세스하려고 시도하는지 확인하고, 그러한 경우 홈으로 리디렉션합니다.



 handleInvalidToken: async (reason) => { console.info('Missing or malformed credentials', {reason}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); },


handleInvalidToken 예상된 일이 발생하면 호출됩니다. 예상되는 이벤트 중 하나는 사용자가 처음으로, 다른 기기에서 또는 자격 증명이 만료된 후에 앱을 보는 것입니다.


인증되지 않은 사용자에 대해 handleInvalidToken 호출된다는 사실을 알고 나면 두 번째 규칙을 진행할 수 있습니다. 인증되지 않은 사용자가 / 페이지 에 액세스하려고 하면 해당 사용자를 /login 으로 리디렉션해야 합니다.


충족해야 할 다른 조건이 없기 때문에 우리는 NextResponse.redirect(new URL(“/login”)) 으로 단순화할 수 있는 redirectToLogin 결과를 반환합니다. 또한 사용자가 리디렉션 루프에 빠지지 않도록 합니다.


마지막으로,

 handleError: async (error) => { console.error('Unhandled authentication error', {error}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); }


handleInvalidToken 과 달리, 예상치 못한* 일이 발생하여 조사가 필요할 때 handleError 호출됩니다. 문서 에서 해당 설명과 함께 가능한 오류 목록을 찾을 수 있습니다.


오류가 발생하면 이 사실을 기록하고 사용자를 로그인 페이지로 안전하게 리디렉션합니다.


* Google 공개 키가 업데이트된 후 INVALID_ARGUMENT 오류와 함께 handleError 호출될 수 있습니다.

이는 키 순환의 한 형태이며 예상 됩니다. 자세한 내용은 이 Github 문제를 참조하세요.


이제 그게 다입니다. 마지막으로.


웹 앱에서 로그아웃하고 http://localhost:3000/ 을 엽니다. /login 페이지로 리디렉션되어야 합니다.

다시 로그인하고 http://localhost:3000/login 을 입력해 보겠습니다. / 페이지로 리디렉션되어야 합니다.


우리는 원활한 사용자 경험을 제공했을 뿐만 아니라 next-firebase-auth-edge 앱 서버에서만 작동하고 추가 클라이언트 측 코드를 도입하지 않는 번들 크기가 없는 라이브러리입니다. 결과 번들은 정말 최소한 입니다. 그것이 내가 완벽하다고 부르는 것입니다.


이제 우리 앱은 서버 및 클라이언트 구성 요소 모두에서 Firebase 인증과 완전히 통합되었습니다. 우리는 Next.js의 잠재력을 최대한 발휘할 준비가 되었습니다!




앱의 소스 코드는 next-firebase-auth-edge/examples/next-typescript-minimal 에서 찾을 수 있습니다.


발문

이 가이드에서는 새로운 Next.js 앱을 Firebase 인증과 통합하는 방법을 살펴보았습니다.


상당히 광범위하기는 하지만 이 기사에서는 비밀번호 재설정 양식이나 이메일 및 비밀번호 이외의 로그인 방법과 같은 인증 흐름의 일부 중요한 부분을 생략했습니다.


라이브러리에 관심이 있다면 본격적인 next-firebase-auth-edge 시작 데모 페이지를 미리 볼 수 있습니다.

Firestore 통합 , 서버 작업 , 앱 체크 지원 등을 제공합니다.


라이브러리는 수많은 예제가 포함된 전용 문서 페이지를 제공합니다.


기사가 마음에 드셨다면 next-firebase-auth-edge 저장소를 추천해 주시면 감사하겠습니다. 건배! 🎉



보너스 – Vercel에 앱 배포

이 보너스 가이드는 Next.js 앱을 Vercel에 배포하는 방법을 알려줍니다.

Git 저장소 생성

Vercel에 배포하려면 새 앱을 위한 저장소를 생성해야 합니다.


https://github.com/ 으로 이동하여 새 저장소를 만듭니다.


create-next-app 이미 로컬 git 저장소를 시작했으므로 프로젝트의 루트 폴더로 이동하여 다음을 실행하면 됩니다.


 git add --all git commit -m "first commit" git branch -M main git remote add origin [email protected]:path-to-your-new-github-repository.git git push -u origin main


새로운 Vercel 프로젝트 추가

https://vercel.com/ 으로 이동하여 Github 계정으로 로그인하세요.


로그인한 후 Vercel의 개요 페이지로 이동하여 새로 추가 > 프로젝트를 클릭하세요.


방금 생성한 Github 저장소 옆에 있는 가져오기를 클릭합니다. 아직 배포하지 마세요.


배포하기 전에 프로젝트 구성을 제공해야 합니다. 몇 가지 환경 변수를 추가해 보겠습니다.

Vercel에 배포


Vercel은 기본적으로 HTTPS를 사용하므로 USE_SECURE_COOKIES true 로 설정해야 합니다.


이제 배포를 클릭할 준비가 되었습니다.


1~2분 정도 기다리면 다음과 유사한 URL을 사용하여 앱에 액세스할 수 있습니다: https://next-typescript-minimal-xi.vercel.app/


완료. 나는 당신이 그것이 그렇게 쉬울 것이라고 기대하지 않았을 것이라고 확신합니다.




가이드가 마음에 드셨다면 next-firebase-auth-edge 저장소를 추천해 주시면 감사하겠습니다.


댓글로 피드백을 알려주실 수도 있습니다. 건배! 🎉