logo

DowanKim

4. 14명의 개인화된 데이터, 어떻게 연결하지

2025년 8월 30일

졸업논문 대체 웹사이트

14명의 디자이너 정보 Firebase 설정 및 데이터 관리

목표

14명의 디자이너 정보를 Firebase Realtime Database에 저장하고, React Query로 상태를 관리하며, 컴포넌트에서 표시합니다.

구현 흐름은 다음과 같습니다.

1. Firebase 초기화: 환경 변수로 설정, 중복 초기화 방지

2. 데이터 구조: designerInfo/{이름} 계층 구조

3. 서비스 함수: fetchDesignerCards, fetchDesignerDetailByName, fetchDesignerNamesSorted

4. React Query: useSuspenseQuery로 캐싱 및 로딩 처리

5. 컴포넌트: Suspense + ErrorBoundary로 로딩/에러 처리

6. 네비게이션: URL 쿼리 파라미터로 디자이너 선택

아래는 구현했던 과정과, 그 과정에서 생긴 문제와 해결을 정리한 글입니다.


1단계: Firebase 초기화 및 설정

문제: Firebase 프로젝트 연결

Firebase 서비스를 사용하려면 프로젝트 설정과 초기화가 필요합니다.

해결: 환경 변수 기반 Firebase 설정

import { initializeApp, getApp, getApps, type FirebaseApp } from "firebase/app"; import { getAnalytics, isSupported, type Analytics } from "firebase/analytics"; // Firebase configuration // Note: In client-side apps, this configuration is not a secret. // Prefer configuring via Vite env vars (VITE_*) with fallbacks for local dev. const firebaseConfig = { apiKey: import.meta.env.VITE_FIREBASE_API_KEY, authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID, appId: import.meta.env.VITE_FIREBASE_APP_ID, measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID, databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL, }; // Initialize (and reuse on HMR) the Firebase app export const app: FirebaseApp = getApps().length ? getApp() : initializeApp(firebaseConfig); // Initialize Analytics only in supported browser environments export const analyticsPromise: Promise<Analytics | null> = typeof window !== "undefined" ? isSupported().then(supported => (supported ? getAnalytics(app) : null)) : Promise.resolve(null); export default app;
  • 환경 변수로 설정을 관리해 보안과 유연성을 확보합니다.
  • getApps().length로 중복 초기화를 방지합니다.
  • databaseURL로 Realtime Database 연결을 지정합니다.

2단계: Firebase Realtime Database 구조 설계

문제: 14명의 디자이너 정보를 체계적으로 저장

각 디자이너의 정보를 일관된 구조로 저장해야 합니다.

해결: 계층적 데이터 구조 설계

Firebase Realtime Database 구조:

{ "designerInfo": { "박세은": { "designerInfo": { "team": "Web", "name": "박세은", "nameEnglish": "Park Se Eun", "email": "03eungreen@naver.com", "intro": "...", "conceptTitle": "...", "conceptDescription": "..." }, "Image": { "after": "...", "before": "...", "sub": "..." }, "Poster": { "title": "...", "description": "..." }, "Inter": { "title": "...", "description": "...", "levelDescription": [...], "levelImages": [...], "interImage": "..." } }, "김도완": { ... }, // ... 14명의 디자이너 } }
  • designerInfo 루트 아래에 디자이너 이름을 키로 사용합니다.
  • 각 디자이너 노드에 designerInfo, Image, Poster, Inter로 구분합니다.
  • 이름을 키로 사용해 조회가 간단합니다.

문제: TypeScript 타입 안정성 확보

Firebase에서 가져온 데이터의 타입을 명확히 해야 합니다.

해결: RawDesignerNode 타입 정의

type RawDesignerNode = { designerInfo?: { team?: string; name?: string; nameEnglish?: string; email?: string; intro?: string; conceptTitle?: string; conceptDescription?: string; }; Image?: { after?: string; before?: string; sub?: string; }; Poster?: { title?: string; description?: string; }; Inter?: { title?: string; description?: string; levelDescription?: string[]; levelImages?: string[]; interImage?: string; }; };
  • 옵셔널 필드로 데이터 누락에 대비합니다.
  • 타입 정의로 개발 시 자동완성과 타입 체크를 지원합니다.

3단계: 데이터 가져오기 서비스 함수 구현

문제: 카드 목록 페이지용 데이터 가져오기

디자이너 목록 페이지에는 이름과 프로젝트명만 필요합니다.

해결: fetchDesignerCards 함수 구현

export async function fetchDesignerCards(): Promise<DesignerCardData[]> { const db = getDatabase(app); const root = ref(db, "designerInfo"); const snapshot = await get(root); const value = snapshot.val() as Record<string, RawDesignerNode> | null; if (!value) return []; const list: DesignerCardData[] = Object.entries(value).map(([key, node]) => { const name = node?.designerInfo?.name ?? key; const projectName = node?.designerInfo?.conceptTitle ?? node?.Poster?.title ?? ""; const image = { after: node?.Image?.after ?? "", before: node?.Image?.before ?? "", sub: node?.Image?.sub ?? "", }; return { name, projectName, image }; }); return list; }
  • getDatabase(app): Firebase Realtime Database 인스턴스 가져오기
  • ref(db, "designerInfo"): designerInfo 경로 참조
  • get(root): 한 번 읽기 (실시간 구독 아님)
  • Object.entries(value): 객체를 배열로 변환해 순회
  • ?? 연산자로 기본값 처리
  • 필요한 필드만 추출해 네트워크 비용을 줄입니다.

문제: 디자이너 상세 페이지용 데이터 가져오기

상세 페이지에는 모든 정보가 필요합니다.

해결: fetchDesignerDetailByName 함수 구현

export async function fetchDesignerDetailByName( name: string ): Promise<DesignerDetailData | null> { const db = getDatabase(app); const nodeRef = ref(db, `designerInfo/${name}`); const snapshot = await get(nodeRef); const node = snapshot.val() as RawDesignerNode | null; if (!node || !node.designerInfo) return null; return { info: { name: node.designerInfo.name ?? name, nameEnglish: node.designerInfo.nameEnglish ?? "", email: node.designerInfo.email ?? "", intro: node.designerInfo.intro ?? "", conceptTitle: node.designerInfo.conceptTitle ?? "", conceptDescription: node.designerInfo.conceptDescription ?? "", team: node.designerInfo.team ?? "", }, image: { after: node?.Image?.after ?? "", before: node?.Image?.before ?? "", sub: node?.Image?.sub ?? "", }, poster: { title: node.Poster?.title ?? "", description: node.Poster?.description ?? "", }, inter: { title: node.Inter?.title ?? "", description: node.Inter?.description ?? "", levelDescription: Array.isArray(node.Inter?.levelDescription) ? node.Inter!.levelDescription! : [], levelImages: Array.isArray(node.Inter?.levelImages) ? node.Inter!.levelImages! : getLocalLevelImages(node.designerInfo.name ?? name), interImage: node.Inter?.interImage ?? getLocalInterImage(node.designerInfo.name ?? name), }, }; }
  • ref(db, "designerInfo/${name}"): 특정 디자이너 경로 참조
  • 단일 노드만 읽어 효율적입니다.
  • Array.isArray로 배열 타입 보장
  • 로컬 폴백 이미지 함수로 누락 데이터 처리

문제: 디자이너 이름 목록 가져오기

네비게이션용 이름 목록이 필요합니다.

해결: fetchDesignerNamesSorted 함수 구현

export async function fetchDesignerNamesSorted(): Promise<string[]> { const db = getDatabase(app); const root = ref(db, "designerInfo"); const snapshot = await get(root); const value = snapshot.val() as Record<string, RawDesignerNode> | null; if (!value) return []; const names = Object.values(value) .map(node => node.designerInfo?.name) .filter((n): n is string => !!n) .sort((a, b) => a.localeCompare(b, "ko")); return names; }
  • Object.values로 노드 배열 생성
  • filter로 유효한 이름만 추출
  • localeCompare(b, "ko")로 한글 정렬

4단계: React Query로 상태 관리 설정

문제: 서버 상태 관리

로딩, 에러, 캐싱, 리페치를 일관되게 처리해야 합니다. 기존에 계획한 대로, 탠스택 쿼리를 사용합니다.

해결: QueryClient 전역 설정

const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false, }, }, });
  • retry: 1: 실패 시 1회 재시도
  • refetchOnWindowFocus: false: 포커스 시 자동 리페치 비활성화 (전시장 환경에 적합)

문제: Suspense와 통합

로딩 상태를 Suspense로 처리하려면 useSuspenseQuery가 필요합니다.

해결: useSuspenseQuery 기반 커스텀 훅

export function useDesignerCards() { return useSuspenseQuery<DesignerCardData[], Error, DesignerCardData[]>({ queryKey: ["designer-cards"], queryFn: fetchDesignerCards, staleTime: 60 * 1000, }); }
  • queryKey: ["designer-cards"]: 캐시 키
  • queryFn: fetchDesignerCards: 데이터 페치 함수
  • staleTime: 60 * 1000: 1분간 fresh 유지, 리페치 없음
  • Suspense와 통합되어 로딩 상태를 자동 처리합니다.

문제: 디자이너별 캐싱

상세 페이지는 더 오래 캐싱하는 것이 좋습니다.

