Firebase를 Next.js 앱과 통합

1. 시작하기 전에

이 Codelab에서는 식당 리뷰 웹사이트인 Friendly Eats라는 Next.js 웹 앱과 Firebase를 통합하는 방법을 알아봅니다.

FRIEND Eats 웹 앱

완성된 웹 앱은 Firebase가 Next.js 앱을 빌드하는 데 어떤 도움이 될 수 있는지 보여주는 유용한 기능을 제공합니다. 이러한 기능에는 다음과 같은 사항이 포함됩니다.

  • 자동 빌드 및 배포: 이 Codelab에서는 구성된 브랜치로 푸시할 때마다 Firebase App Hosting을 사용하여 Next.js 코드를 자동으로 빌드하고 배포합니다.
  • 로그인 및 로그아웃: 완성된 웹 앱을 사용하면 Google 계정으로 로그인 및 로그아웃할 수 있습니다. 사용자 로그인 및 지속성은 전적으로 Firebase 인증을 통해 관리됩니다.
  • 이미지: 완성된 웹 앱을 사용하면 로그인한 사용자가 음식점 이미지를 업로드할 수 있습니다. 이미지 애셋은 Firebase용 Cloud Storage에 저장됩니다. Firebase JavaScript SDK는 업로드된 이미지의 공개 URL을 제공합니다. 그러면 이 공개 URL이 Cloud Firestore의 관련 식당 문서에 저장됩니다.
  • 리뷰: 완성된 웹 앱을 통해 로그인한 사용자가 별표 평점과 텍스트 기반 메시지로 구성된 레스토랑 리뷰를 게시할 수 있습니다. 리뷰 정보는 Cloud Firestore에 저장됩니다.
  • 필터: 완성된 웹 앱을 통해 로그인한 사용자는 카테고리, 위치, 가격에 따라 레스토랑 목록을 필터링할 수 있습니다. 사용되는 정렬 방법을 맞춤설정할 수도 있습니다. Cloud Firestore에서 데이터에 액세스하며 사용된 필터를 기반으로 Firestore 쿼리가 적용됩니다.

기본 요건

  • GitHub 계정
  • Next.js 및 JavaScript에 관한 지식

학습할 내용

  • Next.js 앱 라우터 및 서버 측 렌더링과 함께 Firebase를 사용하는 방법
  • Firebase용 Cloud Storage에서 이미지를 유지하는 방법
  • Cloud Firestore 데이터베이스에서 데이터를 읽고 쓰는 방법
  • Firebase JavaScript SDK로 Google 계정으로 로그인을 사용하는 방법

필요한 사항

  • Git
  • Node.js의 최신 안정화 버전
  • 원하는 브라우저(예: Chrome)
  • 코드 편집기와 터미널이 있는 개발 환경
  • Firebase 프로젝트 생성 및 관리에 사용되는 Google 계정
  • Firebase 프로젝트를 Blaze 요금제로 업그레이드하는 기능

2. 개발 환경 및 GitHub 저장소 설정

이 Codelab은 앱의 시작 코드베이스를 제공하며 Firebase CLI를 사용합니다.

GitHub 저장소 만들기

Codelab 소스는 https://github.com/firebase/friendeats-web에서 확인할 수 있습니다. 저장소에는 여러 플랫폼용 샘플 프로젝트가 포함되어 있습니다. 그러나 이 Codelab에서는 nextjs-start 디렉터리만 사용합니다. 다음 디렉터리를 기록해 둡니다.

* `nextjs-start`: contains the starter code upon which you build.
* `nextjs-end`: contains the solution code for the finished web app.

