Syw.Frontend

๐Ÿš€ Next.js + Firebase๋กœ ๋งŒ๋“œ๋Š” ํ’€์Šคํƒ ์›น์•ฑ ์‹ค์ „ ํ”„๋กœ์ ํŠธ

1๋‹จ๊ณ„. Next.js + Firebase ์‹ค์ „ ์•ฑ

1-1. ํ”„๋กœ์ ํŠธ & Firebase ๊ธฐ๋ณธ ์„ธํŒ…

๐ŸŽฏ ๋ชฉํ‘œ

  • Next.js(App Router) ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
  • Firebase Console์—์„œ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ & ์„œ๋น„์Šค ์ผœ๊ธฐ(Auth/Firestore/Storage)
  • .env.local ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •
  • firebase.client.ts ์ดˆ๊ธฐํ™” ํŒŒ์ผ ๊ตฌ์„ฑ
  • Firestore์— ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์“ฐ๊ธฐ/์ฝ๊ธฐ๊นŒ์ง€ ํ™•์ธ

1) Next.js ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

npx create-next-app@latest nextjs-firebase
# โœ” TypeScript: Yes
# โœ” ESLint: Yes
# โœ” Tailwind: No (SCSS ์‚ฌ์šฉํ•  ์˜ˆ์ •)
# โœ” App Router: Yes
# โœ” src/: ์„ ํƒ์€ ์ž์œ  (์—ฌ๊ธฐ์„  ๊ธฐ๋ณธ ๋ฃจํŠธ ์‚ฌ์šฉ)
# โœ” import alias: Yes (@/*)
cd nextjs-firebase
npm i
npm i -D sass

โ€ป ์ถ”ํ›„ ๊ฒฝ๋กœ๋ฅผ ์„ค๋ช…ํ•  ๋•Œ ์ตœ์ƒ๋‹จ src๋Š” ์ œ์™ธํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

styles/globals.scss๋ฅผ app/layout.tsx์— ์ถ”๊ฐ€:

// app/layout.tsx
import './globals.scss';

2) Firebase ์ฝ˜์†” ์„ค์ •

  1. [console.firebase.google.com]์—์„œ ์ƒˆ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ (์˜ˆ: nextjs-firebase-course)
  2. Authentication โ†’ Sign-in method โ†’ Email/Password ํ™œ์„ฑํ™”(+ Google Sign-in์€ 3๊ฐ•์—์„œ)
  3. Firestore โ†’ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งŒ๋“ค๊ธฐ โ†’ ๋ชจ๋“œ: ํ…Œ์ŠคํŠธ(๊ฐœ๋ฐœ ๋‹จ๊ณ„) โ†’ ์œ„์น˜: asia-northeast3(์„œ์šธ)
  4. Storage โ†’ ์‹œ์ž‘ํ•˜๊ธฐ
    1. ์Šคํ† ๋ฆฌ์ง€๋ถ€ํ„ฐ๋Š” ์š”๊ธˆ ๊ฒฐ์ œ๊ฐ€ ํ•„์š”ํ•˜๋ฏ€๋กœ GCP์˜ Cloud Billing ๊ณ„์ •์ด ์—ฐ๋™๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  5. ํ”„๋กœ์ ํŠธ ์„ค์ • โ†’ ์›น ์•ฑ ์ถ”๊ฐ€ โ†’ Firebase SDK snippet์—์„œ apiKey ๋“ฑ ์„ค์ •๊ฐ’ ํ™•์ธ

A. ์ƒˆ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

B. Authentication ์„ค์ •

C. Firestore ์„ค์ •

Firestore Standard vs Enterprise: Technical Comparison (by ChatGPT 5)

๐Ÿ” 1. Firestore Standard vs Enterprise ๊ตฌ์กฐ์  ์ฐจ์ด

