Syw.Frontend

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

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

1-2. Firebase Auth โ€” ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… & ๋ณดํ˜ธ ํŽ˜์ด์ง€

๐ŸŽฏ ๋ชฉํ‘œ

  • ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ๊ตฌํ˜„
  • ๋กœ๊ทธ์ธ ์ƒํƒœ ์œ ์ง€(onAuthStateChanged)
  • ํ—ค๋”์— ๋กœ๊ทธ์ธ ์ƒํƒœ ํ‘œ์‹œ(๋กœ๊ทธ์•„์›ƒ ํฌํ•จ)
  • ๊ฐ„๋‹จํ•œ ๋ณดํ˜ธ ํŽ˜์ด์ง€(/dashboard) ๊ตฌ์„ฑ

1) ํ—ค๋” ์ปดํฌ๋„ŒํŠธ (๋กœ๊ทธ์ธ ์ƒํƒœ ํ‘œ์‹œ)

components/HeaderAuth.tsx

  • ์ „์—ญ ํ—ค๋”์—์„œ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.
  • ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฉด โ€œ์ด๋ฉ”์ผ + ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผโ€, ์•„๋‹ˆ๋ผ๋ฉด โ€œ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ๋งํฌโ€๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.
'use client';

import { useEffect, useState } from 'react';
import { auth } from '@/lib/firebase.client';
import { onAuthStateChanged, signOut, User } from 'firebase/auth';
import Link from 'next/link';

export default function HeaderAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsub = onAuthStateChanged(auth, (u) => {
      setUser(u);
      setLoading(false);
    });
    return () => unsub();
  }, []);

  if (loading) return null;

  return (
    <header style={{display:'flex',gap:12,alignItems:'center',padding:'12px 16px',borderBottom:'1px solid #eee'}}>
      <Link href="/">Home</Link>
      <Link href="/dashboard">Dashboard</Link>
      <div style={{marginLeft:'auto'}}>
        {user ? (
          <div style={{display:'flex',gap:12,alignItems:'center'}}>
            <span style={{fontSize:14}}>์•ˆ๋…•ํ•˜์„ธ์š”, {user.email}</span>
            <button onClick={() => signOut(auth)} style={{padding:'6px 10px',border:'1px solid #ddd',borderRadius:8}}>
              ๋กœ๊ทธ์•„์›ƒ
            </button>
          </div>
        ) : (
          <div style={{display:'flex',gap:12}}>
            <Link href="/login">๋กœ๊ทธ์ธ</Link>
            <Link href="/signup">ํšŒ์›๊ฐ€์ž…</Link>
          </div>
        )}
      </div>
    </header>
  );
}

๋™์ž‘ ํ๋ฆ„(์ˆœ์„œ๋Œ€๋กœ)

  1. ๋งˆ์šดํŠธ ์‹œ onAuthStateChanged(auth, cb)๋กœ Firebase์—๊ฒŒ โ€œํ˜„์žฌ ์‚ฌ์šฉ์žโ€๋ฅผ ๋ฌผ์–ด๋ด…๋‹ˆ๋‹ค.
  2. Firebase๊ฐ€ ๋กœ์ปฌ ์„ธ์…˜์„ ๋ณต์›ํ•˜๊ณ  ์‚ฌ์šฉ์ž(User | null)๋ฅผ ์ฝœ๋ฐฑ์œผ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  3. ์ „๋‹ฌ๋œ ๊ฐ’์œผ๋กœ user ์ƒํƒœ๋ฅผ ๊ฐฑ์‹ ํ•˜๊ณ , ์ดˆ๊ธฐ ๋กœ๋”ฉ ํ‘œ์‹œ์šฉ loading์„ false๋กœ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค.
  4. ์ดํ›„ ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ/ํ† ํฐ๋ณ€๊ฒฝ์ด ๋ฐœ์ƒํ•˜๋ฉด ์ฝœ๋ฐฑ์ด ์ž๋™ ์žฌํ˜ธ์ถœ๋˜์–ด UI๊ฐ€ ์ฆ‰์‹œ ๋ฐ”๋€๋‹ˆ๋‹ค.
  5. ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‚ฌ๋ผ์งˆ ๋•Œ unsub()๋กœ ๋ฆฌ์Šค๋„ˆ ํ•ด์ œ(๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€).