nextjs-start 폴더를 자체 저장소에 복사합니다.

  1. 터미널을 사용하여 컴퓨터에 새 폴더를 만들고 새 디렉터리로 변경합니다.
    mkdir codelab-friendlyeats-web
    
    cd codelab-friendlyeats-web
    
  2. giget npm 패키지를 사용하여 nextjs-start 폴더만 가져옵니다.
    npx giget@latest gh:firebase/friendlyeats-web/nextjs-start#master . --install
    
  3. git:
    git init
    
    git commit -a -m "codelab starting point"
    
    git branch -M main
    
    을 사용하여 로컬에서 변경사항 추적
  4. 새 GitHub 저장소(https://github.com/new)를 만듭니다. 원하는 이름을 지정합니다.
    1. GitHub는 https://github.com//.git 또는 git@github.com:/.git과 같은 새 저장소 URL을 제공합니다. 이 URL을 복사합니다.
  5. 로컬 변경사항을 새 GitHub 저장소로 푸시합니다. 다음 명령어를 실행하여 자리표시자를 저장소 URL로 대체합니다.
    git remote add origin <your-repository-url>
    
    git push -u origin main
    
  6. 이제 GitHub 저장소에 시작 코드가 표시됩니다.

Firebase CLI 설치 또는 업데이트

다음 명령어를 실행하여 Firebase CLI가 설치되어 있고 v13.9.0 이상인지 확인합니다.

firebase --version

더 낮은 버전이 표시되거나 Firebase CLI가 설치되어 있지 않으면 설치 명령어를 실행합니다.

npm install -g firebase-tools@latest

권한 오류로 인해 Firebase CLI를 설치할 수 없는 경우 npm 문서를 참조하거나 다른 설치 옵션을 사용하세요.

Firebase에 로그인

  1. 다음 명령어를 실행하여 Firebase CLI에 로그인합니다.
    firebase login
    
  2. Firebase에서 데이터를 수집할지 여부에 따라 Y 또는 N을 입력합니다.
  3. 브라우저에서 Google 계정을 선택한 다음 허용을 클릭합니다.

3. Firebase 프로젝트 설정

이 섹션에서는 Firebase 프로젝트를 설정하고 Firebase 웹 앱을 이 프로젝트에 연결합니다. 또한 샘플 웹 앱에서 사용하는 Firebase 서비스를 설정합니다.

Firebase 프로젝트 만들기

  1. Firebase Console에서 프로젝트 추가를 클릭합니다.
  2. 프로젝트 이름 입력 텍스트 상자에 FriendlyEats Codelab(또는 원하는 프로젝트 이름)을 입력한 후 계속을 클릭합니다.
  3. Firebase 요금제 확인 모달에서 요금제가 Blaze인지 확인한 후 요금제 확인을 클릭합니다.
  4. 이 Codelab의 경우 Google 애널리틱스가 필요하지 않으므로 이 프로젝트에 Google 애널리틱스 사용 설정 옵션을 사용 중지합니다.
  5. 프로젝트 만들기를 클릭합니다.
  6. 프로젝트가 프로비저닝될 때까지 기다린 후 계속을 클릭합니다.
  7. Firebase 프로젝트에서 프로젝트 설정으로 이동합니다. 나중에 필요하므로 프로젝트 ID를 기록해 둡니다. 이 고유 식별자는 프로젝트를 식별하는 방법입니다(예: Firebase CLI에서).

Firebase 요금제 업그레이드

App Hosting을 사용하려면 Firebase 프로젝트에서 Blaze 요금제를 사용해야 합니다. 즉, Cloud Billing 계정과 연결되어 있어야 합니다.

  • Cloud Billing 계정에는 신용카드와 같은 결제 수단이 필요합니다.
  • Firebase와 Google Cloud를 처음 사용하는 경우 $300 크레딧과 무료 체험판 Cloud Billing 계정을 받을 자격이 되는지 확인하세요.

프로젝트를 Blaze 요금제로 업그레이드하려면 다음 단계를 따르세요.

  1. Firebase Console에서 요금제 업그레이드를 선택합니다.
  2. 대화상자에서 Blaze 요금제를 선택한 다음 화면에 표시된 안내에 따라 프로젝트를 Cloud Billing 계정과 연결합니다.
    Cloud Billing 계정을 만들어야 하는 경우 업그레이드를 완료하기 위해 Firebase Console의 업그레이드 흐름으로 돌아가야 할 수 있습니다.

Firebase 프로젝트에 웹 앱 추가

  1. Firebase 프로젝트에서 프로젝트 개요로 이동한 다음 e41f2efdd9539c31.png 을 클릭합니다.

    프로젝트에 이미 앱이 등록된 경우 앱 추가를 클릭하여 웹 아이콘을 표시합니다.
  2. 앱 닉네임 텍스트 상자에 My Next.js app과 같이 기억하기 쉬운 앱 닉네임을 입력합니다.
  3. 이 앱에 Firebase 호스팅도 설정 체크박스를 선택 해제된 상태로 둡니다.
  4. 앱 등록 > 다음 > 다음 > 콘솔로 이동을 클릭합니다.

Firebase Console에서 Firebase 서비스 설정

인증 설정

  1. Firebase Console에서 인증으로 이동합니다.
  2. 시작하기를 클릭합니다.
  3. 추가 제공업체 열에서 Google > 사용 설정을 클릭합니다.
  4. 프로젝트의 공개용 이름 텍스트 상자에 기억하기 쉬운 이름(예: My Next.js app)을 입력합니다.
  5. 프로젝트 지원 이메일 드롭다운에서 이메일 주소를 선택합니다.
  6. 저장을 클릭합니다.

Cloud Firestore 설정

  1. Firebase Console에서 Firestore로 이동합니다.
  2. 데이터베이스 만들기 > 다음 > 테스트 모드에서 시작 > 다음을 클릭합니다.
    이 Codelab의 후반부에서는 보안 규칙을 추가하여 데이터를 보호합니다. 데이터베이스에 대한 보안 규칙을 추가하지 않은 채 앱을 공개적으로 배포하거나 노출하지 마세요.
  3. 기본 위치를 사용하거나 원하는 위치를 선택합니다.
    실제 앱의 경우 사용자와 가까운 위치를 선택합니다. 이 위치는 나중에 변경할 수 없으며 자동으로 기본 Cloud Storage 버킷의 위치가 됩니다(다음 단계).
  4. 완료를 클릭합니다.

Firebase용 Cloud Storage 설정

  1. Firebase Console에서 스토리지로 이동합니다.
  2. 시작하기 > 테스트 모드에서 시작 > 다음을 클릭합니다.
    이 Codelab의 후반부에서 데이터를 보호하는 보안 규칙을 추가합니다. 스토리지 버킷에 대한 보안 규칙을 추가하지 않은 채 앱을 공개적으로 배포하거나 노출하지 마세요.
  3. 이전 단계에서 Firestore를 설정했기 때문에 버킷의 위치가 이미 선택되어 있어야 합니다.
  4. 완료를 클릭합니다.

4. 시작 코드베이스 검토

이 섹션에서는 이 Codelab에서 기능을 추가할 앱 시작 코드베이스의 몇 가지 영역을 검토합니다.

폴더 및 파일 구조

다음 표에는 앱의 폴더 및 파일 구조가 요약되어 있습니다.

폴더 및 파일

설명

src/components

필터, 헤더, 식당 세부정보, 리뷰에 관한 React 구성요소

src/lib

React 또는 Next.js에 바인딩되지 않은 유틸리티 함수

src/lib/firebase

Firebase 관련 코드 및 Firebase 구성

public

웹 앱의 정적 애셋(예: 아이콘)

src/app

Next.js 앱 라우터를 사용한 라우팅

src/app/restaurant

API 경로 핸들러

package.jsonpackage-lock.json

npm을 사용한 프로젝트 종속 항목

next.config.js

Next.js 관련 구성(서버 작업이 사용 설정됨)

jsconfig.json

JavaScript 언어 서비스 구성

서버 및 클라이언트 구성요소

앱이 앱 라우터를 사용하는 Next.js 웹 앱입니다. 서버 렌더링은 앱 전체에서 사용됩니다. 예를 들어 src/app/page.js 파일은 기본 페이지를 담당하는 서버 구성요소입니다. src/components/RestaurantListings.jsx 파일은 파일 시작 부분에 "use client" 지시어로 표시되는 클라이언트 구성요소입니다.

Import 문

다음과 같은 import 문을 확인할 수 있습니다.

import RatingPicker from "@/src/components/RatingPicker.jsx";

앱은 투박한 상대 가져오기 경로를 피하기 위해 @ 기호를 사용하며, 경로 별칭을 사용할 수 있습니다.

Firebase 관련 API

모든 Firebase API 코드는 src/lib/firebase 디렉터리에 래핑됩니다. 그러면 개별 React 구성요소는 Firebase 함수를 직접 가져오는 대신 src/lib/firebase 디렉터리에서 래핑된 함수를 가져옵니다.

모의 데이터

모의 레스토랑 및 리뷰 데이터는 src/lib/randomData.js 파일에 포함되어 있습니다. 이 파일의 데이터는 src/lib/fakeRestaurants.js 파일의 코드에 조합됩니다.

5. App Hosting 백엔드 만들기

이 섹션에서는 git 저장소의 브랜치를 감시하도록 App Hosting 백엔드를 설정합니다.

이 섹션을 마치면 App Hosting 백엔드가 GitHub의 저장소에 연결되어 main 브랜치에 새 커밋을 푸시할 때마다 앱의 새 버전을 자동으로 다시 빌드하고 출시합니다.

보안 규칙 배포

이 코드에는 이미 Firestore 및 Firebase용 Cloud Storage에 대한 보안 규칙 세트가 있습니다. 보안 규칙을 배포하면 데이터베이스와 버킷의 데이터를 오용으로부터 더욱 안전하게 보호할 수 있습니다.

  1. 터미널에서 이전에 만든 Firebase 프로젝트를 사용하도록 CLI를 구성합니다.
    firebase use --add
    
    별칭을 입력하라는 메시지가 표시되면 friendlyeats-codelab를 입력합니다.
  2. 이러한 보안 규칙을 배포하려면 터미널에서 다음 명령어를 실행합니다.
    firebase deploy --only firestore:rules,storage
    
  3. "Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?"라는 메시지가 표시되면 Enter를 눌러 를 선택합니다.

웹 앱 코드에 Firebase 구성 추가

  1. Firebase Console에서 프로젝트 설정으로 이동합니다.
  2. SDK 설정 및 구성 창에서 '앱 추가'를 클릭하고 코드 괄호 아이콘 을 클릭하여 새 웹 앱을 등록합니다.
  3. 웹 앱 만들기 과정이 끝나면 firebaseConfig 변수를 복사하고 그 속성과 값을 복사합니다.
  4. 코드 편집기에서 apphosting.yaml 파일을 열고 환경 변수 값에 Firebase Console의 구성 값을 입력합니다.
  5. 파일에서 기존 속성을 복사한 속성으로 바꿉니다.
  6. 파일을 저장합니다.

백엔드 만들기

  1. Firebase Console에서 앱 호스팅 페이지로 이동합니다.

&#39;시작하기&#39; 버튼이 있는 App Hosting 콘솔의 0 상태

  1. 백엔드 만들기 흐름을 시작하려면 '시작하기'를 클릭하세요. 백엔드를 다음과 같이 구성합니다.
  2. 첫 번째 단계의 안내에 따라 앞에서 만든 GitHub 저장소에 연결합니다.
  3. 배포 설정을 지정합니다.
    1. 루트 디렉터리를 /로 유지합니다.
    2. 라이브 브랜치를 main로 설정합니다.
    3. 자동 출시 사용 설정
  4. 백엔드 이름을 friendlyeats-codelab로 지정합니다.
  5. 'Firebase 웹 앱 만들기 또는 연결'의 '기존 Firebase 웹 앱 선택' 드롭다운에서 이전에 구성한 웹 앱을 선택합니다.
  6. '완료 및 배포'를 클릭합니다. 잠시 후 새 App Hosting 백엔드의 상태를 확인할 수 있는 새 페이지로 이동하게 됩니다.
  7. 출시가 완료되면 '도메인'에서 무료 도메인을 클릭합니다. DNS 전파로 인해 작업을 시작하는 데 몇 분 정도 걸릴 수 있습니다.

초기 웹 앱을 배포했습니다. GitHub 저장소의 main 브랜치에 새 커밋을 푸시할 때마다 Firebase Console에서 새 빌드와 출시가 시작되는 것을 확인할 수 있으며 출시가 완료되면 사이트가 자동으로 업데이트됩니다.

6. 웹 앱에 인증 추가

이 섹션에서는 로그인할 수 있도록 웹 앱에 인증을 추가합니다.

로그인 및 로그아웃 함수 구현

  1. src/lib/firebase/auth.js 파일에서 onAuthStateChanged, signInWithGoogle, signOut 함수를 다음 코드로 바꿉니다.
export function onAuthStateChanged(cb) {
	return _onAuthStateChanged(auth, cb);
}

export async function signInWithGoogle() {
  const provider = new GoogleAuthProvider();

  try {
    await signInWithPopup(auth, provider);
  } catch (error) {
    console.error("Error signing in with Google", error);
  }
}

export async function signOut() {
  try {
    return auth.signOut();
  } catch (error) {
    console.error("Error signing out with Google", error);
  }
}

이 코드는 다음 Firebase API를 사용합니다.

Firebase API

설명

GoogleAuthProvider

Google 인증 제공업체 인스턴스를 만듭니다.

signInWithPopup

대화상자 기반 인증 흐름을 시작합니다.

auth.signOut

사용자를 로그아웃시킵니다.

src/components/Header.jsx 파일에서 코드는 이미 signInWithGoogle 함수와 signOut 함수를 호출합니다.

  1. 'Google 인증 추가'라는 커밋 메시지와 함께 커밋을 만들고 GitHub 저장소에 푸시합니다. 1. Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
  2. 웹 앱에서 페이지를 새로고침하고 Google 계정으로 로그인을 클릭합니다. 웹 앱이 업데이트되지 않으므로 로그인이 성공했는지 확실하지 않습니다.

서버에 인증 상태 전송

인증 상태를 서버에 전달하기 위해 서비스 워커를 사용합니다. fetchWithFirebaseHeadersgetAuthIdToken 함수를 다음 코드로 바꿉니다.

async function fetchWithFirebaseHeaders(request) {
  const app = initializeApp(firebaseConfig);
  const auth = getAuth(app);
  const installations = getInstallations(app);
  const headers = new Headers(request.headers);
  const [authIdToken, installationToken] = await Promise.all([
    getAuthIdToken(auth),
    getToken(installations),
  ]);
  headers.append("Firebase-Instance-ID-Token", installationToken);
  if (authIdToken) headers.append("Authorization", `Bearer ${authIdToken}`);
  const newRequest = new Request(request, { headers });
  return await fetch(newRequest);
}

async function getAuthIdToken(auth) {
  await auth.authStateReady();
  if (!auth.currentUser) return;
  return await getIdToken(auth.currentUser);
}

서버에서 인증 상태 읽기

FirebaseServerApp을 사용하여 클라이언트의 인증 상태를 서버에서 미러링합니다.

src/lib/firebase/serverApp.js를 열고 getAuthenticatedAppForUser 함수를 바꿉니다.

export async function getAuthenticatedAppForUser() {
  const idToken = headers().get("Authorization")?.split("Bearer ")[1];
  console.log('firebaseConfig', JSON.stringify(firebaseConfig));
  const firebaseServerApp = initializeServerApp(
    firebaseConfig,
    idToken
      ? {
          authIdToken: idToken,
        }
      : {}
  );

  const auth = getAuth(firebaseServerApp);
  await auth.authStateReady();

  return { firebaseServerApp, currentUser: auth.currentUser };
}

인증 변경사항 구독

인증 변경사항을 구독하려면 다음 단계를 따르세요.

  1. src/components/Header.jsx 파일로 이동합니다.
  2. useUserSession 함수를 코드를 다음 코드로 바꿉니다.
function useUserSession(initialUser) {
	// The initialUser comes from the server via a server component
	const [user, setUser] = useState(initialUser);
	const router = useRouter();

	// Register the service worker that sends auth state back to server
	// The service worker is built with npm run build-service-worker
	useEffect(() => {
		if ("serviceWorker" in navigator) {
			const serializedFirebaseConfig = encodeURIComponent(JSON.stringify(firebaseConfig));
			const serviceWorkerUrl = `/auth-service-worker.js?firebaseConfig=${serializedFirebaseConfig}`
		
		  navigator.serviceWorker
			.register(serviceWorkerUrl)
			.then((registration) => console.log("scope is: ", registration.scope));
		}
	  }, []);

	useEffect(() => {
		const unsubscribe = onAuthStateChanged((authUser) => {
			setUser(authUser)
		})

		return () => unsubscribe()
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, []);

	useEffect(() => {
		onAuthStateChanged((authUser) => {
			if (user === undefined) return

			// refresh when user changed to ease testing
			if (user?.email !== authUser?.email) {
				router.refresh()
			}
		})
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [user])

	return user;
}

이 코드는 onAuthStateChanged 함수가 인증 상태가 변경되었다고 지정하면 React 상태 후크를 사용하여 사용자를 업데이트합니다.

변경사항 확인

src/app/layout.js 파일의 루트 레이아웃은 헤더를 렌더링하고 가능한 경우 사용자를 속성으로 전달합니다.

<Header initialUser={currentUser?.toJSON()} />

즉, <Header> 구성요소는 가능한 경우 서버 런타임 중에 사용자 데이터를 렌더링합니다. 초기 페이지 로드 후 페이지 수명 주기 동안 인증 업데이트가 있으면 onAuthStateChanged 핸들러가 이를 처리합니다.

이제 새 빌드를 출시하고 빌드한 내용을 확인할 차례입니다.

  1. '로그인 상태 표시' 커밋 메시지와 함께 커밋을 만들어 GitHub 저장소에 푸시합니다.
  2. Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
  3. 새 인증 동작을 확인합니다.
    1. 브라우저에서 웹 앱을 새로고침합니다. 표시 이름이 헤더에 표시됩니다.
    2. 로그아웃하고 다시 로그인하세요. 페이지는 페이지 새로고침 없이 실시간으로 업데이트됩니다. 사용자별로 이 단계를 반복할 수 있습니다.
    3. 선택사항: 웹 앱을 마우스 오른쪽 버튼으로 클릭하고 페이지 소스 보기를 선택한 후 표시 이름을 검색합니다. 서버에서 반환된 원시 HTML 소스에 표시됩니다.

7. 레스토랑 정보 보기

웹 앱에는 레스토랑 및 리뷰에 대한 모의 데이터가 포함되어 있습니다.

레스토랑 하나 이상 추가

로컬 Cloud Firestore 데이터베이스에 모의 레스토랑 데이터를 삽입하려면 다음 단계를 따르세요.

  1. 웹 앱에서 2cf67d488d8e6332.png > 샘플 식당 추가를 선택합니다.
  2. Firebase Console의 Firestore 데이터베이스 페이지에서 음식점을 선택합니다. 레스토랑 컬렉션에 표시되는 최상위 문서는 각각 레스토랑을 나타냅니다.
  3. 문서 몇 개를 클릭하여 레스토랑 문서의 속성을 살펴봅니다.

레스토랑 목록 표시

이제 Cloud Firestore 데이터베이스에 Next.js 웹 앱이 표시할 수 있는 레스토랑이 있습니다.

데이터 가져오기 코드를 정의하려면 다음 단계를 따르세요.

  1. src/app/page.js 파일에서 <Home /> 서버 구성요소를 찾고 서버 런타임에 레스토랑 목록을 검색하는 getRestaurants 함수 호출을 검토합니다. 다음 단계에서 getRestaurants 함수를 구현합니다.
  2. src/lib/firebase/firestore.js 파일에서 applyQueryFilters 함수와 getRestaurants 함수를 다음 코드로 바꿉니다.
function applyQueryFilters(q, { category, city, price, sort }) {
	if (category) {
		q = query(q, where("category", "==", category));
	}
	if (city) {
		q = query(q, where("city", "==", city));
	}
	if (price) {
		q = query(q, where("price", "==", price.length));
	}
	if (sort === "Rating" || !sort) {
		q = query(q, orderBy("avgRating", "desc"));
	} else if (sort === "Review") {
		q = query(q, orderBy("numRatings", "desc"));
	}
	return q;
}

export async function getRestaurants(db = db, filters = {}) {
	let q = query(collection(db, "restaurants"));

	q = applyQueryFilters(q, filters);
	const results = await getDocs(q);
	return results.docs.map(doc => {
		return {
			id: doc.id,
			...doc.data(),
			// Only plain objects can be passed to Client Components from Server Components
			timestamp: doc.data().timestamp.toDate(),
		};
	});
}
  1. 'Firestore에서 식당 목록 읽기'라는 커밋 메시지를 사용하여 커밋을 만들고 GitHub 저장소에 푸시합니다.
  2. Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
  3. 웹 앱에서 페이지를 새로고침합니다. 레스토랑 이미지는 페이지에 타일로 표시됩니다.

서버 런타임에 레스토랑 목록이 로드되는지 확인

Next.js 프레임워크를 사용하면 데이터가 서버 런타임 시 또는 클라이언트 측 런타임에 로드되는 시기를 명확히 파악하지 못할 수 있습니다.

서버 런타임에 레스토랑 목록이 로드되는지 확인하려면 다음 단계를 따르세요.

  1. 웹 앱에서 DevTools를 열고 JavaScript를 사용 중지합니다.

DevTools에서 JavaScript 사용 중지

  1. 웹 앱을 새로고침합니다. 레스토랑 목록이 계속 로드됩니다. 레스토랑 정보는 서버 응답에 반환됩니다. JavaScript가 사용 설정되면 클라이언트 측 JavaScript 코드를 통해 레스토랑 정보가 하드레이션됩니다.
  2. DevTools에서 JavaScript를 다시 사용 설정합니다.

Cloud Firestore 스냅샷 리스너로 레스토랑 업데이트 리슨

이전 섹션에서는 초기 레스토랑 집합이 src/app/page.js 파일에서 어떻게 로드되는지 확인했습니다. src/app/page.js 파일은 서버 구성요소이며 Firebase 데이터 가져오기 코드를 포함해 서버에서 렌더링됩니다.

src/components/RestaurantListings.jsx 파일은 클라이언트 구성요소이며 서버에서 렌더링한 마크업을 하이드레이션하도록 구성할 수 있습니다.

서버 렌더링 마크업을 하이드레이션하도록 src/components/RestaurantListings.jsx 파일을 구성하려면 다음 단계를 따르세요.

  1. src/components/RestaurantListings.jsx 파일에서 이미 작성된 다음 코드를 확인합니다.
useEffect(() => {
        const unsubscribe = getRestaurantsSnapshot(data => {
                setRestaurants(data);
        }, filters);

        return () => {
                unsubscribe();
        };
}, [filters]);

이 코드는 이전 단계에서 구현한 getRestaurants() 함수와 유사한 getRestaurantsSnapshot() 함수를 호출합니다. 그러나 이 스냅샷 함수는 레스토랑의 컬렉션이 변경될 때마다 콜백이 호출되도록 콜백 메커니즘을 제공합니다.

  1. src/lib/firebase/firestore.js 파일에서 getRestaurantsSnapshot() 함수를 다음 코드로 바꿉니다.
export function getRestaurantsSnapshot(cb, filters = {}) {
	if (typeof cb !== "function") {
		console.log("Error: The callback parameter is not a function");
		return;
	}

	let q = query(collection(db, "restaurants"));
	q = applyQueryFilters(q, filters);

	const unsubscribe = onSnapshot(q, querySnapshot => {
		const results = querySnapshot.docs.map(doc => {
			return {
				id: doc.id,
				...doc.data(),
				// Only plain objects can be passed to Client Components from Server Components
				timestamp: doc.data().timestamp.toDate(),
			};
		});

		cb(results);
	});

	return unsubscribe;
}

이제 Firestore 데이터베이스 페이지를 통한 변경사항이 웹 앱에 실시간으로 반영됩니다.

  1. '실시간 식당 업데이트 수신 대기'라는 커밋 메시지로 커밋을 만들고 GitHub 저장소에 푸시합니다.
  2. Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
  3. 웹 앱에서 27ca5d1e8ed8adfe.png > 샘플 식당 추가를 선택합니다. 스냅샷 함수가 올바르게 구현되면 레스토랑이 페이지 새로고침 없이 실시간으로 표시됩니다.

8. 웹 앱에서 사용자가 제출한 리뷰 저장

  1. src/lib/firebase/firestore.js 파일에서 updateWithRating() 함수를 다음 코드로 바꿉니다.
const updateWithRating = async (
	transaction,
	docRef,
	newRatingDocument,
	review
) => {
	const restaurant = await transaction.get(docRef);
	const data = restaurant.data();
	const newNumRatings = data?.numRatings ? data.numRatings + 1 : 1;
	const newSumRating = (data?.sumRating || 0) + Number(review.rating);
	const newAverage = newSumRating / newNumRatings;

	transaction.update(docRef, {
		numRatings: newNumRatings,
		sumRating: newSumRating,
		avgRating: newAverage,
	});

	transaction.set(newRatingDocument, {
		...review,
		timestamp: Timestamp.fromDate(new Date()),
	});
};

이 코드는 새 리뷰를 나타내는 새 Firestore 문서를 삽입합니다. 또한 이 코드는 레스토랑을 나타내는 기존 Firestore 문서도 평점 수와 계산된 평균 평점의 업데이트된 수치로 업데이트합니다.

  1. addReviewToRestaurant() 함수를 코드를 다음 코드로 바꿉니다.
export async function addReviewToRestaurant(db, restaurantId, review) {
	if (!restaurantId) {
		throw new Error("No restaurant ID has been provided.");
	}

	if (!review) {
		throw new Error("A valid review has not been provided.");
	}

	try {
		const docRef = doc(collection(db, "restaurants"), restaurantId);
		const newRatingDocument = doc(
			collection(db, `restaurants/${restaurantId}/ratings`)
		);

		// corrected line
		await runTransaction(db, transaction =>
			updateWithRating(transaction, docRef, newRatingDocument, review)
		);
	} catch (error) {
		console.error(
			"There was an error adding the rating to the restaurant",
			error
		);
		throw error;
	}
}

Next.js 서버 작업 구현

Next.js 서버 작업은 양식 제출 페이로드에서 텍스트 값을 가져오기 위해 양식 데이터에 액세스할 수 있는 편리한 API(예: data.get("text"))를 제공합니다.

Next.js 서버 작업을 사용하여 리뷰 양식 제출을 처리하려면 다음 단계를 따르세요.

  1. src/components/ReviewDialog.jsx 파일의 <form> 요소에서 action 속성을 찾습니다.
<form action={handleReviewFormSubmission}>

action 속성 값은 다음 단계에서 구현하는 함수를 참조합니다.

  1. src/app/actions.js 파일에서 handleReviewFormSubmission() 함수를 다음 코드로 바꿉니다.
// This is a next.js server action, which is an alpha feature, so
// use with caution.
// https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
export async function handleReviewFormSubmission(data) {
        const { app } = await getAuthenticatedAppForUser();
        const db = getFirestore(app);

        await addReviewToRestaurant(db, data.get("restaurantId"), {
                text: data.get("text"),
                rating: data.get("rating"),

                // This came from a hidden form field.
                userId: data.get("userId"),
        });
}

레스토랑 리뷰 추가

리뷰 제출 지원을 구현했으므로 이제 리뷰가 Cloud Firestore에 올바르게 삽입되었는지 확인할 수 있습니다.

리뷰를 추가하고 Cloud Firestore에 삽입되었는지 확인하려면 다음 단계를 따르세요.

  1. '사용자가 식당 리뷰를 제출하도록 허용'이라는 커밋 메시지를 사용하여 커밋을 만들고 GitHub 저장소에 푸시합니다.
  2. Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
  3. 웹 앱을 새로고침하고 홈페이지에서 식당을 선택합니다.
  4. 음식점 페이지에서 3e19beef78bb0d0e.png 아이콘을 클릭합니다.
  5. 별표 평점을 선택합니다.
  6. 리뷰를 작성합니다.
  7. 제출을 클릭합니다. 그러면 리뷰가 리뷰 목록 상단에 표시됩니다.
  8. Cloud Firestore의 문서 추가 창에서 리뷰한 식당의 문서를 검색하여 선택합니다.
  9. 컬렉션 시작 창에서 평점을 선택합니다.
  10. 문서 추가 창에서 검토할 문서를 찾아 예상대로 삽입되었는지 확인합니다.

Firestore 에뮬레이터의 문서

9. 웹 앱에서 사용자가 업로드한 파일 저장

이 섹션에서는 로그인했을 때 레스토랑과 연결된 이미지를 대체할 수 있는 기능을 추가합니다. Firebase Storage에 이미지를 업로드하고 식당을 나타내는 Cloud Firestore 문서에서 이미지 URL을 업데이트합니다.

웹 앱에서 사용자가 업로드한 파일을 저장하려면 다음 단계를 따르세요.

  1. src/components/Restaurant.jsx 파일에서 사용자가 파일을 업로드할 때 실행되는 코드를 관찰합니다.
async function handleRestaurantImage(target) {
        const image = target.files ? target.files[0] : null;
        if (!image) {
                return;
        }

        const imageURL = await updateRestaurantImage(id, image);
        setRestaurant({ ...restaurant, photo: imageURL });
}

변경할 필요는 없지만 다음 단계에서 updateRestaurantImage() 함수의 동작을 구현합니다.

  1. src/lib/firebase/storage.js 파일에서 updateRestaurantImage() 함수와 uploadImage() 함수를 다음 코드로 바꿉니다.
export async function updateRestaurantImage(restaurantId, image) {
	try {
		if (!restaurantId)
			throw new Error("No restaurant ID has been provided.");

		if (!image || !image.name)
			throw new Error("A valid image has not been provided.");

		const publicImageUrl = await uploadImage(restaurantId, image);
		await updateRestaurantImageReference(restaurantId, publicImageUrl);

		return publicImageUrl;
	} catch (error) {
		console.error("Error processing request:", error);
	}
}

async function uploadImage(restaurantId, image) {
	const filePath = `images/${restaurantId}/${image.name}`;
	const newImageRef = ref(storage, filePath);
	await uploadBytesResumable(newImageRef, image);

	return await getDownloadURL(newImageRef);
}

updateRestaurantImageReference() 함수는 이미 구현되어 있습니다. 이 함수는 업데이트된 이미지 URL로 Cloud Firestore의 기존 식당 문서를 업데이트합니다.

이미지 업로드 기능 확인

이미지가 예상대로 업로드되는지 확인하려면 다음 단계를 따르세요.

  1. '사용자가 각 식당의 사진을 변경하도록 허용'이라는 커밋 메시지를 사용하여 커밋을 만들고 GitHub 저장소에 푸시합니다.
  2. Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
  3. 웹 앱에서 로그인했는지 확인하고 레스토랑을 선택합니다.
  4. 7067eb41fea41ff0.png를 클릭하고 파일 시스템에서 이미지를 업로드합니다. 이미지는 로컬 환경을 벗어나 Cloud Storage에 업로드됩니다. 이미지는 업로드한 즉시 표시됩니다.
  5. Firebase용 Cloud Storage로 이동합니다.
  6. 레스토랑을 나타내는 폴더로 이동합니다. 업로드한 이미지가 폴더에 있습니다.

6cf3f9e2303c931c.png

10. 생성형 AI로 음식점 리뷰 요약

이 섹션에서는 사용자가 모든 리뷰를 읽지 않고도 레스토랑에 대한 모든 생각을 빠르게 이해할 수 있도록 리뷰 요약 기능을 추가합니다.

Cloud Secret Manager에 Gemini API 키 저장

  1. Gemini API를 사용하려면 API 키가 필요합니다. Google AI Studio에서 키를 만듭니다.
  2. App Hosting은 Cloud Secret Manager와 통합되어 API 키와 같은 민감한 값을 안전하게 저장할 수 있게 해줍니다.
    1. 터미널에서 다음 명령어를 실행하여 새 보안 비밀을 만듭니다.
    firebase apphosting:secrets:set gemini-api-key
    
    1. 보안 비밀 값을 입력하라는 메시지가 표시되면 Google AI Studio에서 Gemini API 키를 복사하여 붙여넣습니다.
    2. 새 보안 비밀을 apphosting.yaml에 추가해야 하는지 묻는 메시지가 표시되면 Y를 입력하여 수락합니다.

이제 Gemini API 키가 Cloud Secret Manager에 안전하게 저장되며 App Hosting 백엔드에서 액세스할 수 있습니다.

리뷰 요약 구성요소 구현

  1. src/components/Reviews/ReviewSummary.jsx에서 GeminiSummary 함수를 다음 코드로 바꿉니다.
    export async function GeminiSummary({ restaurantId }) {
        const { firebaseServerApp } = await getAuthenticatedAppForUser();
        const reviews = await getReviewsByRestaurantId(
            getFirestore(firebaseServerApp),
            restaurantId
        );
    
        const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
        const model = genAI.getGenerativeModel({ model: "gemini-pro"});
    
        const reviewSeparator = "@";
        const prompt = `
            Based on the following restaurant reviews, 
            where each review is separated by a '${reviewSeparator}' character, 
            create a one-sentence summary of what people think of the restaurant. 
    
            Here are the reviews: ${reviews.map(review => review.text).join(reviewSeparator)}
        `;
    
        try {
            const result = await model.generateContent(prompt);
            const response = await result.response;
            const text = response.text();
    
            return (
                <div className="restaurant__review_summary">
                    <p>{text}</p>
                    <p>✨ Summarized with Gemini</p>
                </div>
            );
        } catch (e) {
            console.error(e);
            return <p>Error contacting Gemini</p>;
        }
    }
    
  2. 'AI를 사용하여 리뷰 요약'이라는 커밋 메시지를 사용하여 커밋을 만들고 GitHub 저장소에 푸시합니다.
  3. Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
  4. 식당 페이지를 엽니다. 상단에 페이지에 있는 모든 리뷰에 대한 한 문장 요약이 표시됩니다.
  5. 새 리뷰를 추가하고 페이지를 새로고침하세요. 그러면 요약이 변경됩니다.

11. 결론

수고하셨습니다 Firebase를 사용하여 Next.js 앱에 기능을 추가하는 방법을 알아봤습니다. 구체적으로 다음을 사용했습니다.

자세히 알아보기