Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions app/optimized-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import "../styles/globals.css";
import { headers } from "next/headers";
import { HeaderLayout } from "@/components/Header/HeaderLayout";
import Footer from "@/components/Footer";
import { Providers } from "./providers";
import { Locale } from "@/lib/lang/locales";
import { Metadata } from "next";
import { koMetadata, enMetadata, generateDetailMetadata } from "./metadata";
import { GoogleRedirectHandler } from "@/components/auth/GoogleRedirectHandler";
import Script from "next/script";
import { getImageDetails } from "@/app/details/utils/hooks/fetchImageDetails";
import { cache } from "react";
import { notFound } from "next/navigation";

// 메타데이터 생성 함수를 캐시화하여 성능 개선
const getCachedImageDetails = cache(async (imageId: string) => {
try {
return await getImageDetails(imageId);
} catch (error) {
console.error(`Failed to fetch image details for ${imageId}:`, error);
return null;
}
});

// 메타데이터 생성 최적화
export async function generateMetadata(): Promise<Metadata> {
const headersList = await headers();
const locale = headersList.get("x-locale") || "ko";
const pathname = headersList.get("x-pathname") || "";
const searchParams = headersList.get("x-search-params") || "";

// 기본 메타데이터 선택 (조건부 최적화)
const baseMetadata = locale === "ko" ? koMetadata : enMetadata;

// 상세 페이지 메타데이터 처리 (캐시 적용)
if (pathname.startsWith('/details/')) {
const imageId = pathname.split('/').pop();
if (imageId && imageId !== 'undefined' && imageId !== 'null') {
try {
const imageData = await getCachedImageDetails(imageId);
if (imageData) {
return generateDetailMetadata(imageData, locale as 'ko' | 'en');
}
} catch (error) {
// 메타데이터 생성 실패 시 기본 메타데이터 사용
console.warn(`Metadata generation failed for ${imageId}, using default`);
}
}
}

// 검색 페이지 메타데이터 (최적화된 문자열 처리)
if (pathname.startsWith('/search')) {
const params = new URLSearchParams(searchParams);
const query = params.get('q')?.trim() || '';

if (query) {
const searchTitle = `${query} 검색 결과 | DECODED`;
const searchDescription = `DECODED에서 "${query}"에 대한 검색 결과를 확인하세요`;

return {
...baseMetadata,
title: searchTitle,
description: searchDescription,
openGraph: {
...baseMetadata.openGraph,
title: searchTitle,
description: searchDescription,
},
twitter: {
...baseMetadata.twitter,
title: searchTitle,
description: searchDescription,
},
};
}
}

// 목록 페이지 메타데이터 (정적 최적화)
if (pathname.startsWith('/list')) {
const listMetadata = {
title: '아이템 목록 | DECODED',
description: 'DECODED에서 공유된 아이템 목록을 확인하세요',
};

return {
...baseMetadata,
...listMetadata,
openGraph: {
...baseMetadata.openGraph,
...listMetadata,
},
twitter: {
...baseMetadata.twitter,
...listMetadata,
},
};
}

// 기본 메타데이터 반환
return baseMetadata;
}

// 정적 폰트 리소스 정의 (최적화)
const fontPreloadLinks = [
{
rel: "preload" as const,
href: "@/fonts/Pretendard-Regular.otf",
as: "font" as const,
type: "font/otf",
crossOrigin: "anonymous" as const,
},
];

// 외부 CSS 리소스 정의 (최적화)
const externalStyles = [
{
rel: "stylesheet",
href: "https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css",
integrity: "sha512-iKnXkfdkKvzKWFOSZaDOfONgJTe3h4y8IQAPdMsGs+mtIXTcgN+PGSkZ/+/IUWRDYkO+IpGkUCoLx+NwR/BCQ==",
crossOrigin: "anonymous" as const,
referrerPolicy: "no-referrer" as const,
},
];

interface RootLayoutProps {
children: React.ReactNode;
}