์ฃผ์š” ์ƒํƒœ๊ฐ’

  • user: ๋กœ๊ทธ์ธํ•œ ์œ ์ € ๊ฐ์ฒด(null์ด๋ฉด ๋น„๋กœ๊ทธ์ธ)
  • loading: Firebase๊ฐ€ ์ดˆ๊ธฐ ํŒ์ •์„ ๋๋ƒˆ๋Š”์ง€ ์—ฌ๋ถ€(๊นœ๋ฐ•์ž„ ๋ฐฉ์ง€)

๋ Œ๋” ์กฐ๊ฑด

  • loading์ผ ๋• null์„ ๋ฐ˜ํ™˜ํ•ด ์ดˆ๊ธฐ ๊นœ๋ฐ•์ž„์„ ์ค„์ž…๋‹ˆ๋‹ค.
  • user๊ฐ€ ์žˆ์œผ๋ฉด ์ด๋ฉ”์ผ/๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ, ์—†์œผ๋ฉด ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ๋งํฌ๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฒคํŠธ

  • ๋กœ๊ทธ์•„์›ƒ: signOut(auth) ํ˜ธ์ถœ๋งŒ์œผ๋กœ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€๋‹ˆ๋‹ค(๋ณ„๋„ ๋ผ์šฐํŒ… ๋ถˆํ•„์š”).

    onAuthStateChanged๊ฐ€ user=null์„ ์ „๋‹ฌ โ†’ UI๊ฐ€ ์ฆ‰์‹œ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค.

app/layout.tsx์— ํ—ค๋” ์ถ”๊ฐ€:

import './globals.scss';
import HeaderAuth from '@/components/HeaderAuth';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko">
      <body>
        <HeaderAuth />
        {children}
      </body>
    </html>
  );
}

2) ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€

  • ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋กœ๊ทธ์ธ ํผ์„ ์ œ๊ณตํ•˜๊ณ , ์„ฑ๊ณต ์‹œ ๋ณดํ˜ธ ํŽ˜์ด์ง€(/dashboard)๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

app/login/page.tsx

'use client';

import { FormEvent, useState } from 'react';
import { auth } from '@/lib/firebase.client';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { useRouter } from 'next/navigation';
import Link from 'next/link';

export default function LoginPage() {
  const router = useRouter();
  const [email, setEmail] = useState('');
  const [pw, setPw] = useState('');
  const [err, setErr] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  const onSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setErr(null);
    setLoading(true);
    try {
      await signInWithEmailAndPassword(auth, email, pw);
      router.replace('/dashboard');
    } catch (e: any) {
      setErr(e?.message ?? '๋กœ๊ทธ์ธ ์‹คํŒจ');
    } finally {
      setLoading(false);
    }
  };

  return (
    <main style={{ maxWidth: 420, margin: '40px auto', padding: 16 }}>
      <h1>๋กœ๊ทธ์ธ</h1>
      <form
        onSubmit={onSubmit}
        style={{ display: 'grid', gap: 10, marginTop: 16 }}
      >
        <input
          type='email'
          placeholder='์ด๋ฉ”์ผ'
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          style={{ padding: 10, border: '1px solid #ddd', borderRadius: 8 }}
        />
        <input
          type='password'
          placeholder='๋น„๋ฐ€๋ฒˆํ˜ธ'
          value={pw}
          onChange={(e) => setPw(e.target.value)}
          style={{ padding: 10, border: '1px solid #ddd', borderRadius: 8 }}
        />
        {err && <p style={{ color: 'crimson', fontSize: 14 }}>{err}</p>}
        <button
          disabled={loading}
          style={{ padding: '10px 12px', borderRadius: 8 }}
        >
          {loading ? '๋กœ๊ทธ์ธ ์ค‘โ€ฆ' : '๋กœ๊ทธ์ธ'}
        </button>
      </form>
      <p style={{ marginTop: 12, fontSize: 14 }}>
        ๊ณ„์ •์ด ์—†์œผ์‹ ๊ฐ€์š”? <Link href='/signup'>ํšŒ์›๊ฐ€์ž…</Link>
      </p>
    </main>
  );
}

