11.5 메타데이터·환경변수·배포 — Metadata 타입과 process.env 안전성
Metadata API 타입
Next.js 13+의 Metadata API는 완전히 타입 안전합니다.
정적 메타데이터
// app/blog/page.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: '블로그',
description: '최신 게시물을 확인하세요.',
keywords: ['TypeScript', 'Next.js', '개발'],
authors: [{ name: '홍길동', url: 'https://example.com' }],
openGraph: {
title: '블로그',
description: '최신 게시물을 확인하세요.',
url: 'https://example.com/blog',
siteName: '내 블로그',
images: [
{
url: 'https://example.com/og.png',
width: 1200,
height: 630,
alt: 'OG 이미지',
},
],
locale: 'ko_KR',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: '블로그',
description: '최신 게시물을 확인하세요.',
images: ['https://example.com/og.png'],
creator: '@username',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
},
},
alternates: {
canonical: 'https://example.com/blog',
languages: {
'ko-KR': 'https://example.com/ko/blog',
'en-US': 'https://example.com/en/blog',
},
},
};
동적 메타데이터
// app/blog/[slug]/page.tsx
import { Metadata, ResolvingMetadata } from 'next';
interface Props {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | undefined }>;
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata // 부모 메타데이터 접근
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
// 부모 메타데이터 활용
const parentImages = (await parent).openGraph?.images ?? [];
if (!post) {
return {
title: '게시글을 찾을 수 없음',
};
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [
post.coverImage
? { url: post.coverImage, width: 1200, height: 630 }
: ...parentImages,
],
type: 'article',
publishedTime: post.publishedAt.toISOString(),
authors: [post.author.name],
},
};
}
metadataBase 설정
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'), // 상대 URL의 기준점
title: {
default: '내 앱',
template: '%s | 내 앱', // 자식 페이지: "페이지 제목 | 내 앱"
},
description: '내 앱의 설명',
};
process.env 타입 안전성
기본 방법: declare namespace
// src/types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
// 필수 환경변수
NODE_ENV: 'development' | 'production' | 'test';
DATABASE_URL: string;
NEXTAUTH_SECRET: string;
NEXTAUTH_URL: string;
// 선택적 환경변수
PORT?: string;
LOG_LEVEL?: 'debug' | 'info' | 'warn' | 'error';
}
}
// 사용 시 타입 안전
const db = new Client({ connectionString: process.env.DATABASE_URL }); // string
const port = parseInt(process.env.PORT ?? '3000'); // string | undefined → number
Zod로 런타임 검증 (권장)
환경변수 타입 선언만으로는 런타임에 실제 값이 있는지 보장되지 않습니다. Zod를 사용하면 앱 시작 시 검증합니다.
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
PORT: z.coerce.number().int().positive().default(3000),
// 선택적
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().optional(),
// 클라이언트용 (NEXT_PUBLIC_ 접두사)
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_GA_ID: z.string().optional(),
});
// 환경변수 검증 및 파싱
function validateEnv() {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('❌ 환경변수 오류:');
result.error.issues.forEach(issue => {
console.error(` ${issue.path.join('.')}: ${issue.message}`);
});
process.exit(1); // 서버 시작 중단
}
return result.data;
}
export const env = validateEnv();
// env.DATABASE_URL: string (undefined 없음, 검증 완료)
// 사용
import { env } from '@/lib/env';
const db = new Client({ connectionString: env.DATABASE_URL });
const port = env.PORT; // number (이미 파싱됨)
@t3-oss/env-nextjs (T3 스택 방식)
npm install @t3-oss/env-nextjs zod
// env.mjs
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
},
client: {
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_GA_ID: z.string().optional(),
},
// process.env 명시적 전달 (Edge Runtime 호환성)
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
},
});
Vercel 배포 통합
vercel.json 타입
// vercel.json
{
"framework": "nextjs",
"buildCommand": "npm run build",
"devCommand": "npm run dev",
"installCommand": "npm install",
"regions": ["icn1"],
"env": {
"DATABASE_URL": "@database-url"
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store" }
]
}
]
}
Edge Runtime 설정
// app/api/geo/route.ts
import { NextRequest } from 'next/server';
export const runtime = 'edge'; // Edge Runtime 지정
export async function GET(request: NextRequest) {
// Edge에서는 geo 정보 접근 가능
const { geo } = request; // { country: string, city: string, ... }
return Response.json({
country: geo?.country,
city: geo?.city,
});
}
고수 팁
1. 환경변수 접두사 규칙
NEXT_PUBLIC_*: 클라이언트(브라우저)에서 접근 가능
나머지: 서버에서만 접근 가능 (클라이언트 번들에 포함 안 됨)
2. 로컬 환경변수 파일 우선순위
.env.local → 로컬 개발 (git에 커밋하지 않음)
.env.development → 개발 환경
.env.production → 프로덕션 환경
.env → 모든 환경 (공통 기본값)
3. 빌드 시 체크 (next.config.ts)
// next.config.ts
import './src/env'; // 빌드 시작 시 환경변수 검증 실행
const config: NextConfig = { /* ... */ };
export default config;