ํ•ญ๋ชฉ Standard Enterprise
์Šคํ† ๋ฆฌ์ง€ ๊ตฌ์กฐ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ (SSD + HDD) ์ „๋ฉด SSD
์ฟผ๋ฆฌ ์—”์ง„ ํ‘œ์ค€ ๊ณ ๊ธ‰ (์—”ํ„ฐํ”„๋ผ์ด์ฆˆ์šฉ, MongoDB ํ˜ธํ™˜ ํฌํ•จ)
๋ฌธ์„œ ํฌ๊ธฐ ํ•œ๋„ 1MiB 4MiB
๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ ๋™์ผ ๋™์ผ
์ง€์› ํ™˜๊ฒฝ ๋™์ผ ๋™์ผ

์—ฌ๊ธฐ์„œ ๊ฐ€์žฅ ์ง์ ‘์ ์ธ ์„ฑ๋Šฅ ์ฐจ์ด๋ฅผ ๋งŒ๋“œ๋Š” ๋ถ€๋ถ„์ด ๋ฐ”๋กœ "์Šคํ† ๋ฆฌ์ง€์™€ ์ฟผ๋ฆฌ ์—”์ง„"์ž…๋‹ˆ๋‹ค.


โšก 2. ์†๋„ ์ฐจ์ด์˜ ์›์ธ

  1. ์Šคํ† ๋ฆฌ์ง€ ํƒ€์ž…

    • ํ‘œ์ค€: ์ผ๋ถ€ ๋ฐ์ดํ„ฐ๋Š” HDD(ํ•˜๋“œ๋””์Šคํฌ) ๊ธฐ๋ฐ˜์œผ๋กœ ์ €์žฅ๋˜์–ด ์žˆ์–ด, ์ฝ๊ธฐ/์“ฐ๊ธฐ ์†๋„๊ฐ€ ์ƒ๋Œ€์ ์œผ๋กœ ๋А๋ฆฝ๋‹ˆ๋‹ค.
    • ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ: ์ „๋ถ€ SSD ๊ธฐ๋ฐ˜์œผ๋กœ ์šด์˜๋˜์–ด, ๋Œ€๋Ÿ‰์˜ ๋žœ๋ค ์•ก์„ธ์Šค ์ฟผ๋ฆฌ๋‚˜ ์ธ๋ฑ์Šค ์กฐํšŒ ์‹œ ์†๋„๊ฐ€ ๋น ๋ฆ…๋‹ˆ๋‹ค.
    • ํŠนํžˆ ๋Œ€์šฉ๋Ÿ‰ ๋ฌธ์„œ ์ง‘ํ•ฉ์„ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋Š” ๋ณตํ•ฉ ์ฟผ๋ฆฌ์—์„œ SSD์˜ ์ด์ ์ด ๋‘๋“œ๋Ÿฌ์ง‘๋‹ˆ๋‹ค.
  2. ์ฟผ๋ฆฌ ์—”์ง„ ์ฐจ์ด

    • Enterprise๋Š” MongoDB ํ˜ธํ™˜ํ˜• ์ฟผ๋ฆฌ ์—”์ง„์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ๋ณต์žกํ•œ ํ•„ํ„ฐ๋ง๊ณผ ์กฐ์ธ ์„ฑ๊ฒฉ์˜ ์ฟผ๋ฆฌ์— ์ตœ์ ํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

    • Standard๋ณด๋‹ค ์ธ๋ฑ์Šค ๊ฒ€์ƒ‰ ๋ฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๊ณผ์ •์ด ๊ฐœ์„ ๋˜์–ด ์žˆ์–ด,

      ๊ฐ™์€ ์ฟผ๋ฆฌ๋ผ๋„ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ฐ€ 10~30% ๋น ๋ฅด๊ฒŒ ์‘๋‹ตํ•˜๋Š” ์‚ฌ๋ก€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

  3. ์บ์‹ฑ ๋ฐ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜

    • Enterprise๋Š” SSD ๊ธฐ๋ฐ˜ ์บ์‹ฑ ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ตœ๊ทผ ์ ‘๊ทผํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋” ๋น ๋ฅด๊ฒŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
    • ๋Œ€๋Ÿ‰์˜ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ(์˜ˆ: ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ, ๋กœ๊ทธ ์‹œ์Šคํ…œ)์ผ์ˆ˜๋ก ์ฐจ์ด๊ฐ€ ๋‚ฉ๋‹ˆ๋‹ค.