๋™์ž‘ ํ๋ฆ„(ํผ ์ œ์ถœ)

  1. onSubmit์—์„œ ๊ธฐ๋ณธ ์ œ์ถœ ์ด๋ฒคํŠธ ๋ง‰๊ธฐ โ†’ SPA ๋‚ด ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
  2. loading์„ true๋กœ ๋ฐ”๊พธ๊ณ , ์—๋Ÿฌ ์ƒํƒœ ์ดˆ๊ธฐํ™”
  3. signInWithEmailAndPassword(auth, email, pw) ํ˜ธ์ถœ
  4. ์„ฑ๊ณต ์‹œ router.replace('/dashboard')
    • replace๋ฅผ ์“ฐ๋Š” ์ด์œ : ๋’ค๋กœ ๊ฐ€๊ธฐ๋ฅผ ๋ˆŒ๋Ÿฌ๋„ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋˜๋Œ์•„๊ฐ€์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ
  5. ์‹คํŒจ ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ err์— ์ €์žฅํ•ด ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œ
  6. ๋งˆ์ง€๋ง‰์— loading=false

์ฃผ์š” ์ƒํƒœ๊ฐ’

  • email, pw: ์ œ์–ด ์ปดํฌ๋„ŒํŠธ๋กœ ์ž…๋ ฅ๊ฐ’์„ ์ƒํƒœ๋กœ ๊ด€๋ฆฌ
  • err: Firebase ์—๋Ÿฌ ๋ฉ”์‹œ์ง€(์‹ค์„œ๋น„์Šค์—์„  ์ฝ”๋“œ๋ณ„ ํ•œ๊ธ€ํ™” ๊ถŒ์žฅ)
  • loading: ์ œ์ถœ ์ค‘ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™”/ํ…์ŠคํŠธ ๋ณ€๊ฒฝ

ํผ/UX ํฌ์ธํŠธ

  • ์ธํ’‹์€ ๊ธฐ๋ณธ์ ์ธ ์Šคํƒ€์ผ๊ณผ placeholder๋งŒ ํฌํ•จ(ํ•„์š” ์‹œ autoComplete ์ถ”๊ฐ€ ๊ถŒ์žฅ)
  • loading ๋™์•ˆ ๋ฒ„ํŠผ์„ ๋น„ํ™œ์„ฑํ™”ํ•ด ์ค‘๋ณต ์ œ์ถœ ๋ฐฉ์ง€
  • ์‹คํŒจ ์‹œ ์‚ฌ์šฉ์ž ์นœํ™”์  ๋ฌธ๊ตฌ๋กœ ๋งคํ•‘ํ•˜๋ฉด ์ดํƒˆ๋ฅ  ๊ฐ์†Œ(์˜ˆ: โ€œ์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.โ€)

๋‚ด๋น„๊ฒŒ์ด์…˜

  • ํ•˜๋‹จ์— โ€œํšŒ์›๊ฐ€์ž…โ€ ๋งํฌ๋ฅผ ์ œ๊ณตํ•ด ์ „ํ™˜ ๋™์„  ํ™•๋ณด
  • ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ ๋ณดํ˜ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜์ง€๋งŒ, ๋‚˜์ค‘์— next ์ฟผ๋ฆฌ ํŒจํ„ด(/login?next=/target)์„ ๋„์ž…ํ•˜๋ฉด ๋” ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณต๊ท€ UX ๊ตฌํ˜„ ๊ฐ€๋Šฅ

3) ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€

์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๊ณ„์ •์„ ์ƒ์„ฑํ•˜๊ณ , ์„ฑ๊ณตํ•˜๋ฉด ์ž๋™ ๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ /dashboard๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.

app/signup/page.tsx

'use client';

import { FormEvent, useState } from 'react';
import { auth } from '@/lib/firebase.client';
import { createUserWithEmailAndPassword } from 'firebase/auth';
import { useRouter } from 'next/navigation';
import Link from 'next/link';

