18화 | [AI와 1인기업] 유료 회원 멤버십 구축: Firebase Auth 연동을 통한 이메일 비밀번호 회원가입 및 마이페이지 라이선스 키 조회 화면 개발

유료 회원 멤버십 구축: Firebase Auth 연동을 통한 이메일 비밀번호 회원가입 및 마이페이지 라이선스 키 조회 화면 개발 (18화)

안녕하세요! AI 크루들과 무자본 IT 1인 기업을 꾸려가고 있는 크리에이티브 디렉터 DAVID입니다.

지난 17화에서는 비결제 사용자가 유료 소프트웨어(.ex5, .zip)의 다운로드 다이렉트 주소를 획득해 직접 접근하는 침입 시도를 차단하기 위해, 서버 통신의 최전선에서 요청을 가로채 판독하는 'Next.js 미들웨어(Middleware) 보안 관문'을 완성했습니다.

이로써 보안의 기본 뼈대는 단단하게 조여졌습니다. 이제 사용자가 내 웹사이트에 정식으로 가입 및 로그인하여 본인이 구매한 소프트웨어 라이선스 목록을 한눈에 조회하고, 남은 구독 유효 기한을 실시간으로 확인 및 갱신할 수 있는 '유료 회원 마이페이지 대시보드'를 탑재할 단계입니다. 오늘 구글의 강력한 초경량 인증 보안망인 Firebase Auth를 연동해 가입 폼부터 실시간 조회 UI까지 얹는 회원제 시스템의 정석을 공개합니다!





1. 왜 자체 로그인 DB 구축 대신 'Firebase Auth'인가?

1인 창업 기업가가 사용자의 비밀번호 암호화(Hashing), 세션 토큰 유지, 비밀번호 찾기(재설정) 메일 발송, 해킹 방어용 보안 인프라를 백엔드에 직접 생 코딩하는 것은 비효율적일 뿐 아니라 심각한 보안 리스크를 안게 됩니다. Firebase Auth를 채택하면 다음과 같은 엔터프라이즈급 안정성을 무상으로 확보할 수 있습니다.

  • 비밀번호 암호화 무상 처리: 업계 최고 수준의 해시 알고리즘(Scrypt)이 적용되어 비밀번호가 완벽하게 안전한 보안 서버에 격리 보관됩니다.
  • 손쉬운 메일링 기능 연동: '비밀번호 재설정 메일', '이메일 주소 인증' 등의 시스템 이메일 템플릿이 대시보드 클릭 몇 번으로 자동 기동됩니다.
  • 유연한 소셜 로그인 확장: 추후 구글, 깃허브, 카카오 로그인 기능을 복잡한 OAuth 코딩 없이 즉시 덧붙일 수 있습니다.

2. 유료 회원 마이페이지 대시보드 라우팅 및 상태 설계안

인증된 회원만 접근할 수 있는 마이페이지(/mypage)에서 유저에게 결제된 라이선스 자산을 시각적으로 안전하게 노출해 주는 프론트엔드-백엔드 3단계 상태 데이터 흐름 구조입니다.

처리 프로세스 작동 방식 및 라이브러리 마케팅 및 UX 디테일
1단계: 클라이언트 로그인 signInWithEmailAndPassword 사용자가 이메일/비번을 입력하면 Firebase SDK가 인증 토큰(JWT)을 획득하고, 쿠키에 세션을 자동 바인딩합니다.
2단계: 유료 라이선스 조회 Firestore WHERE query 로그인된 사용자의 식별 정보(Email)를 기준으로 DB를 쿼리하여, 해당 이메일로 자동 발급되었던 라이선스 키 목록을 긁어옵니다.
3단계: 잔여 기한 및 대수 렌더링 React State & Tailwind UI 키 값, 작동 상태(ACTIVE/EXPIRED), 남은 날짜 수(D-Day), 등록된 기기 대수 목록을 한눈에 알기 쉽게 대시보드 카드 형태로 출력합니다.

3. Firebase Auth 연동 및 라이선스 조회 마이페이지 실전 코드

유저가 로그인하여 자신의 라이선스 내역을 서버 사이드와 클라이언트 사이드에서 안전하게 렌더링해내는 Next.js 16 리액트 컴포넌트 실전 소스코드입니다.

'use client';
import { useState, useEffect } from 'react';
import { getAuth, onAuthStateChanged, signOut } from 'firebase/auth';
import { initializeApp } from 'firebase/app';

// Firebase 프로젝트 환경설정 객체 초기화
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,
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