๐Ÿงช 3. ์‹ค์ œ ์ฒด๊ฐ ์˜ˆ์‹œ

์‹œ๋‚˜๋ฆฌ์˜ค Standard Enterprise
๊ฐ„๋‹จํ•œ where() ์ฟผ๋ฆฌ ๊ฑฐ์˜ ๋™์ผ ๋™์ผ
๋ณตํ•ฉ ์ฟผ๋ฆฌ (2~3๊ฐœ ํ•„ํ„ฐ + ์ •๋ ฌ) ์•ฝ๊ฐ„ ๋А๋ฆผ ์•ฝ 1.2~1.5๋ฐฐ ๋น ๋ฆ„
๋Œ€๋Ÿ‰ ๋ฌธ์„œ ์ฝ๊ธฐ (10๋งŒ ๊ฑด ์ด์ƒ) ์‘๋‹ต ์ง€์—ฐ ๊ฐ€๋Šฅ ์ผ์ •ํ•˜๊ณ  ๋น ๋ฆ„
์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ๋นˆ๋„ ๋†’์„ ๋•Œ ๊ฐ„ํ—์  ์ง€์—ฐ ๋ฐœ์ƒ ์•ˆ์ •์ 

๐Ÿงญ 4. ์ •๋ฆฌ

ํ•ญ๋ชฉ ์†๋„ ์˜ํ–ฅ ์—ฌ๋ถ€ ์„ค๋ช…
์Šคํ† ๋ฆฌ์ง€ (SSD vs HDD) โœ… ๋†’์Œ I/O ์„ฑ๋Šฅ ์ง์ ‘ ์ฐจ์ด
์ฟผ๋ฆฌ ์—”์ง„ (๊ณ ๊ธ‰/ํ‘œ์ค€) โœ… ์ค‘๊ฐ„ ์ธ๋ฑ์Šค ์ตœ์ ํ™” ๊ฐœ์„ 
๋ฌธ์„œ ํฌ๊ธฐ ํ•œ๋„ โš™๏ธ ๊ฐ„์ ‘์  ํฐ ๋ฌธ์„œ๋ฅผ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
๋ฐ์ดํ„ฐ ๋ณดํ˜ธ ๊ธฐ๋Šฅ โŒ ์—†์Œ ์†๋„์™€ ๋ฌด๊ด€

โœ… ๊ฒฐ๋ก 

  • ์†๋„ ์ฐจ์ด๋Š” ๋ถ„๋ช…ํžˆ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

    ํŠนํžˆ SSD ๊ธฐ๋ฐ˜ ์Šคํ† ๋ฆฌ์ง€์™€ ๊ณ ๊ธ‰ ์ฟผ๋ฆฌ ์—”์ง„ ๋•๋ถ„์— ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๋Š” ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ์…‹ ์ฒ˜๋ฆฌ ์‹œ ๋” ๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์ž…๋‹ˆ๋‹ค.

  • ๋‹จ, ์ผ๋ฐ˜์ ์ธ ์›น/์•ฑ ์ˆ˜์ค€(์ˆ˜์ฒœ~์ˆ˜๋งŒ ๋ฌธ์„œ)์˜ CRUD๋ผ๋ฉด ์ฒด๊ฐ ์ฐจ์ด๋Š” ๊ฑฐ์˜ ์—†์Šต๋‹ˆ๋‹ค.

  • ํ•˜์ง€๋งŒ ๋Œ€์‹œ๋ณด๋“œ, ๋ถ„์„, ๋กœ๊ทธ, ๋ฆฌํฌํŒ… ๋“ฑ ๋Œ€๋Ÿ‰ ์ฟผ๋ฆฌ ์ค‘์‹ฌ์˜ ์‹œ์Šคํ…œ์ด๋ผ๋ฉด Enterprise ์—…๊ทธ๋ ˆ์ด๋“œ๊ฐ€ ์†๋„ ํ–ฅ์ƒ์— ํ™•์‹คํžˆ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

D. Storage ์„ค์ •