export default function SignupPage() {
  const router = useRouter();
  const [email, setEmail] = useState('');
  const [pw, setPw] = useState('');
  const [err, setErr] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  const onSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setErr(null);
    setLoading(true);
    try {
      await createUserWithEmailAndPassword(auth, email, pw);
      router.replace('/dashboard');
    } catch (e: any) {
      setErr(e?.message ?? 'ํšŒ์›๊ฐ€์ž… ์‹คํŒจ');
    } finally {
      setLoading(false);
    }
  };

  return (
    <main style={{ maxWidth: 420, margin: '40px auto', padding: 16 }}>
      <h1>ํšŒ์›๊ฐ€์ž…</h1>
      <form
        onSubmit={onSubmit}
        style={{ display: 'grid', gap: 10, marginTop: 16 }}
      >
        <input
          type='email'
          placeholder='์ด๋ฉ”์ผ'
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          style={{ padding: 10, border: '1px solid #ddd', borderRadius: 8 }}
        />
        <input
          type='password'
          placeholder='๋น„๋ฐ€๋ฒˆํ˜ธ(6์ž ์ด์ƒ)'
          value={pw}
          onChange={(e) => setPw(e.target.value)}
          style={{ padding: 10, border: '1px solid #ddd', borderRadius: 8 }}
        />
        {err && <p style={{ color: 'crimson', fontSize: 14 }}>{err}</p>}
        <button
          disabled={loading}
          style={{ padding: '10px 12px', borderRadius: 8 }}
        >
          {loading ? '์ƒ์„ฑ ์ค‘โ€ฆ' : 'ํšŒ์›๊ฐ€์ž…'}
        </button>
      </form>
      <p style={{ marginTop: 12, fontSize: 14 }}>
        ์ด๋ฏธ ๊ณ„์ •์ด ์žˆ์œผ์‹ ๊ฐ€์š”? <Link href='/login'>๋กœ๊ทธ์ธ</Link>
      </p>
    </main>
  );
}

๋™์ž‘ ํ๋ฆ„(ํผ ์ œ์ถœ)

  1. onSubmit์—์„œ ๊ธฐ๋ณธ ๋™์ž‘ ๋ง‰๊ธฐ โ†’ loading=true, ์—๋Ÿฌ ์ดˆ๊ธฐํ™”
  2. createUserWithEmailAndPassword(auth, email, pw) ํ˜ธ์ถœ
    • Firebase ์ •์ฑ…์ƒ ๋น„๋ฐ€๋ฒˆํ˜ธ 6์ž ์ด์ƒ ํ•„์š”(placeholder๋กœ ์•ˆ๋‚ด)
  3. ์„ฑ๊ณต ์‹œ Firebase๊ฐ€ ์ฆ‰์‹œ ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ๋˜๋ฉฐ router.replace('/dashboard')
  4. ์‹คํŒจ ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ(์ด๋ฏธ ์‚ฌ์šฉ ์ค‘ ์ด๋ฉ”์ผ, ์•ฝํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋“ฑ)
  5. ๋งˆ์ง€๋ง‰์— loading=false

์ฃผ์š” ์ƒํƒœ๊ฐ’/๋ Œ๋”

  • email, pw ์ œ์–ด ์ธํ’‹ + err, loading์€ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์™€ ๋™์ผ ํŒจํ„ด
  • ์—๋Ÿฌ ์ฝ”๋“œ๋ณ„ ๋ฉ”์‹œ์ง€ ๋งคํ•‘์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ฐ€์ž… ์‹คํŒจ์˜ ์›์ธ์„ ๋ช…ํ™•ํžˆ ์•ˆ๋‚ด ๊ฐ€๋Šฅ

