해당 기능은 공통업무 기능입니다.
다음과 같이 미리 준비된 Boilerplate confy-app 형태로 제공됩니다.
신규 빌드/배포 후에도 사용자의 화면에서 이전 내용의 페이지가 나타나는 이슈에 대응하고자 배포마다 고유한 ID(.next/BUILD_ID)를 생성하고 클라이언트에서 이를 확인합니다.
// app/api/build-id/route.ts
import fs from 'fs';
import path from 'path';
import { NextResponse } from 'next/server';
export async function GET() {
try {
// .next/BUILD_ID 파일에서 빌드 ID 읽기
const buildIdPath = path.join(process.cwd(), '.next', 'BUILD_ID');
const buildId = fs.readFileSync(buildIdPath, 'utf8');
// CORS 헤더 설정 및 캐시 방지
return NextResponse.json(
{ buildId },
{
headers: {
'Cache-Control': 'no-store, max-age=0, must-revalidate',
'Access-Control-Allow-Origin': '*',
},
}
);
} catch (error) {
console.error('빌드 ID를 가져오는 중 오류가 발생했습니다:', error);
return NextResponse.json(
{ error: '빌드 ID를 가져오는 중 오류가 발생했습니다.' },
{ status: 500 }
);
}
}
// hooks/useBuildId.ts
'use client';
import { useEffect, useState, useRef } from 'react';
interface UseBuildIdResult {
shouldReload: boolean;
resetNotification: () => void;
}
interface NextData {
buildId: string;
[key: string]: any;
}
export function useBuildId(checkInterval = 60000): UseBuildIdResult {
const [shouldReload, setShouldReload] = useState<boolean>(false);
const currentBuildId = useRef<string | null>(null);
const resetNotification = () => {
setShouldReload(false);
};
useEffect(() => {
// 브라우저 환경에서만 실행
if (typeof window === 'undefined') return;
// 프로덕션 환경에서만 실행
if (process.env.NODE_ENV !== 'production') return;
// 현재 클라이언트의 빌드 ID 가져오기
const nextDataElement = document.getElementById('NEXT_DATA');
if (!nextDataElement) return;
try {
const nextData = JSON.parse(nextDataElement.textContent || '{}') as NextData;
currentBuildId.current = nextData.buildId;
} catch (error) {
console.error('빌드 ID 파싱 중 오류 발생:', error);
return;
}
// 주기적으로 서버의 최신 빌드 ID 확인
const checkBuildId = async () => {
try {
// 빌드 ID API 엔드포인트 호출
const response = await fetch('/api/build-id', {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache'
}
});
if (!response.ok) {
throw new Error(`API 응답 오류: ${response.status}`);
}
const data = await response.json();
const serverBuildId = data.buildId;
// 빌드 ID가 다르면 새로고침 필요
if (serverBuildId && currentBuildId.current && serverBuildId !== currentBuildId.current) {
setShouldReload(true);
}
} catch (error) {
console.error('빌드 ID 확인 중 오류 발생:', error);
}
};
// 초기 확인
checkBuildId();
// 주기적으로 확인
const interval = setInterval(checkBuildId, checkInterval);
return () => clearInterval(interval);
}, [checkInterval]);
return { shouldReload, resetNotification };
}
// components/UpdateNotification.tsx
'use client';
import { useEffect, useState } from 'react';
import styles from './UpdateNotification.module.css';
interface UpdateNotificationProps {
shouldReload: boolean;
onDismiss: () => void;
}
export function UpdateNotification({ shouldReload, onDismiss }: UpdateNotificationProps) {
const [visible, setVisible] = useState<boolean>(false);
useEffect(() => {
if (shouldReload) {
setVisible(true);
}
}, [shouldReload]);
if (!visible) return null;
const handleUpdate = () => {
window.location.reload();
};
const handleDismiss = () => {
setVisible(false);
onDismiss();
};
return (
<div className={styles.notification}>
<p>새 버전이 사용 가능합니다!</p>
<div className={styles.actions}>
<button
className={styles.updateButton}
onClick={handleUpdate}
>
지금 업데이트
</button>
<button
className={styles.laterButton}
onClick={handleDismiss}
>
나중에
</button>
</div>
</div>
);
}
// app/providers.tsx
'use client';
import { ReactNode } from 'react';
import { UpdateNotification } from '@/components/UpdateNotification';
import { useBuildId } from '@/hooks/useBuildId';
interface ProvidersProps {
children: ReactNode;
}
export function Providers({ children }: ProvidersProps) {
const { shouldReload, resetNotification } = useBuildId();
return (
<>
{children}
<UpdateNotification
shouldReload={shouldReload}
onDismiss={resetNotification}
/>
</>
);
}
layout.tsx // 글로벌 레이아웃에 주입
import type { Metadata } from 'next'
...
import { UpdateNotificationProvider } from '@/components/updateNotificationProvider'
...
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app'
}
export default async function RootLayout({
children
}: Readonly<{
children: React.ReactNode
}>) {
const locale = await getLocale()
const messages = await getMessages()
const session = await getServerSession(authOptions)
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
<AuthProviders session={session}>
<NavigationGuardProvider>
<ThemeProviders>
<TopMenuLink />
<MainContent>
<UpdateNotificationProvider> // 버전 변경 감시
{children}
</UpdateNotificationProvider>
</MainContent>
</ThemeProviders>
</NavigationGuardProvider>
</AuthProviders>
</NextIntlClientProvider>
</body>
</html>
)
}