Cloud Billing ๊ฒฐ์ œ ๊ณ„์ • ์—ฐ๋™์„ ์œ„ํ•ด์„œ ํ”„๋กœ์ ํŠธ ์—…๊ทธ๋ ˆ์ด๋“œ๋ฅผ ํ•ฉ๋‹ˆ๋‹ค. Cloud Billing ๊ฒฐ์ œ ๊ณ„์ • ์ƒ์„ฑ ๊ณผ์ •์€ ์ƒ๋žตํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ํ•ด์™ธ ๊ฒฐ์ œ๊ฐ€ ๊ฐ€๋Šฅํ•œ ์นด๋“œ ์—ฐ๋™์ด ํ•„์š”ํ•˜๋ฉฐ ์‹ค์Šต์„ ์œ„ํ•œ ์Šคํ† ๋ฆฌ์ง€ ๋น„์šฉ์„ ์—ฐ๋™ํ•ด๋„ ๋Œ€๋Ÿ‰์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ์˜ฌ๋ฆฌ์ง€ ์•Š๋Š” ์ด์ƒ ๋น„์šฉ ๊ฒฐ์ œ๊ฐ€ ๊ฑฐ์˜ ๋˜์ง€ ์•Š์œผ๋‹ˆ ์ฐธ๊ณ  ํ•ด์ฃผ์„ธ์š”. (๋ณด์•ˆ ์„ค์ •๋„ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์€ ์Šคํ† ๋ฆฌ์ง€ ๋ถ€๋ถ„ ๊ฐ•์˜๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋” ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.)

์ƒ์„ฑํ•œ ๊ฒฐ์ œ ๊ณ„์ •์„ ์—ฐ๊ฒฐํ•ด์„œ ์š”๊ธˆ์ œ๋ฅผ Spark ์—์„œ Blaze๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

์†Œ์•ก์ด๋ผ๋„ ์š”๊ธˆ์ด ๋ฐœ์ƒํ•˜๋ฉด ์ด๋ฉ”์ผ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ์˜ˆ์‚ฐ ๊ธˆ์•ก์„ 25์›์œผ๋กœ ์„ค์ • ํ•ฉ๋‹ˆ๋‹ค.

์ขŒ์ธก ํ•˜๋‹จ์— ๋ณ€๊ฒฝ๋œ ์š”๊ธˆ์ œ๋ฅผ ํ™•์ธํ•˜๊ณ  ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

์œ ๋ฃŒ ์œ„์น˜์˜ ASIA-NORTHEAST3 (์„œ์šธ)๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์†๋„๊ฐ€ ๋น ๋ฅด์ง€๋งŒ ์ผ๋‹จ ๋ฌด๋ฃŒ ์œ„์น˜(US-CENTRAL1)๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

E. ์›น์•ฑ ์ถ”๊ฐ€ ๋ฐ APIํ‚ค ํ™•์ธ

์›น ์•ฑ์„ ๋“ฑ๋กํ•˜๊ณ  ๋‚˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด apiํ‚ค๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3) ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •

ํŒŒ์ด์–ด๋ฒ ์ด์Šค ์„ค์น˜

npm i firebase

๋ฃจํŠธ์— .env.local ์ƒ์„ฑ:

# Client์—์„œ ํ•„์š”ํ•œ ๊ฐ’์€ ๋ฐ˜๋“œ์‹œ NEXT_PUBLIC_ ์ ‘๋‘์‚ฌ
NEXT_PUBLIC_FIREBASE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=1234567890
NEXT_PUBLIC_FIREBASE_APP_ID=1:1234567890:web:abcdefg1234567

4) Firebase ์ดˆ๊ธฐํ™” ํŒŒ์ผ

lib/firebase.client.ts ์ƒ์„ฑ:

import { initializeApp, getApps, getApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET!,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
  // messagingSenderId, measurementId ํ•„์š” ์‹œ ์ถ”๊ฐ€
};

const app = getApps().length ? getApp() : initializeApp(firebaseConfig);

export const auth = getAuth(app);
export const db = getFirestore(app);
export const storage = getStorage(app);

