// app/layout.js
export const metadata = {
title: {
default: 'My App',
template: '%s | My App'
},
description: '앱 설명',
openGraph: {
title: 'My App',
description: '앱 설명',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
},
}
// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
const post = await fetchPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
},
}
}
형제 컴포넌트가 같은 상태를 공유할 때 공통 부모로 상태를 끌어올리는 패턴
// ❌ 잘못된 방법
function App() {
return (
<>
<SearchInput /> {/* 검색어 상태 */}
<SearchResults /> {/* 검색어 필요하지만 접근 불가 */}
</>
)
}
// ✅ 올바른 방법
function App() {
const [searchQuery, setSearchQuery] = useState('')
return (
<>
<SearchInput value={searchQuery} onChange={setSearchQuery} />
<SearchResults query={searchQuery} />
</>
)
}
export default function BlogPost({ post }) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
author: {
'@type': 'Person',
name: post.author,
},
datePublished: post.publishedAt,
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
</>
)
}
.env # 기본값
.env.local # 로컬용 (git 제외)
.env.development # 개발환경
.env.production # 프로덕션
// lib/env.js
const env = {
API_URL: process.env.NEXT_PUBLIC_API_URL,
SECRET_KEY: process.env.SECRET_KEY, // 서버에서만 접근
}
// 필수 환경변수 체크
Object.entries(env).forEach(([key, value]) => {
if (!value) {
throw new Error(`Missing ${key} environment variable`)
}
})
export { env }
// hooks/useLocalStorage.js
import { useState } from 'react'
export function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
if (typeof window === 'undefined') return initialValue
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setStoredValue = (newValue) => {
setValue(newValue)
localStorage.setItem(key, JSON.stringify(newValue))
}
return [value, setStoredValue]
}
// 사용법
function MyComponent() {
const [name, setName] = useLocalStorage('username', '')
return <input value={name} onChange={(e) => setName(e.target.value)} />
}
// hooks/useApi.js
import { useState, useEffect } from 'react'
export function useApi(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
const response = await fetch(url)
const result = await response.json()
setData(result)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchData()
}, [url])
return { data, loading, error }
}
// 사용법
function ProductList() {
const { data, loading, error } = useApi('/api/products')
if (loading) return <div>로딩 중...</div>
if (error) return <div>에러: {error}</div>
return <div>{data?.map(item => <div key={item.id}>{item.name}</div>)}</div>
}
// hooks/useDebounce.js
import { useState, useEffect } from 'react'
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
// 사용법
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('')
const debouncedSearchTerm = useDebounce(searchTerm, 500)
useEffect(() => {
if (debouncedSearchTerm) {
// API 호출
console.log('검색:', debouncedSearchTerm)
}
}, [debouncedSearchTerm])
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="검색..."
/>
)
}
이 내용들을 직접 구현해보면서 Next.js의 핵심 개념들을 체화해보세요! 🚀