UX/๋ณด์•ˆ ํฌ์ธํŠธ

  • type="password"๋กœ ์ž…๋ ฅ ๊ฐ€๋ฆผ ์ฒ˜๋ฆฌ
  • ํ•„์š” ์‹œ ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ธฐ๋ณธ ํ˜•์‹ ๊ฒ€์ฆ(์ด๋ฉ”์ผ ์ •๊ทœ์‹, ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ธธ์ด) ํ›„ Firebase ํ˜ธ์ถœ โ†’ ๋ถˆํ•„์š”ํ•œ API ์š”์ฒญ ๊ฐ์†Œ
  • ๊ฐ€์ž… ์งํ›„ ํ”„๋กœํ•„ ์„ธํŒ… ํ๋ฆ„(๋‹‰๋„ค์ž„ ์„ค์ • ๋“ฑ)์œผ๋กœ ํ™•์žฅํ•˜๊ธฐ ์ข‹์Œ

4) ํด๋ผ์ด์–ธํŠธ ๋ณดํ˜ธ ๊ฐ€๋“œ ์ปดํฌ๋„ŒํŠธ

  • ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ /dashboard ๋“ฑ ๋ณดํ˜ธ๋œ ํŽ˜์ด์ง€์— ์ ‘๊ทผํ–ˆ์„ ๋•Œ, ์ž๋™์œผ๋กœ /login์œผ๋กœ ๋Œ๋ ค๋ณด๋‚ด๋Š” ํด๋ผ์ด์–ธํŠธ ๋ณดํ˜ธ ๊ฐ€๋“œ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.
  • ์ฆ‰, โ€œ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ๋งŒ ์ด ์ปดํฌ๋„ŒํŠธ ์•ˆ์ชฝ์˜ children์„ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒโ€ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค.

components/AuthGate.tsx

'use client';

import { ReactNode, useEffect, useState } from 'react';
import { auth } from '@/lib/firebase.client';
import { onAuthStateChanged } from 'firebase/auth';
import { useRouter } from 'next/navigation';

export default function AuthGate({ children }: { children: ReactNode }) {
  const router = useRouter();
  const [ready, setReady] = useState(false);
  const [authed, setAuthed] = useState(false);

  useEffect(() => {
    const unsub = onAuthStateChanged(auth, (u) => {
      setAuthed(!!u);
      setReady(true);
      if (!u) router.replace('/login');
    });
    return () => unsub();
  }, [router]);

  if (!ready) return null; // ๋˜๋Š” ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ

  return <>{authed ? children : null}</>;
}

๋™์ž‘ ์›๋ฆฌ

  1. ๋งˆ์šดํŠธ ์‹œ onAuthStateChanged(auth, callback)์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

    • Firebase๊ฐ€ ํ˜„์žฌ ๋กœ๊ทธ์ธ ์ƒํƒœ(User | null)๋ฅผ ์•Œ๋ ค์ค๋‹ˆ๋‹ค.
    • ์ด ์‹œ์ ์—์„œ auth๊ฐ€ ๋กœ์ปฌ ์„ธ์…˜์„ ๋ณต์›ํ•˜๋ฏ€๋กœ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.
  2. ์ฝœ๋ฐฑ ๋‚ด๋ถ€์—์„œ:

    • ์‚ฌ์šฉ์ž๊ฐ€ ์žˆ์œผ๋ฉด setAuthed(true)

    • ์—†์œผ๋ฉด setAuthed(false) ํ›„ router.replace('/login')์œผ๋กœ ์ด๋™์‹œํ‚ต๋‹ˆ๋‹ค.

      โ†’ ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณดํ˜ธ ํŽ˜์ด์ง€๋ฅผ ์ง์ ‘ ์ž…๋ ฅํ•ด ๋“ค์–ด์˜ค๋Š” ๊ฒƒ์„ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค.

  3. ready ์ƒํƒœ๋Š” โ€œFirebase๊ฐ€ ์ดˆ๊ธฐ ํŒ๋‹จ์„ ๋งˆ์ณค๋‹คโ€๋Š” ์˜๋ฏธ๋กœ,

    ํŒ์ •์ด ๋๋‚˜๊ธฐ ์ „์—๋Š” null์„ ๋ฐ˜ํ™˜ํ•ด ๋กœ๋”ฉ ์ค‘ ๊นœ๋ฐ•์ž„์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.