์„œ๋ฒ„์—์„œ Admin SDK๋ฅผ ์“ธ ๊ณ„ํš์ด๋ฉด ์ถ”ํ›„ lib/firebase.admin.ts๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค(5๊ฐ• ์˜ˆ์ •).

์ง€๊ธˆ์€ ํด๋ผ์ด์–ธํŠธ SDK๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

5) Firestore Rules (๊ฐœ๋ฐœ์šฉ)

๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ๋Š” ํ…Œ์ŠคํŠธ ๊ทœ์น™์œผ๋กœ ์‹œ์ž‘ํ•˜๊ณ , 7๊ฐ•์—์„œ ํ”„๋กœ๋•์…˜ ๊ทœ์น™์œผ๋กœ ๊ฐ•ํ™”ํ•ฉ๋‹ˆ๋‹ค.

6) ํ…Œ์ŠคํŠธ UI: ๊ธ€ ์ถ”๊ฐ€ & ๋ชฉ๋ก ๋ณด๊ธฐ

app/page.tsx์— ๊ฐ„๋‹จํ•œ ํผ & ๋ฆฌ์ŠคํŠธ(ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ) ์ถ”๊ฐ€:

'use client';

import { FormEvent, useEffect, useState } from 'react';
import { db } from '@/lib/firebase.client';
import {
  collection,
  addDoc,
  serverTimestamp,
  getDocs,
  query,
  orderBy,
} from 'firebase/firestore';

type Post = {
  id: string;
  title: string;
  createdAt?: { seconds: number; nanoseconds: number } | null;
};

export default function HomePage() {
  const [title, setTitle] = useState('');
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState(false);

  const fetchPosts = async () => {
    const q = query(collection(db, 'posts'), orderBy('createdAt', 'desc'));
    const snap = await getDocs(q);
    const list: Post[] = snap.docs.map((d) => ({
      id: d.id,
      ...(d.data() as any),
    }));
    setPosts(list);
  };

  useEffect(() => {
    fetchPosts();
  }, []);

  const onSubmit = async (e: FormEvent) => {
    e.preventDefault();
    if (!title.trim()) return;
    setLoading(true);
    try {
      await addDoc(collection(db, 'posts'), {
        title: title.trim(),
        createdAt: serverTimestamp(),
      });
      setTitle('');
      await fetchPosts();
    } finally {
      setLoading(false);
    }
  };

  return (
    <main style={{ maxWidth: 720, margin: '40px auto', padding: 16 }}>
      <h1>Next.js + Firebase โ€” Test</h1>

      <form
        onSubmit={onSubmit}
        style={{ display: 'flex', gap: 8, marginTop: 16 }}
      >
        <input
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder='์ œ๋ชฉ ์ž…๋ ฅ'
          style={{
            flex: 1,
            padding: 10,
            border: '1px solid #ddd',
            borderRadius: 8,
          }}
        />
        <button
          type='submit'
          disabled={loading}
          style={{ padding: '10px 16px', borderRadius: 8 }}
        >
          {loading ? '์ €์žฅ ์ค‘โ€ฆ' : '์ถ”๊ฐ€'}
        </button>
      </form>

      <ul style={{ marginTop: 24, display: 'grid', gap: 12 }}>
        {posts.map((p) => (
          <li
            key={p.id}
            style={{ border: '1px solid #eee', padding: 12, borderRadius: 8 }}
          >
            <strong>{p.title}</strong>
          </li>
        ))}
      </ul>
    </main>
  );
}

7) ์‹คํ–‰ & ํ™•์ธ

npm run dev
# http://localhost:3000 ์ ‘์† โ†’ ์ œ๋ชฉ ์ž…๋ ฅ ํ›„ '์ถ”๊ฐ€' โ†’ Firestore์— ๋ฌธ์„œ ์ƒ์„ฑ๋˜๋Š”์ง€ ํ™•์ธ

์‹ค์Šต ์ €์žฅ์†Œ

GitHub - heroyooi/nextjs-firebase at ch1_1

๐Ÿ’ฌ ๋Œ“๊ธ€

    โ€ป ๋กœ๊ทธ์ธ ํ›„ ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.