1. 시작하기 전에
이 Codelab에서는 식당 리뷰 웹사이트인 Friendly Eats라는 Next.js 웹 앱과 Firebase를 통합하는 방법을 알아봅니다.
완성된 웹 앱은 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 계정으로 로그인을 사용하는 방법
필요한 사항
2. 개발 환경 및 GitHub 저장소 설정
이 Codelab에서는 앱의 시작 코드베이스를 제공하며 Firebase CLI를 사용합니다.
GitHub 저장소 만들기
Codelab 소스는 https://github.com/firebase/friendlyeats-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
폴더를 자체 저장소에 복사합니다.
- 터미널을 사용하여 컴퓨터에 새 폴더를 만들고 새 디렉터리로 변경합니다.
mkdir codelab-friendlyeats-web cd codelab-friendlyeats-web
- giget npm 패키지를 사용하여
nextjs-start
폴더만 가져옵니다.npx giget@latest gh:firebase/friendlyeats-web/nextjs-start#master . --install
- git을 사용하여 변경사항을 로컬에서 추적합니다.
git init git commit -a -m "codelab starting point" git branch -M main
- 새 GitHub 저장소(https://github.com/new)를 만듭니다. 원하는 이름을 지정합니다.
- GitHub는
https://github.com/
또는/ .git git@github.com:
과 같은 새 저장소 URL을 제공합니다. 이 URL을 복사합니다./ .git
- GitHub는
- 로컬 변경사항을 새 GitHub 저장소로 푸시합니다. 다음 명령어를 실행합니다. 이때
자리표시자를 저장소 URL로 바꿉니다.git remote add origin <your-repository-url> git push -u origin main
- 이제 GitHub 저장소에 시작 코드가 표시됩니다.
Firebase CLI 설치 또는 업데이트
다음 명령어를 실행하여 Firebase CLI가 설치되어 있고 v13.9.0 이상인지 확인합니다.
firebase --version
낮은 버전이 표시되거나 Firebase CLI가 설치되어 있지 않으면 설치 명령어를 실행합니다.
npm install -g firebase-tools@latest
권한 오류로 인해 Firebase CLI를 설치할 수 없는 경우 npm 문서를 참고하거나 다른 설치 옵션을 사용하세요.
Firebase에 로그인
- 다음 명령어를 실행하여 Firebase CLI에 로그인합니다.
firebase login
- Firebase에서 데이터를 수집할지 여부에 따라
Y
또는N
을 입력합니다. - 브라우저에서 Google 계정을 선택한 다음 허용을 클릭합니다.
셋째, Firebase 프로젝트 설정
이 섹션에서는 Firebase 프로젝트를 설정하고 Firebase 웹 앱을 프로젝트에 연결합니다. 샘플 웹 앱에서 사용하는 Firebase 서비스도 설정합니다.
Firebase 프로젝트 만들기
- Firebase Console에서 프로젝트 추가를 클릭합니다.
- 프로젝트 이름 입력 텍스트 상자에
FriendlyEats Codelab
(또는 원하는 프로젝트 이름)을 입력한 후 계속을 클릭합니다. - Firebase 결제 요금제 확인 창에서 요금제가 Blaze인지 확인한 다음 요금제 확인을 클릭합니다.
- 이 Codelab에서는 Google 애널리틱스가 필요하지 않으므로 이 프로젝트에 Google 애널리틱스 사용 설정 옵션을 사용 중지합니다.
- 프로젝트 만들기를 클릭합니다.
- 프로젝트가 프로비저닝될 때까지 기다린 다음 계속을 클릭합니다.
- Firebase 프로젝트에서 프로젝트 설정으로 이동합니다. 나중에 필요하므로 프로젝트 ID를 기록해 둡니다. 이 고유 식별자는 프로젝트를 식별하는 방법입니다(예: Firebase CLI에서).
Firebase 요금제 업그레이드
App Hosting을 사용하려면 Firebase 프로젝트에서 Blaze 요금제를 사용해야 합니다. 즉, Cloud Billing 계정과 연결되어 있어야 합니다.
- Cloud Billing 계정에는 신용카드와 같은 결제 수단이 필요합니다.
- Firebase와 Google Cloud를 처음 사용하는 경우 $300 크레딧과 무료 체험판 Cloud Billing 계정을 받을 자격이 되는지 확인하세요.
프로젝트를 Blaze 요금제로 업그레이드하려면 다음 단계를 따르세요.
- Firebase Console에서 요금제 업그레이드를 선택합니다.
- 대화상자에서 Blaze 요금제를 선택한 다음 화면에 표시된 안내에 따라 프로젝트를 Cloud Billing 계정과 연결합니다.
Cloud Billing 계정을 만들어야 하는 경우 업그레이드를 완료하기 위해 Firebase Console의 업그레이드 흐름으로 돌아가야 할 수 있습니다.
Firebase 프로젝트에 웹 앱 추가
- Firebase 프로젝트에서 프로젝트 개요로 이동한 다음 웹을 클릭합니다.
프로젝트에 이미 앱이 등록된 경우 앱 추가를 클릭하여 웹 아이콘을 표시합니다. - 앱 닉네임 텍스트 상자에
My Next.js app
과 같이 기억하기 쉬운 앱 닉네임을 입력합니다. - 이 앱에 Firebase 호스팅도 설정 체크박스를 선택 해제된 상태로 둡니다.
- 앱 등록 > 다음 > 다음 > 콘솔로 이동을 클릭합니다.
Firebase Console에서 Firebase 서비스 설정
인증 설정
- Firebase Console에서 인증으로 이동합니다.
- 시작하기를 클릭합니다.
- 추가 제공업체 열에서 Google > 사용 설정을 클릭합니다.
- 프로젝트의 공개용 이름 텍스트 상자에 기억하기 쉬운 이름(예:
My Next.js app
)을 입력합니다. - 프로젝트 지원 이메일 드롭다운에서 이메일 주소를 선택합니다.
- 저장을 클릭합니다.
Cloud Firestore 설정
- Firebase Console에서 Firestore로 이동합니다.
- 데이터베이스 만들기 > 다음 > 테스트 모드에서 시작 > 다음을 실행합니다.
이 Codelab의 후반부에서는 보안 규칙을 추가하여 데이터를 보호합니다. 데이터베이스에 대한 보안 규칙을 추가하지 않은 채 앱을 공개적으로 배포하거나 노출하지 마세요. - 기본 위치를 사용하거나 원하는 위치를 선택합니다.
실제 앱의 경우 사용자와 가까운 위치를 선택하는 것이 좋습니다. 이 위치는 나중에 변경할 수 없으며 자동으로 기본 Cloud Storage 버킷의 위치가 됩니다(다음 단계). - 완료를 클릭합니다.
Firebase용 Cloud Storage 설정
- Firebase Console에서 스토리지로 이동합니다.
- 시작하기 > 테스트 모드에서 시작 > 다음을 클릭합니다.
이 Codelab의 후반부에서 데이터를 보호하는 보안 규칙을 추가합니다. 스토리지 버킷에 대한 보안 규칙을 추가하지 않은 채 앱을 공개적으로 배포하거나 노출하지 마세요. - 이전 단계에서 Firestore를 설정했기 때문에 버킷의 위치가 이미 선택되어 있습니다.
- 완료를 클릭합니다.
4. 시작 코드베이스 검토
이 섹션에서는 이 Codelab에서 기능을 추가할 앱의 시작 코드베이스 영역을 몇 가지 검토합니다.
폴더 및 파일 구조
다음 표에는 앱의 폴더 및 파일 구조가 요약되어 있습니다.
폴더 및 파일 | 설명 |
| 필터, 헤더, 레스토랑 세부정보, 리뷰용 React 구성요소 |
| React 또는 Next.js에 바인딩되지 않은 유틸리티 함수 |
| Firebase 관련 코드 및 Firebase 구성 |
| 웹 앱의 정적 애셋(예: 아이콘) |
| Next.js 앱 라우터를 사용한 라우팅 |
| API 경로 핸들러 |
| npm을 사용한 프로젝트 종속 항목 |
| Next.js 관련 구성(서버 작업이 사용 설정됨) |
| 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 백엔드가 GitHub의 저장소에 연결되어 main
브랜치에 새 커밋을 푸시할 때마다 앱의 새 버전을 자동으로 다시 빌드하고 출시합니다.
보안 규칙 배포
이 코드에는 이미 Firestore 및 Firebase용 Cloud Storage에 대한 보안 규칙 세트가 있습니다. 보안 규칙을 배포하면 데이터베이스와 버킷의 데이터를 오용으로부터 더욱 안전하게 보호할 수 있습니다.
- 터미널에서 이전에 만든 Firebase 프로젝트를 사용하도록 CLI를 구성합니다.
드림 별칭을 입력하라는 메시지가 표시되면firebase use --add
friendlyeats-codelab
을 입력합니다. - 이러한 보안 규칙을 배포하려면 터미널에서 다음 명령어를 실행합니다.
firebase deploy --only firestore:rules,storage
"Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?"
라는 메시지가 표시되면Enter
키를 눌러 예를 선택합니다.
웹 앱 코드에 Firebase 구성 추가
- Firebase Console에서 프로젝트 설정으로 이동합니다.
- SDK 설정 및 구성 창에서 '앱 추가'를 클릭합니다. 그런 다음 코드 괄호 아이콘
을 클릭하여 새 웹 앱을 등록합니다.
- 웹 앱 만들기 흐름이 끝나면
firebaseConfig
변수를 복사하고 속성과 값을 복사합니다. - 코드 편집기에서
apphosting.yaml
파일을 열고 환경 변수 값을 Firebase Console의 구성 값으로 채웁니다. - 파일에서 기존 속성을 복사한 속성으로 바꿉니다.
- 파일을 저장합니다.
백엔드 만들기
- Firebase Console에서 앱 호스팅 페이지로 이동합니다.
- '시작하기'를 클릭합니다. 백엔드 생성 흐름을 시작합니다 백엔드를 다음과 같이 구성합니다.
- 첫 번째 단계의 안내에 따라 앞에서 만든 GitHub 저장소에 연결합니다.
- 배포 설정을 지정합니다.
- 루트 디렉터리를
/
로 유지합니다. - 라이브 브랜치를
main
로 설정합니다. - 자동 출시 사용 설정
- 루트 디렉터리를
- 백엔드 이름을
friendlyeats-codelab
로 지정합니다. - 'Firebase 웹 앱 만들기 또는 연결'의 '기존 Firebase 웹 앱 선택'에서 이전에 구성한 웹 앱을 선택합니다. 드롭다운을 선택합니다.
- '완료 및 배포'를 클릭합니다. 잠시 후 새 App Hosting 백엔드의 상태를 확인할 수 있는 새 페이지로 이동하게 됩니다.
- 출시가 완료되면 '도메인'에서 무료 도메인을 클릭합니다. DNS 전파로 인해 작업을 시작하는 데 몇 분 정도 걸릴 수 있습니다.
초기 웹 앱을 배포했습니다. GitHub 저장소의 main
브랜치에 새 커밋을 푸시할 때마다 Firebase Console에서 새 빌드 및 출시가 시작되고 출시가 완료되면 사이트가 자동으로 업데이트됩니다.
6. 웹 앱에 인증 추가
이 섹션에서는 웹 앱에 로그인할 수 있도록 인증을 추가합니다.
로그인 및 로그아웃 함수 구현
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 | 설명 |
Google 인증 제공업체 인스턴스를 만듭니다. | |
대화상자 기반 인증 흐름을 시작합니다. | |
사용자를 로그아웃합니다. |
src/components/Header.jsx
파일에서 코드는 이미 signInWithGoogle
및 signOut
함수를 호출합니다.
- 'Google 인증 추가'라는 커밋 메시지로 커밋을 만듭니다. GitHub 저장소에 푸시합니다 1. Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
- 웹 앱에서 페이지를 새로고침하고 Google 계정으로 로그인을 클릭합니다. 웹 앱이 업데이트되지 않으므로 로그인이 성공했는지 확실하지 않습니다.
인증 상태를 서버로 전송
인증 상태를 서버에 전달하기 위해 서비스 워커를 사용합니다. fetchWithFirebaseHeaders
및 getAuthIdToken
함수를 다음 코드로 바꿉니다.
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 };
}
인증 변경사항 구독
인증 변경사항을 구독하려면 다음 단계를 따르세요.
src/components/Header.jsx
파일로 이동합니다.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
핸들러가 이를 처리합니다.
이제 새 빌드를 출시하고 빌드한 내용을 확인할 차례입니다.
- '로그인 상태 표시' 커밋 메시지로 커밋을 만들고 GitHub 저장소에 푸시합니다.
- Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
- 새 인증 동작을 확인합니다.
- 브라우저에서 웹 앱을 새로고침합니다. 표시 이름이 헤더에 표시됩니다.
- 로그아웃하고 다시 로그인하세요. 페이지 새로고침 없이 페이지가 실시간으로 업데이트됩니다. 다른 사용자에 대해 이 단계를 반복할 수 있습니다.
- 선택사항: 웹 앱을 마우스 오른쪽 버튼으로 클릭하고 페이지 소스 보기를 선택한 후 표시 이름을 검색합니다. 서버에서 반환된 원시 HTML 소스에 표시됩니다.
7. 레스토랑 정보 보기
웹 앱에는 레스토랑 및 리뷰에 대한 모의 데이터가 포함되어 있습니다.
레스토랑 하나 이상 추가
로컬 Cloud Firestore 데이터베이스에 모의 레스토랑 데이터를 삽입하려면 다음 단계를 따르세요.
- 웹 앱에서 > 샘플 레스토랑 추가를 선택합니다.
- Firebase Console의 Firestore 데이터베이스 페이지에서 음식점을 선택합니다. 레스토랑 컬렉션에 표시되는 최상위 문서는 각각 레스토랑을 나타냅니다.
- 문서를 몇 개 클릭하여 레스토랑 문서의 속성을 살펴봅니다.
레스토랑 목록 표시
이제 Cloud Firestore 데이터베이스에 Next.js 웹 앱에서 표시할 수 있는 음식점이 있습니다.
데이터 가져오기 코드를 정의하려면 다음 단계를 따르세요.
src/app/page.js
파일에서<Home />
서버 구성요소를 찾고 서버 런타임 시 음식점 목록을 가져오는getRestaurants
함수 호출을 검토합니다. 다음 단계에서는getRestaurants
함수를 구현합니다.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(),
};
});
}
- 'Firestore에서 음식점 목록 읽기'라는 커밋 메시지로 커밋을 만들고 GitHub 저장소에 푸시합니다.
- Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
- 웹 앱에서 페이지를 새로고침합니다. 레스토랑 이미지는 페이지에 타일로 표시됩니다.
서버 런타임에 레스토랑 목록이 로드되는지 확인
Next.js 프레임워크를 사용하면 데이터가 서버 런타임에 로드되는지 또는 클라이언트 측 런타임에 로드되는지 명확하지 않을 수 있습니다.
서버 런타임에 레스토랑 목록이 로드되는지 확인하려면 다음 단계를 따르세요.
- 웹 앱에서 DevTools를 열고 JavaScript를 사용 중지합니다.
- 웹 앱을 새로고침합니다. 레스토랑 목록이 계속 로드됩니다. 레스토랑 정보는 서버 응답에 반환됩니다. JavaScript가 사용 설정되면 클라이언트 측 JavaScript 코드를 통해 레스토랑 정보가 하드레이션됩니다.
- DevTools에서 JavaScript를 다시 사용 설정합니다.
Cloud Firestore 스냅샷 리스너로 레스토랑 업데이트 리슨
이전 섹션에서는 src/app/page.js
파일에서 초기 레스토랑 세트를 로드하는 방법을 살펴봤습니다. src/app/page.js
파일은 서버 구성요소이며 Firebase 데이터 가져오기 코드를 포함하여 서버에서 렌더링됩니다.
src/components/RestaurantListings.jsx
파일은 클라이언트 구성요소이며 서버에서 렌더링된 마크업을 수화하도록 구성할 수 있습니다.
서버 렌더링 마크업을 하이드레이션하도록 src/components/RestaurantListings.jsx
파일을 구성하려면 다음 단계를 따르세요.
src/components/RestaurantListings.jsx
파일에서 이미 작성된 다음 코드를 확인합니다.
useEffect(() => {
const unsubscribe = getRestaurantsSnapshot(data => {
setRestaurants(data);
}, filters);
return () => {
unsubscribe();
};
}, [filters]);
이 코드는 이전 단계에서 구현한 getRestaurants()
함수와 유사한 getRestaurantsSnapshot()
함수를 호출합니다. 그러나 이 스냅샷 함수는 레스토랑의 컬렉션이 변경될 때마다 콜백이 호출되도록 콜백 메커니즘을 제공합니다.
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 데이터베이스 페이지를 통해 변경한 사항이 웹 앱에 실시간으로 반영됩니다.
- '실시간 식당 업데이트 수신 대기' 커밋 메시지를 사용하여 커밋을 만듭니다. GitHub 저장소에 푸시합니다
- Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
- 웹 앱에서 > 샘플 레스토랑 추가를 선택합니다. 스냅샷 함수가 올바르게 구현되면 레스토랑이 페이지 새로고침 없이 실시간으로 표시됩니다.
8. 웹 앱에서 사용자가 제출한 리뷰 저장
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 문서도 평점 수와 계산된 평균 평점의 업데이트된 수치로 업데이트합니다.
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 서버 작업을 사용하여 리뷰 양식 제출을 처리하려면 다음 단계를 따르세요.
src/components/ReviewDialog.jsx
파일에서<form>
요소의action
속성을 찾습니다.
<form action={handleReviewFormSubmission}>
action
속성 값은 다음 단계에서 구현하는 함수를 참조합니다.
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에 삽입되었는지 확인하려면 다음 단계를 따르세요.
- '사용자가 음식점 리뷰를 제출하도록 허용'이라는 커밋 메시지를 사용하여 커밋을 만듭니다. GitHub 저장소에 푸시합니다
- Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
- 웹 앱을 새로고침하고 홈페이지에서 레스토랑을 선택합니다.
- 레스토랑 페이지에서 를 클릭합니다.
- 별표 평점을 선택합니다.
- 리뷰를 작성합니다.
- 제출을 클릭합니다. 그러면 리뷰가 리뷰 목록 상단에 표시됩니다.
- Cloud Firestore의 문서 추가 창에서 검토한 레스토랑의 문서를 검색하여 선택합니다.
- 컬렉션 시작 창에서 평점을 선택합니다.
- 문서 추가 창에서 검토할 문서를 찾아 예상대로 삽입되었는지 확인합니다.
9. 웹 앱에서 사용자가 업로드한 파일 저장
이 섹션에서는 로그인한 상태에서 레스토랑과 연결된 이미지를 대체할 수 있는 기능을 추가합니다. Firebase Storage에 이미지를 업로드하고 레스토랑을 나타내는 Cloud Firestore 문서에서 이미지 URL을 업데이트합니다.
웹 앱에서 사용자가 업로드한 파일을 저장하려면 다음 단계를 따르세요.
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()
함수의 동작을 구현합니다.
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의 기존 식당 문서를 업데이트합니다.
이미지 업로드 기능 확인
이미지가 예상대로 업로드되는지 확인하려면 다음 단계를 따르세요.
- '사용자가 각 음식점의 사진을 변경하도록 허용'이라는 커밋 메시지로 커밋을 만들고 GitHub 저장소에 푸시합니다.
- Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
- 웹 앱에서 로그인했는지 확인하고 레스토랑을 선택합니다.
- 를 클릭하고 파일 시스템에서 이미지를 업로드합니다. 이미지가 로컬 환경을 벗어나 Cloud Storage에 업로드됩니다. 이미지를 업로드하면 즉시 표시됩니다.
- Firebase용 Cloud Storage로 이동합니다.
- 레스토랑을 나타내는 폴더로 이동합니다. 업로드한 이미지가 폴더에 있습니다.
10. 생성형 AI로 음식점 리뷰 요약
이 섹션에서는 사용자가 모든 리뷰를 읽지 않고도 레스토랑에 대한 다른 사용자의 의견을 빠르게 파악할 수 있도록 리뷰 요약 기능을 추가합니다.
Cloud Secret Manager에 Gemini API 키 저장
- Gemini API를 사용하려면 API 키가 필요합니다. Google AI Studio에서 키를 만듭니다.
- App Hosting은 Cloud Secret Manager와 통합되어 API 키와 같은 민감한 값을 안전하게 저장할 수 있습니다.
- 터미널에서 다음 명령어를 실행하여 새 보안 비밀을 만듭니다.
firebase apphosting:secrets:set gemini-api-key
- 보안 비밀 값을 입력하라는 메시지가 표시되면 Google AI Studio에서 Gemini API 키를 복사하여 붙여넣습니다.
- 새 보안 비밀을
apphosting.yaml
에 추가할지 묻는 메시지가 표시되면Y
를 입력하여 수락합니다.
이제 Gemini API 키가 Cloud Secret Manager에 안전하게 저장되며 App Hosting 백엔드에서 액세스할 수 있습니다.
리뷰 요약 구성요소 구현
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>; } }
- 'AI를 사용하여 리뷰 요약'이라는 커밋 메시지로 커밋을 만들고 GitHub 저장소에 푸시합니다.
- Firebase Console에서 앱 호스팅 페이지를 열고 새 출시가 완료될 때까지 기다립니다.
- 음식점 페이지를 엽니다. 페이지 상단에 페이지에 있는 모든 리뷰를 한 문장으로 요약한 내용이 표시됩니다.
- 새 리뷰를 추가하고 페이지를 새로고침합니다. 요약이 변경된 것을 확인할 수 있습니다.
11. 결론
수고하셨습니다 Firebase를 사용하여 Next.js 앱에 기능을 추가하는 방법을 알아봤습니다. 특히 다음을 사용했습니다.
- Firebase App Hosting: 구성된 브랜치로 푸시할 때마다 Next.js 코드를 자동으로 빌드하고 배포합니다.
- Firebase 인증: 로그인 및 로그아웃 기능 사용 설정
- Cloud Firestore: 레스토랑 데이터 및 레스토랑 리뷰 데이터
- Firebase용 Cloud Storage: 레스토랑 이미지