본문으로 건너뛰기
Advertisement

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;
Advertisement