Node.js Admin SDK를 사용하여 Firestore와 함께 Cloud Run에 풀 스택 Next.js 애플리케이션 배포

1. 개요

Cloud Run은 Google의 확장 가능한 인프라에서 직접 코드를 실행할 수 있게 해 주는 완전 관리형 플랫폼입니다. 이 Codelab에서는 Node.js Admin SDK를 사용하여 Cloud Run의 Next.js 애플리케이션을 Firestore 데이터베이스에 연결하는 방법을 보여줍니다.

이 실습에서는 다음 작업을 수행하는 방법을 배웁니다.

  • Firestore 데이터베이스 만들기
  • Firestore 데이터베이스에 연결되는 애플리케이션을 Cloud Run에 배포

2. 기본 요건

  1. 아직 Google 계정이 없다면 Google 계정을 만들어야 합니다.
    • 직장 또는 학교 계정 대신 개인 계정을 사용하세요. 직장 및 학교 계정에는 이 실습에 필요한 API를 사용 설정하지 못하도록 하는 제한이 있을 수 있습니다.

3. 프로젝트 설정

  1. Google Cloud 콘솔에 로그인합니다.
  2. Cloud 콘솔에서 결제를 사용 설정합니다.
    • 이 실습을 완료하는 데 드는 Cloud 리소스 비용은 미화 1달러 미만입니다.
    • 이 실습이 끝나면 단계에 따라 리소스를 삭제하여 추가 요금이 발생하지 않도록 할 수 있습니다.
    • 신규 사용자는 미화$300 상당의 무료 체험판을 사용할 수 있습니다.
  3. 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다.

4. Cloud Shell 편집기 열기

  1. Cloud Shell 편집기로 이동합니다.
  2. 터미널이 화면 하단에 표시되지 않으면 다음을 실행하여 엽니다.
    • 햄버거 메뉴 햄버거 메뉴 아이콘를 클릭합니다.
    • 터미널을 클릭합니다.
    • 새 터미널을 클릭합니다.Cloud Shell 편집기에서 새 터미널 열기
  3. 터미널에서 다음 명령어를 사용하여 프로젝트를 설정합니다.
    • 형식:
      gcloud config set project [PROJECT_ID]
      
    • 예:
      gcloud config set project lab-project-id-example
      
    • 프로젝트 ID를 기억할 수 없는 경우 다음 단계를 따르세요.
      • 다음 명령어를 사용하여 모든 프로젝트 ID를 나열할 수 있습니다.
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      Cloud Shell 편집기 터미널에서 프로젝트 ID 설정
  4. 승인하라는 메시지가 표시되면 승인을 클릭하여 계속합니다. 클릭하여 Cloud Shell 승인
  5. 다음 메시지가 표시되어야 합니다.
    Updated property [core/project].
    
    WARNING이 표시되고 Do you want to continue (Y/N)?라는 메시지가 표시되면 프로젝트 ID를 잘못 입력한 것일 수 있습니다. N를 누르고 Enter를 누른 후 gcloud config set project 명령어를 다시 실행해 보세요.

5. API 사용 설정

터미널에서 API를 사용 설정합니다.

gcloud services enable \
  firestore.googleapis.com \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com

승인하라는 메시지가 표시되면 승인을 클릭하여 계속합니다. 클릭하여 Cloud Shell 승인

이 명령어를 완료하는 데 몇 분이 걸릴 수 있지만 결국 다음과 비슷한 성공 메시지가 표시됩니다.

Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.

6. Firestore 데이터베이스 만들기

  1. gcloud firestore databases create 명령어를 실행하여 Firestore 데이터베이스를 만듭니다.
    gcloud firestore databases create --location=nam5
    

7. 애플리케이션 준비