์ฃผ์š” ์ƒํƒœ๊ฐ’

์ƒํƒœ ์„ค๋ช…
ready Firebase์˜ ์ธ์ฆ ์—ฌ๋ถ€ ํŒ๋‹จ์ด ์™„๋ฃŒ๋๋Š”์ง€ ์—ฌ๋ถ€
authed ํ˜„์žฌ ๋กœ๊ทธ์ธ๋˜์–ด ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ (boolean)
  • ready๊ฐ€ false์ผ ๋•Œ๋Š” ์•„๋ฌด๊ฒƒ๋„ ๋ Œ๋”ํ•˜์ง€ ์•Š์•„

    โ€œ๋กœ๊ทธ์ธ ํŒ์ • ์ „ ํŽ˜์ด์ง€๊ฐ€ ์ž ๊น ๋ณด์˜€๋‹ค ์‚ฌ๋ผ์ง€๋Š”โ€ ํ˜„์ƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

  • authed๊ฐ€ true์ผ ๋•Œ๋งŒ ์‹ค์ œ ํŽ˜์ด์ง€ ์ฝ˜ํ…์ธ (children)๋ฅผ ๋ Œ๋”ํ•ฉ๋‹ˆ๋‹ค.


๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์ฒ˜๋ฆฌ

  • router.replace('/login')์„ ์‚ฌ์šฉํ•œ ์ด์œ ๋Š”

    ๋’ค๋กœ ๊ฐ€๊ธฐ๋ฅผ ๋ˆŒ๋Ÿฌ๋„ ๋‹ค์‹œ ๋ณดํ˜ธ ํŽ˜์ด์ง€๋กœ ๋Œ์•„์˜ค์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค.

    (์ฆ‰, ๋ธŒ๋ผ์šฐ์ € ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์ƒˆ๋กœ ๋ฎ์–ด์”Œ์›๋‹ˆ๋‹ค.)

  • router.push()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋’ค๋กœ๊ฐ€๊ธฐ ์‹œ /dashboard๋กœ ๋‹ค์‹œ ๋Œ์•„๊ฐˆ ์ˆ˜ ์žˆ์–ด

    ์ธ์ฆ ํ๋ฆ„์—์„œ๋Š” ๋ณดํ†ต replace()๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.


ํ•œ๊ณ„ (๋ณด์•ˆ ๊ด€์ )

  • ์ด ๋ฐฉ์‹์€ ํด๋ผ์ด์–ธํŠธ ๋‹จ ๋ณดํ˜ธ(Client-Side Guard) ๋กœ์ง์ž…๋‹ˆ๋‹ค.

  • ์ฆ‰, ํŽ˜์ด์ง€๊ฐ€ ๋กœ๋“œ๋œ ๋’ค JavaScript์—์„œ ์ ‘๊ทผ์„ ๋ง‰๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์—

    ์™„์ „ํ•œ ๋ณด์•ˆ์€ ์•„๋‹™๋‹ˆ๋‹ค. (์ดˆ๊ธฐ HTML์€ ์ด๋ฏธ ๋‹ค์šด๋กœ๋“œ๋œ ์ƒํƒœ)

  • 5๊ฐ•์—์„œ ๋‹ค๋ฃฐ Server Actions + Admin SDK ๋‹จ๊ณ„๋ฅผ ์ ์šฉํ•˜๋ฉด

    ์„œ๋ฒ„์—์„œ ํ† ํฐ์„ ๊ฒ€์ฆํ•ด, ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ์•„์˜ˆ ํŽ˜์ด์ง€ HTML์„ ๋ฐ›์ง€ ๋ชปํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    โ†’ ๊ทธ๋•Œ๊ฐ€ ์ง„์งœ โ€œ์„œ๋ฒ„ ๋ณดํ˜ธ(Server-Side Protection)โ€ ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค.

4) ๋Œ€์‹œ๋ณด๋“œ ํŽ˜์ด์ง€(๋กœ๊ทธ์ธ ํ•„์š”)

  • AuthGate๋กœ ๋ณดํ˜ธ๋œ ์‹ค์ œ โ€œ๋กœ๊ทธ์ธ ์ „์šฉ ํŽ˜์ด์ง€โ€์ž…๋‹ˆ๋‹ค.
  • ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ ์ด ํŽ˜์ด์ง€์˜ ๋‚ด์šฉ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

