4-5일차: Metadata API + 상태 끌어올리기 + SEO 최적화, 환경변수 + 커스텀 훅 + .env 설정

짱효·2025년 9월 29일

Next.js 학습 4-5일차 핵심 정리

4일차: Metadata API + 상태 끌어올리기 + SEO 최적화

🔧 Metadata API 핵심

기본 설정

// 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} />
    </>
  )
}

🚀 SEO 최적화 핵심

JSON-LD 구조화 데이터

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>
    </>
  )
}

5일차: 환경변수 + 커스텀 훅 + .env 설정

🔐 환경변수 관리 핵심

파일 구조

.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 }

🎣 커스텀 훅 핵심

useLocalStorage

// 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)} />
}

useApi (데이터 패칭)

// 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>
}

useDebounce (검색 최적화)

// 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="검색..."
    />
  )
}

💡 핵심 포인트

4일차 핵심

  • Metadata API: SEO를 위한 메타데이터 관리
  • 상태 끌어올리기: 컴포넌트 간 상태 공유의 기본 패턴
  • SEO: JSON-LD 구조화 데이터로 검색 최적화

5일차 핵심

  • 환경변수: 보안과 환경별 설정 분리
  • 커스텀 훅: 로직 재사용으로 코드 간소화
  • 실용 훅: localStorage, API 패칭, 디바운스

🎯 실습 과제

  1. 메타데이터: 블로그 포스트에 동적 OG 이미지 적용
  2. 상태 관리: 쇼핑카트 상태를 여러 컴포넌트에서 공유
  3. 커스텀 훅: 자주 쓰는 로직을 훅으로 추상화
  4. 환경변수: 개발/프로덕션 환경 분리

이 내용들을 직접 구현해보면서 Next.js의 핵심 개념들을 체화해보세요! 🚀

profile
✨🌏확장해 나가는 프론트엔드 개발자입니다✏️

0개의 댓글