export default function MyPage() {
    const [user, setUser] = useState(null);
    const [licenses, setLicenses] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
            if (currentUser) {
                setUser(currentUser);
                // 로그인 완료 시 백엔드 API를 찔러 유저 이메일에 귀속된 라이선스 조회
                try {
                    const res = await fetch(`/api/user/licenses?email=${currentUser.email}`);
                    const data = await res.json();
                    setLicenses(data.licenses || []);
                } catch (err) {
                    console.error("라이선스 조회 실패:", err);
                }
            } else {
                // 비로그인 상태 시 상점으로 리다이렉트
                window.location.href = '/shop';
            }
            setLoading(false);
        });
        return () => unsubscribe();
    }, []);

    const handleLogout = async () => {
        await signOut(auth);
        window.location.href = '/';
    };

    if (loading) return <div style={{ color: '#fff', textAlign: 'center', padding: '100px' }}>인증 및 라이선스 데이터를 로드하는 중...</div>;

    return (
        <div style={{ maxWidth: '800px', margin: '50px auto', padding: '20px', background: '#0a0a0a', border: '1px solid #222', borderRadius: '8px' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #222', paddingBottom: '15px' }}>
                <h1 style={{ color: '#fff', fontSize: '1.8rem' }}>마이 라이선스 대시보드</h1>
                <button onClick={handleLogout} style={{ background: '#e74c3c', color: '#fff', border: 'none', padding: '8px 15px', borderRadius: '4px', cursor: 'pointer' }}>로그아웃</button>
            </div>

            {/* 사용자 계정 정보 영역 */}
            <div style={{ marginTop: '20px', color: '#aaa' }}>
                <p>계정 정보: <strong style={{ color: '#fff' }}>{user?.email}</strong></p>
            </div>

            {/* 보유 라이선스 리스트 렌더링 */}
            <div style={{ marginTop: '30px' }}>
                <h2 style={{ color: '#f39c12', fontSize: '1.3rem', marginBottom: '15px' }}>보유 중인 정품 라이선스 ({licenses.length})</h2>
                
                {licenses.length === 0 ? (
                    <p style={{ color: '#666' }}>현재 발급된 라이선스가 없습니다. 상점에서 프로그램을 구매해 주세요.</p>
                ) : (
                    licenses.map((lic) => {
                        const isExpired = new Date() > new Date(lic.expiredAt);
                        return (
                            <div key={lic.licenseKey} style={{ border: '1px solid #333', padding: '15px', borderRadius: '6px', marginBottom: '15px', background: '#111' }}>
                                <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '10px' }}>
                                    <span style={{ fontFamily: 'monospace', fontSize: '1.1rem', color: '#fff', fontWeight: 'bold' }}>{lic.licenseKey}</span>
                                    <span style={{ padding: '3px 8px', borderRadius: '3px', fontSize: '0.8rem', background: isExpired ? '#c0392b' : '#27ae60', color: '#fff' }}>
                                        {isExpired ? 'EXPIRED(만료)' : 'ACTIVE(정상)'}
                                    </span>
                                </div>
                                <p style={{ margin: '5px 0', fontSize: '0.9rem', color: '#999' }}>대상 상품: <strong style={{ color: '#ccc' }}>{lic.productId}</strong></p>
                                <p style={{ margin: '5px 0', fontSize: '0.9rem', color: '#999' }}>만료 기한: {new Date(lic.expiredAt).toLocaleDateString()}</p>
                                <p style={{ margin: '5px 0', fontSize: '0.9rem', color: '#999' }}>등록된 기기 수: {lic.activeDevices?.length || 0} / {lic.maxDevices} 대</p>
                            </div>
                        );
                    })
                )}
            </div>
        </div>
    );
}

4. 18화를 마치며: 서비스형 소프트웨어(SaaS)로의 비상

Firebase Auth 회원제 시스템과 실시간 라이선스 대시보드 마이페이지를 연결함으로써, 이제 우리의 비즈니스는 단순 파일 일회성 전송 상업 모델을 벗어나 사용자가 직접 본인의 권한을 유연하게 제어하는 진정한 '서비스형 소프트웨어(SaaS) 플랫폼'의 기틀을 완벽하게 완성했습니다.

유료 회원들이 웹에서 본인의 사용 기한을 볼 수 있게 가독성을 확보했습니다. 하지만 이 마이페이지가 정상 작동하려면 사용자가 유료 라이선스를 실제로 결제했을 때, 자동으로 DB 테이블이 갱신되고 사용자가 결제한 이메일과 똑같은 메일 주소로 회원 가입 계정이 즉시 매핑되어 연동되는 '백엔드 마이크로 통합'이 매끄럽게 돌아야 합니다.

다음 19화에서는 이메일을 통한 원클릭 매직링크 회원가입 기법을 연동하여 비밀번호 기억의 장벽마저 파괴하고, 결제와 이메일 계정을 1초 만에 자동 매핑해 주는 "5부 유료 회원 멤버십 구축: 결제 이메일과 Firebase Auth 계정 연동 및 비밀번호 없는 로그인(Magic Link) 구현"을 심층적으로 정복해 보겠습니다.

구글 파이어베이스 보안 가입망 위에서 사용자 락인(Lock-in) 멤버십을 견고히 형성해 보세요. 다음 편에서 뵙겠습니다! 😊🚀


*글쓴이: AI와 함께 무한한 가치를 만들어가는 Creative Director, DAVID.*

댓글

이 블로그의 인기 게시물

프롤로그 | AI 1인 기업 창업 가이드: 코딩 없이 시작하는 IT 스타트업 20부작 로드맵

23화 | [AI와 1인기업] AI 비즈니스 에이전트 이식: OpenAI Assistants RAG 엔진 기반의 텔레그램 무인 기술지원(CS) 자동 챗봇 구축