export default async function RootLayout({ children }: RootLayoutProps) {
const headersList = await headers();
const locale = headersList.get("x-locale") || "en";
const pathname = headersList.get("x-pathname") || "";

// 조건부 렌더링 최적화
const isCallbackPage = pathname.includes("/auth/callback");
const shouldShowHeaderFooter = !isCallbackPage;

return (
<html lang={locale}>
<head>
{/* 최적화된 폰트 프리로딩 */}
{fontPreloadLinks.map((link, index) => (
<link key={index} {...link} />
))}

{/* 최적화된 외부 스타일시트 로딩 */}
{externalStyles.map((style, index) => (
<link key={index} {...style} />
))}
</head>

<body className="flex flex-col min-h-screen">
{/* 조건부 컴포넌트 렌더링 */}
<GoogleRedirectHandler />

<Providers locale={locale as Locale}>
{/* 헤더를 조건부로만 렌더링 */}
{shouldShowHeaderFooter && <HeaderLayout />}

<main className="flex-1">
{children}
</main>

{/* 푸터를 조건부로만 렌더링 */}
{shouldShowHeaderFooter && <Footer />}
</Providers>

{/* 최적화된 외부 스크립트 로딩 */}
<Script
src="https://t1.kakaocdn.net/kakao_js_sdk/2.6.0/kakao.min.js"
strategy="afterInteractive"
integrity="sha384-6MFdIr0zOira1CHQkedUqJVql0YtcZA1P0nbPrQYJXVJZUkTk/oX4U9GhUIs3/z8"
crossOrigin="anonymous"
/>
</body>
</html>
);
}
100 changes: 61 additions & 39 deletions app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
import { useState, useMemo, useCallback, memo } from "react";
import { LocaleContext } from "@/lib/contexts/locale-context";
import { langMap, Locale } from "@/lib/lang/locales";
import { StatusModal } from "@/components/ui/modal/status-modal";
import { useStatusStore } from "@/components/ui/modal/status-modal/utils/store";
import { useLoginModalStore } from "@/components/auth/login-modal/store";
import { Toaster } from "sonner";

function GlobalStatusModal() {
// 최적화된 글로벌 상태 모달 컴포넌트
const GlobalStatusModal = memo(function GlobalStatusModal() {
const { isOpen, type, messageKey, title, message, closeStatus } =
useStatusStore();
const { openLoginModal } = useLoginModalStore();

const handleStatusClose = () => {
// 상태 핸들러 메모화
const handleStatusClose = useCallback(() => {
closeStatus();
if (type === "warning" && messageKey === "login") {
console.log(
Expand All @@ -24,7 +26,7 @@ function GlobalStatusModal() {
openLoginModal();
}, 300);
}
};
}, [closeStatus, type, messageKey, openLoginModal]);

return (
<StatusModal
Expand All @@ -36,61 +38,81 @@ function GlobalStatusModal() {
message={message}
/>
);
}
});

// 토스터 설정 메모화
const toasterConfig = {
position: "top-center" as const,
toastOptions: {
style: {
background: "#333",
color: "white",
border: "1px solid #444",
},
duration: 3000,
},
};

interface ProvidersProps {
children: React.ReactNode;
locale?: Locale;
}

export function Providers({ children, locale = "en" }: ProvidersProps) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error: any) => {
if (error?.code === "NETWORK_ERROR" && failureCount < 2) {
return true;
}
export const Providers = memo(function Providers({
children,
locale = "en"
}: ProvidersProps) {
// QueryClient 인스턴스 메모화 (한 번만 생성)
const [queryClient] = useState(() => {
return new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error: any) => {
// 네트워크 오류는 최대 2번 재시도
if (error?.code === "NETWORK_ERROR" && failureCount < 2) {
return true;
}

if (error?.status === 401) {
return false;
}
// 인증 오류는 재시도하지 않음
if (error?.status === 401) {
return false;
}

return failureCount < 1;
},
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30000),
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
// 기타 오류는 1번만 재시도
return failureCount < 1;
},
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30000),
staleTime: 5 * 60 * 1000, // 5분
gcTime: 10 * 60 * 1000, // 10분 (이전 cacheTime)
refetchOnWindowFocus: false, // 윈도우 포커스시 자동 refetch 비활성화 (성능 개선)
},
})
);
mutations: {
retry: 1, // 뮤테이션도 1번 재시도
},
},
});
});

const localeValue = {
// Locale context 값 메모화
const localeValue = useMemo(() => ({
locale,
t: langMap[locale],
};
}), [locale]);

return (
<QueryClientProvider client={queryClient}>
<LocaleContext.Provider value={localeValue}>
{children}
<GlobalStatusModal />
<Toaster
position="top-center"
toastOptions={{
style: {
background: "#333",
color: "white",
border: "1px solid #444",
},
duration: 3000,
}}
/>
<Toaster {...toasterConfig} />
</LocaleContext.Provider>
</QueryClientProvider>
);
});

// 개발 환경에서 디스플레이 네임 설정
if (process.env.NODE_ENV === 'development') {
Providers.displayName = 'Providers';
GlobalStatusModal.displayName = 'GlobalStatusModal';
}
Loading