Next.js 16, TypeScript 및 Unsplash API로 현대적인 이미지 갤러리 구축

발행: (2025년 12월 12일 오전 05:59 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

🛠 기술 스택 및 기능

핵심 기술

  • Next.js 16 – 서버 컴포넌트를 지원하는 App Router
  • TypeScript – 완전한 타입 안전성
  • Tailwind CSS v4 – CSS 변수와 함께하는 유틸리티‑우선 스타일링
  • Unsplash API – 고품질 이미지 소스
  • Lucide React – 최신 아이콘 라이브러리

주요 기능

  • ✅ 초기 페이지 로드를 위한 서버‑사이드 렌더링 (SSR)
  • ✅ 인기 사진을 위한 정적 사이트 생성 (SSG)
  • /photos/[id] 로 동적 라우팅
  • next/image 로 이미지 최적화
  • ✅ 디바운싱을 활용한 실시간 검색
  • ✅ 카테고리 필터링 (자연, 건축 등)
  • ✅ 시스템 설정을 감지하는 다크 모드
  • ✅ 반응형, 모바일‑우선 디자인
  • ✅ SEO 최적화된 동적 메타데이터
  • ✅ 오류 경계와 로딩 상태

🏗 프로젝트 구조

파일 구조

next-image-gallery/
├── app/
│   ├── layout.tsx           # Root layout with ThemeProvider
│   ├── page.tsx             # Home page (Server Component)
│   ├── loading.tsx          # Global loading UI
│   ├── error.tsx            # Global error boundary
│   ├── api/
│   │   └── photos/
│   │       └── route.ts     # API route for client‑side fetching
│   └── photos/[id]/
│       ├── page.tsx         # Dynamic photo detail page
│       ├── loading.tsx      # Photo detail loading state
│       └── not-found.tsx    # 404 page
├── components/
│   ├── ui/
│   │   └── Button.tsx       # Reusable button component
│   ├── ImageCard.tsx        # Photo card with hover effects
│   ├── ImageGrid.tsx        # Responsive grid layout
│   ├── ImageDetail.tsx      # Photo detail view
│   ├── Navigation.tsx       # Header navigation
│   ├── ThemeToggle.tsx      # Dark mode toggle (Client)
│   ├── SearchBar.tsx        # Search input with debounce (Client)
│   ├── FilterBar.tsx        # Category filters (Client)
│   └── GalleryClient.tsx    # Client‑side orchestrator
├── lib/
│   ├── api.ts               # Unsplash API functions
│   ├── types.ts             # TypeScript interfaces
│   └── utils.ts             # Helper functions
└── providers/
    └── ThemeProvider.tsx    # Theme context provider

⚡ 서버 컴포넌트 전략

서버 vs. 클라이언트 컴포넌트

// app/page.tsx – Server Component (default)
export default async function Home() {
  // ✅ Fetch data on the server
  const initialPhotos = await getPhotos({
    page: 1,
    perPage: 20,
    orderBy: "popular",
  });

  return <GalleryClient initialPhotos={initialPhotos} />;
}

// components/GalleryClient.tsx – Client Component
"use client"; // ← Explicit directive

export function GalleryClient({ initialPhotos }: Props) {
  const [photos, setPhotos] = useState(initialPhotos);
  // ...interactive logic
}

왜 중요한가

Server Components

  • ✅ 클라이언트에 JavaScript가 전송되지 않음
  • ✅ API/데이터베이스에 직접 접근
  • ✅ 더 나은 SEO (HTML에 콘텐츠 포함)
  • ✅ 초기 로드 속도 향상

Client Components

  • ✅ 인터랙티브 기능 (hooks, 이벤트 핸들러)
  • ✅ 브라우저 API 접근 (localStorage 등)
  • ✅ 실시간 업데이트

The Pattern

Server Component (page.tsx)

Fetch data on server

Pass to Client Component

Client Component (GalleryClient.tsx)

Handle user interactions

Fetch additional data via API route

결과: 빠른 SSR 렌더링 + 풍부한 클라이언트 인터랙션을 최적화된 번들 크기로 제공.

🚀 SSG를 활용한 동적 라우트

인기 사진을 위한 정적 사이트 생성

// app/photos/[id]/page.tsx
export async function generateStaticParams() {
  const photos = await getPhotosForStaticGeneration(30);
  return photos.map((photo) => ({ id: photo.id }));
}

빌드 시 출력

npm run build

# Example output:
 /photos/[id]                  (Static)
 /photos/abc123
 /photos/def456
 /photos/xyz789
... (30 total pages)

SEO를 위한 동적 메타데이터

export async function generateMetadata({ params }: Props): Promise<any> {
  const { id } = await params;
  const photo = await getPhotoById(id);

  return {
    title: `${photo.alt_description} - Photo Gallery`,
    description: photo.description || `Photo by ${photo.user.name}`,
    openGraph: {
      images: [
        {
          url: photo.urls.regular,
          width: photo.width,
          height: photo.height,
        },
      ],
    },
    twitter: {
      card: "summary_large_image",
    },
  };
}

이점

  • 사진당 고유 메타 태그를 통한 완벽한 SEO
  • 아름다운 소셜 미디어 미리보기
  • 사전 생성된 페이지 세트의 즉시 로드

🖼 이미지 최적화

Next.js Image 컴포넌트

(여기에 next/image 사용 예시를 삽입하세요 – 원본 기사에서는 이 섹션이 비어 있습니다.)

주요 최적화

  • 자동 포맷 선택 – WebP, AVIF, JPEG/PNG 로 폴백
  • 반응형 이미지sizes="(max-width: 768px) 100vw, 33vw"
  • 지연 로딩 – 첫 네 이미지에만 priority={true} 적용
  • 블러 플레이스홀더 – CLS 방지를 위한 컬러 SVG 플레이스홀더

설정

// next.config.ts
export default {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.unsplash.com",
        pathname: "/**",
      },
    ],
  },
};

🔍 검색 및 필터링

디바운싱된 검색 컴포넌트

// components/SearchBar.tsx
export function SearchBar({ onSearch }: Props) {
  const [query, setQuery] = useState("");

  useEffect(() => {
    const debouncedSearch = debounce((value: string) => {
      onSearch(value);
    }, 500); // Wait 500 ms after user stops typing

    debouncedSearch(query);
  }, [query, onSearch]);

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search photos..."
    />
  );
}

왜 디바운싱을 사용하나요?
디바운싱을 사용하지 않으면 사용자가 입력할 때마다 네트워크 요청이 발생해 API에 불필요한 부하가 걸리고 사용자 경험이 저하됩니다. 디바운싱은 사용자가 잠시 멈춘 뒤에 빠른 입력을 하나의 요청으로 묶어 보냅니다.

모든 코드 스니펫은 동작하며 Next.js 16 프로젝트에 바로 복사해서 사용할 수 있습니다.

Back to Blog

관련 글

더 보기 »