해결: 상세 데이터용 긴 staleTime 설정

export function useDesignerDetail(name: string) { return useSuspenseQuery< DesignerDetailData | null, Error, DesignerDetailData | null >({ queryKey: ["designer-detail", name], queryFn: () => fetchDesignerDetailByName(name), staleTime: 5 * 60 * 1000, // 5분으로 증가 gcTime: 10 * 60 * 1000, // 10분간 메모리에 유지 }); }
  • queryKey: ["designer-detail", name]: 이름별 캐시 키
  • staleTime: 5 * 60 * 1000: 5분간 fresh
  • gcTime: 10 * 60 * 1000: 10분간 메모리 유지

5단계: 컴포넌트에서 데이터 사용

문제: 디자이너 목록 페이지 데이터 표시

카드 목록을 표시해야 합니다.

해결: DesignersPage에서 useDesignerCards 사용

const DesignersGridContent = () => { const { data } = useDesignerCards(); const designers = useMemo(() => data ?? [], [data]); return ( <DesignerGrid> {designers.map(d => ( <DesignerCard key={`${d.name}-${d.projectName}`} name={d.name} projectName={d.projectName} image={getLocalPersonImages(d.name)} /> ))} </DesignerGrid> ); };
  • useDesignerCards()로 데이터 가져오기
  • useMemo로 불필요한 재계산 방지
  • map으로 카드 렌더링

문제: Suspense와 ErrorBoundary로 로딩/에러 처리

로딩과 에러를 일관되게 처리해야 합니다.

해결: Suspense + ErrorBoundary 조합

export const DesignersPage = () => { return ( <ErrorBoundary> <Suspense fallback={<SuspenseFallback />}> <DesignersPageContainer> <Title>DESIGNERS</Title> <DesignersGridContent /> <div style={{ height: "8.33vw" }}></div> </DesignersPageContainer> </Suspense> </ErrorBoundary> ); };
  • Suspense: 로딩 중 SuspenseFallback 표시
  • ErrorBoundary: 에러 발생 시 폴백 UI 표시

문제: 디자이너 상세 페이지 데이터 표시

URL 쿼리로 디자이너를 선택하고 상세 정보를 표시해야 합니다.

해결: URL 쿼리 파라미터로 디자이너 선택

export const DesignerDetailPage = () => { const [searchParams] = useSearchParams(); const targetName = searchParams.get("name") ?? "박세은"; const { data: names } = useDesignerNames(); return ( <> <ErrorBoundary> <Suspense fallback={<SuspenseFallback />}> <DesignerDetailContent name={targetName} /> <ListSelectBox list={names ?? []} currentName={targetName} /> </Suspense> </ErrorBoundary> </> ); };
  • useSearchParams()로 URL 쿼리 읽기
  • searchParams.get("name")으로 디자이너 이름 추출
  • 기본값 "박세은" 설정

문제: 상세 데이터 표시

가져온 데이터를 컴포넌트에 전달해야 합니다.

해결: DesignerDetailContent에서 useDesignerDetail 사용

const DesignerDetailContent = ({ name }: { name: string }) => { const { data } = useDesignerDetail(name); console.log(data); return ( <> {!data ? null : ( <> <DesignerInfo team={data.info.team} name={data.info.name} nameEnglish={data.info.nameEnglish} email={data.info.email} intro={data.info.intro} conceptTitle={data.info.conceptTitle} conceptDescription={data.info.conceptDescription} /> <Poster title={data.poster.title} description={data.poster.description} designerName={data.info.name} /> <Inter title={data.inter.title} description={data.inter.description} levelDescription={data.inter.levelDescription} levelImages={data.inter.levelImages} interImage={data.inter.interImage} designerKey={normalizeDesignerKey(data.info.nameEnglish)} /> </> )} </> ); };
  • useDesignerDetail(name): 이름으로 상세 데이터 가져오기
  • data가 없으면 null 반환
  • 각 섹션 컴포넌트에 필요한 props 전달

6단계: DesignerCard 컴포넌트에서 네비게이션

문제: 카드 클릭 시 상세 페이지로 이동

카드 클릭 시 해당 디자이너 상세 페이지로 이동해야 합니다.

해결: useNavigate로 프로그래밍 방식 네비게이션

const handleClick = () => { const params = new URLSearchParams({ name }); navigate(`/designer?${params.toString()}`); };
  • URLSearchParams로 쿼리 파라미터 생성
  • navigate로 상세 페이지로 이동

이 구성으로 14명의 디자이너 정보를 효율적으로 관리하고 표시합니다. React Query의 캐싱으로 불필요한 네트워크 요청을 줄이고, Suspense로 로딩 상태를 일관되게 처리하였습니다.