app/dashboard/page.tsx

'use client';

import AuthGate from '@/components/AuthGate';

export default function DashboardPage() {
  return (
    <AuthGate>
      <main style={{ maxWidth: 720, margin: '40px auto', padding: 16 }}>
        <h1>๋Œ€์‹œ๋ณด๋“œ</h1>
        <p>๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ ๋ณผ ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค.</p>
      </main>
    </AuthGate>
  );
}

๊ตฌ์กฐ ์„ค๋ช…

<AuthGate>
  <main> ... </main>
</AuthGate>
  • <AuthGate>๊ฐ€ ๊ฐ์‹ธ๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์—,

    ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋Š” ๋‚ด๋ถ€ ์ฝ˜ํ…์ธ (main)์„ ์ „ํ˜€ ๋ณผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

    AuthGate๊ฐ€ /login์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ์‹œํ‚ค๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๋™์ž‘ ์ˆœ์„œ

  1. ์‚ฌ์šฉ์ž๊ฐ€ /dashboard URL์— ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.
  2. ํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋ง๋˜๋ฉด์„œ AuthGate๊ฐ€ ๋จผ์ € ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
  3. Firebase Auth๊ฐ€ ํ˜„์žฌ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  4. ๋™์ž‘ ์„ค๋ช…
    • โœ… ๋กœ๊ทธ์ธ๋˜์–ด ์žˆ์œผ๋ฉด โ†’ <main> ๋‚ด์šฉ์„ ๋ Œ๋”ํ•ฉ๋‹ˆ๋‹ค.
    • โŒ ๋กœ๊ทธ์ธ ์•ˆ ๋˜์–ด ์žˆ์œผ๋ฉด โ†’ /login์œผ๋กœ ์ด๋™์‹œํ‚ต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค

์‹œ๋‚˜๋ฆฌ์˜ค ๊ฒฐ๊ณผ
๋กœ๊ทธ์ธ ์•ˆ ํ•œ ์ƒํƒœ๋กœ /dashboard ์ ‘๊ทผ /login์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
ํšŒ์›๊ฐ€์ž… ์งํ›„ ์ž๋™ ๋กœ๊ทธ์ธ ์ƒํƒœ /dashboard ์ ‘๊ทผ ์„ฑ๊ณต
์ƒˆ๋กœ๊ณ ์นจํ•ด๋„ ์„ธ์…˜ ์œ ์ง€ Firebase๊ฐ€ ์ž๋™์œผ๋กœ ๋ณต์›
๋กœ๊ทธ์•„์›ƒ ํ›„ /dashboard ์ ‘๊ทผ ๋‹ค์‹œ /login์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ

5) ๊ฐ„๋‹จ SCSS

app/globals.scss

:root {
  --radius: 10px;
}
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  font-family: system-ui, -apple-system, Segoe UI, Roboto, 'Apple SD Gothic Neo',
    sans-serif;
}
li {
  list-style-type: none;
}
a {
  text-decoration: none;
  color: #1a73e8;
}
input,
button {
  font: inherit;
}
button {
  cursor: pointer;
}

6) ๋™์ž‘ ํ™•์ธ

  • /signup์—์„œ ํšŒ์›๊ฐ€์ž… โ†’ ์ž๋™ ๋กœ๊ทธ์ธ โ†’ /dashboard ์ง„์ž…
  • ํ—ค๋”์— ์ด๋ฉ”์ผ ํ‘œ์‹œ, ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ƒํƒœ ๋ณ€๊ฒฝ ํ™•์ธ
  • /login์—์„œ ๋กœ๊ทธ์ธ ํ›„ /dashboard ์ ‘๊ทผ ๊ฐ€๋Šฅ

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

GitHub - heroyooi/nextjs-firebase at ch1_2

๐Ÿ’ฌ ๋Œ“๊ธ€

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