HTTP 요청에 응답하는 Next.js 애플리케이션을 준비합니다.

  1. task-app라는 새 Next.js 프로젝트를 만들려면 다음 명령어를 사용합니다.
    npx --yes create-next-app@15 task-app \
      --ts \
      --eslint \
      --tailwind \
      --no-src-dir \
      --turbopack \
      --app \
      --no-import-alias
    
  2. 디렉터리를 task-app으로 변경합니다.
    cd task-app
    
  1. Firestore 데이터베이스와 상호작용하려면 firebase-admin를 설치하세요.
    npm install firebase-admin
    
  1. Cloud Shell 편집기에서 actions.ts 파일을 엽니다.
    cloudshell edit app/actions.ts
    
    이제 빈 파일이 화면 상단에 표시됩니다. 여기에서 이 actions.ts 파일을 수정할 수 있습니다. 코드가 화면 상단 섹션에 표시됨
  2. 다음 코드를 복사하여 열린 actions.ts 파일에 붙여넣습니다.
    'use server'
    import { initializeApp, applicationDefault, getApps } from 'firebase-admin/app';
    import { getFirestore } from 'firebase-admin/firestore';
    const credential = applicationDefault();
    
    // Only initialize app if it does not already exist
    if (getApps().length === 0) {
      initializeApp({ credential });
    }
    
    const db = getFirestore();
    const tasksRef = db.collection('tasks');
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };
    
    // CREATE
    export async function addNewTaskToDatabase(newTask: string) {
      await tasksRef.doc().create({
        title: newTask,
        status: 'IN_PROGRESS',
        createdAt: Date.now(),
      });
      return;
    }
    
    // READ
    export async function getTasksFromDatabase() {
      const snapshot = await tasksRef.orderBy('createdAt', 'desc').limit(100).get();
      const tasks = await snapshot.docs.map(doc => ({
        id: doc.id,
        title: doc.data().title,
        status: doc.data().status,
        createdAt: doc.data().createdAt,
      }));
      return tasks;
    }
    
    // UPDATE
    export async function updateTaskInDatabase(task: Task) {
      await tasksRef.doc(task.id).set(task);
      return;
    }
    
    // DELETE
    export async function deleteTaskFromDatabase(taskId: string) {
      await tasksRef.doc(taskId).delete();
      return;
    }
    
  1. Cloud Shell 편집기에서 page.tsx 파일을 엽니다.
    cloudshell edit app/page.tsx
    
    이제 화면 상단에 기존 파일이 표시됩니다. 여기에서 이 page.tsx 파일을 수정할 수 있습니다. 코드가 화면 상단 섹션에 표시됨
  2. page.tsx 파일의 기존 콘텐츠를 삭제합니다.
  3. 다음 코드를 복사하여 열린 page.tsx 파일에 붙여넣습니다.
    'use client'
    import React, { useEffect, useState } from "react";
    import { addNewTaskToDatabase, getTasksFromDatabase, deleteTaskFromDatabase, updateTaskInDatabase } from "./actions";
    
    type Task = {
      id: string;
      title: string;
      status: 'IN_PROGRESS' | 'COMPLETE';
      createdAt: number;
    };
    
    export default function Home() {
      const [newTaskTitle, setNewTaskTitle] = useState('');
      const [tasks, setTasks] = useState<Task[]>([]);
    
      async function getTasks() {
        const updatedListOfTasks = await getTasksFromDatabase();
        setTasks(updatedListOfTasks);
      }
    
      useEffect(() => {
        getTasks();
      }, []);
    
      async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault();
        await addNewTaskToDatabase(newTaskTitle);
        await getTasks();
        setNewTaskTitle('');
      };
    
      async function updateTask(task: Task, newTaskValues: Partial<Task>) {
        await updateTaskInDatabase({ ...task, ...newTaskValues });
        await getTasks();
      }
    
      async function deleteTask(taskId: string) {
        await deleteTaskFromDatabase(taskId);
        await getTasks();
      }
    
      return (
        <main className="p-4">
          <h2 className="text-2xl font-bold mb-4">To Do List</h2>
          <div className="flex mb-4">
            <form onSubmit={handleSubmit} className="flex mb-8">
              <input
                type="text"
                placeholder="New Task Title"
                value={newTaskTitle}
                onChange={(e) => setNewTaskTitle(e.target.value)}
                className="flex-grow border border-gray-400 rounded px-3 py-2 mr-2 bg-inherit"
              />
              <button
                type="submit"
                className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-nowrap"
              >
                Add New Task
              </button>
            </form>
          </div>
          <table className="w-full">
            <tbody>
              {tasks.map(function (task) {
                const isComplete = task.status === 'COMPLETE';
                return (
                  <tr key={task.id} className="border-b border-gray-200">
                    <td className="py-2 px-4">
                      <input
                        type="checkbox"
                        checked={isComplete}
                        onChange={() => updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })}
                        className="transition-transform duration-300 ease-in-out transform scale-100 checked:scale-125 checked:bg-green-500"
                      />
                    </td>
                    <td className="py-2 px-4">
                      <span
                        className={`transition-all duration-300 ease-in-out ${isComplete ? 'line-through text-gray-400 opacity-50' : 'opacity-100'}`}
                      >
                        {task.title}
                      </span>
                    </td>
                    <td className="py-2 px-4">
                      <button
                        onClick={() => deleteTask(task.id)}
                        className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded float-right"
                      >
                        Delete
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </main>
      );
    }
    

이제 애플리케이션을 배포할 준비가 되었습니다.

8. Cloud Run에 애플리케이션 배포

  1. 아래 명령어를 실행하여 애플리케이션을 Cloud Run에 배포합니다.
    gcloud run deploy helloworld \
      --region=us-central1 \
      --source=.
    
  2. 메시지가 표시되면 YEnter를 눌러 계속 진행할지 확인합니다.
    Do you want to continue (Y/n)? Y
    

몇 분 후 애플리케이션에서 방문할 URL을 제공합니다.

URL로 이동하여 애플리케이션이 작동하는지 확인합니다. URL을 방문하거나 페이지를 새로고침할 때마다 작업 앱이 표시됩니다.

9. 축하합니다

이 실습에서는 다음 작업을 수행하는 방법을 배웠습니다.

  • PostgreSQL용 Cloud SQL 인스턴스 만들기
  • Cloud SQL 데이터베이스에 연결하는 애플리케이션을 Cloud Run에 배포

삭제

Cloud SQL에는 무료 등급이 없으며 계속 사용하면 요금이 청구됩니다. 추가 비용이 청구되지 않도록 Cloud 프로젝트를 삭제할 수 있습니다.

Cloud Run에서는 서비스를 사용하지 않을 때 비용이 청구되지 않지만 Artifact Registry에 컨테이너 이미지를 저장하는 데 요금이 부과될 수 있습니다. Cloud 프로젝트를 삭제하면 해당 프로젝트 내에서 사용되는 모든 리소스에 대한 청구가 중단됩니다.

원하는 경우 프로젝트를 삭제합니다.

gcloud projects delete $GOOGLE_CLOUD_PROJECT

cloudshell 디스크에서 불필요한 리소스를 삭제할 수도 있습니다. 다음과 같은 작업을 할 수 있습니다.

  1. Codelab 프로젝트 디렉터리를 삭제합니다.
    rm -rf ~/task-app
    
  2. 경고 이 작업은 실행취소할 수 없습니다. 공간을 확보하기 위해 Cloud Shell의 모든 항목을 삭제하려면 전체 홈 디렉터리를 삭제하면 됩니다. 보관하려는 모든 항목이 다른 곳에 저장되어 있는지 확인하세요.
    sudo rm -rf $HOME
    

계속 학습하기