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분간 freshgcTime: 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로 로딩 상태를 일관되게 처리하였습니다.