Next.js 미디어 삽입
Next.js와 Tailwind CSS로 이미지 및 비디오 삽입 완벽 가이드
서론
현대 웹사이트에서 이미지와 비디오는 콘텐츠의 핵심 요소입니다. 하지만 단순히 HTML의 <img> 태그나 <video> 태그를 사용하는 것만으로는 최적의 사용자 경험을 제공하기 어렵습니다. Next.js의 최적화된 Image 컴포넌트와 Tailwind CSS의 유틸리티 클래스를 활용하면, 성능과 디자인을 모두 잡을 수 있습니다. 이 글에서는 Next.js와 Tailwind CSS를 사용하여 이미지와 비디오를 효과적으로 삽입하고 최적화하는 방법을 다루겠습니다.
1. Next.js Image 컴포넌트 기본 사용법
1.1 Image 컴포넌트란?
Next.js의 Image 컴포넌트는 HTML의 <img> 태그를 확장한 것으로, 자동 이미지 최적화, 지연 로딩, 반응형 이미지 생성 등의 기능을 제공합니다.
import Image from 'next/image';
export default function MyComponent() {
return (
<Image
src="/images/hero.jpg"
alt="Hero image"
width={800}
height={600}
/>
);
}
1.2 기본 속성
import Image from 'next/image';
function ImageExample() {
return (
<Image
src="/images/photo.jpg"
alt="설명 텍스트"
width={1200}
height={800}
priority={false} // 우선 로딩 여부
placeholder="blur" // 블러 효과
blurDataURL="data:..." // 블러 이미지 URL
quality={75} // 이미지 품질 (1-100)
sizes="(max-width: 768px) 100vw, 50vw"
/>
);
}
1.3 외부 이미지 사용
외부 도메인의 이미지를 사용하려면 next.config.js에 도메인을 추가해야 합니다.
// next.config.js
module.exports = {
images: {
domains: ['example.com', 'cdn.example.com'],
// 또는 더 유연한 패턴 사용
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com',
pathname: '/images/**',
},
],
},
};
import Image from 'next/image';
function ExternalImage() {
return (
<Image
src="https://example.com/images/photo.jpg"
alt="External image"
width={800}
height={600}
/>
);
}
2. Tailwind CSS로 이미지 스타일링
2.1 기본 스타일링
import Image from 'next/image';
function StyledImage() {
return (
<div className="relative w-full h-64">
<Image
src="/images/hero.jpg"
alt="Hero"
fill
className="object-cover rounded-lg shadow-lg"
/>
</div>
);
}
2.2 반응형 이미지
import Image from 'next/image';
function ResponsiveImage() {
return (
<div className="w-full md:w-1/2 lg:w-1/3">
<Image
src="/images/gallery.jpg"
alt="Gallery"
width={600}
height={400}
className="w-full h-auto rounded-xl"
/>
</div>
);
}
2.3 이미지 오버레이 효과
import Image from 'next/image';
function ImageWithOverlay() {
return (
<div className="relative w-full h-96 group">
<Image
src="/images/background.jpg"
alt="Background"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-black/50 group-hover:bg-black/70 transition-all duration-300">
<div className="absolute inset-0 flex items-center justify-center">
<h2 className="text-white text-4xl font-bold">오버레이 텍스트</h2>
</div>
</div>
</div>
);
}
2.4 이미지 갤러리
import Image from 'next/image';
const images = [
{ src: '/images/1.jpg', alt: 'Image 1' },
{ src: '/images/2.jpg', alt: 'Image 2' },
{ src: '/images/3.jpg', alt: 'Image 3' },
];
function ImageGallery() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{images.map((image, index) => (
<div key={index} className="relative aspect-square overflow-hidden rounded-lg">
<Image
src={image.src}
alt={image.alt}
fill
className="object-cover hover:scale-110 transition-transform duration-300"
/>
</div>
))}
</div>
);
}
3. 고급 Image 컴포넌트 활용
3.1 블러 플레이스홀더
import Image from 'next/image';
// 작은 블러 이미지 생성 (예: 10x10 픽셀)
const blurDataUrl = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q==';
function BlurImage() {
return (
<Image
src="/images/photo.jpg"
alt="Photo"
width={800}
height={600}
placeholder="blur"
blurDataURL={blurDataUrl}
className="rounded-lg"
/>
);
}
3.2 우선 로딩 이미지
import Image from 'next/image';
function HeroSection() {
return (
<div className="relative w-full h-screen">
<Image
src="/images/hero.jpg"
alt="Hero"
fill
priority
className="object-cover"
/>
</div>
);
}
3.3 이미지 최적화 설정
// next.config.js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 60,
},
};
4. 비디오 삽입 방법
4.1 HTML5 Video 태그
function VideoPlayer() {
return (
<div className="w-full max-w-4xl mx-auto">
<video
className="w-full rounded-lg shadow-lg"
controls
autoPlay
muted
loop
>
<source src="/videos/video.mp4" type="video/mp4" />
<source src="/videos/video.webm" type="video/webm" />
브라우저가 비디오 태그를 지원하지 않습니다.
</video>
</div>
);
}
4.2 Tailwind CSS로 비디오 스타일링
function StyledVideo() {
return (
<div className="relative w-full aspect-video bg-black rounded-xl overflow-hidden shadow-2xl">
<video
className="w-full h-full object-cover"
controls
poster="/images/video-thumbnail.jpg"
>
<source src="/videos/presentation.mp4" type="video/mp4" />
</video>
</div>
);
}
4.3 반응형 비디오
function ResponsiveVideo() {
return (
<div className="container mx-auto px-4">
<div className="relative w-full aspect-video max-w-4xl mx-auto">
<video
className="w-full h-full rounded-lg"
controls
>
<source src="/videos/demo.mp4" type="video/mp4" />
</video>
</div>
</div>
);
}
4.4 비디오 오버레이와 컨트롤
'use client';
import { useState } from 'react';
function VideoWithCustomControls() {
const [isPlaying, setIsPlaying] = useState(false);
const videoRef = useRef(null);
const togglePlay = () => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
return (
<div className="relative w-full aspect-video group">
<video
ref={videoRef}
src="/videos/video.mp4"
className="w-full h-full object-cover rounded-lg"
loop
/>
<button
onClick={togglePlay}
className="absolute inset-0 flex items-center justify-center bg-black/30 group-hover:bg-black/50 transition-all"
>
{isPlaying ? (
<div className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-white" fill="currentColor" viewBox="0 0 20 20">
<path d="M6 4h2v12H6V4zm6 0h2v12h-2V4z" />
</svg>
</div>
) : (
<div className="w-20 h-20 bg-white/20 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-white ml-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
</svg>
</div>
)}
</button>
</div>
);
}
5. YouTube 및 Vimeo 비디오 임베드
5.1 YouTube 비디오 임베드
function YouTubeVideo({ videoId }) {
return (
<div className="relative w-full aspect-video">
<iframe
className="absolute top-0 left-0 w-full h-full rounded-lg"
src={`https://www.youtube.com/embed/${videoId}`}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
);
}
// 사용 예시
<YouTubeVideo videoId="dQw4w9WgXcQ" />
5.2 Vimeo 비디오 임베드
function VimeoVideo({ videoId }) {
return (
<div className="relative w-full aspect-video">
<iframe
className="absolute top-0 left-0 w-full h-full rounded-lg"
src={`https://player.vimeo.com/video/${videoId}`}
frameBorder="0"
allow="autoplay; fullscreen; picture-in-picture"
allowFullScreen
/>
</div>
);
}
5.3 반응형 임베드 비디오 컴포넌트
function ResponsiveEmbed({ url, title }) {
return (
<div className="relative w-full aspect-video max-w-4xl mx-auto">
<iframe
className="absolute top-0 left-0 w-full h-full rounded-lg shadow-xl"
src={url}
title={title}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
);
}
6. 이미지와 비디오 혼합 레이아웃
6.1 이미지와 비디오 그리드
import Image from 'next/image';
const mediaItems = [
{ type: 'image', src: '/images/1.jpg', alt: 'Image 1' },
{ type: 'video', src: '/videos/1.mp4', thumbnail: '/images/v1-thumb.jpg' },
{ type: 'image', src: '/images/2.jpg', alt: 'Image 2' },
{ type: 'video', src: '/videos/2.mp4', thumbnail: '/images/v2-thumb.jpg' },
];
function MediaGrid() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{mediaItems.map((item, index) => (
<div key={index} className="relative aspect-square overflow-hidden rounded-xl group">
{item.type === 'image' ? (
<Image
src={item.src}
alt={item.alt}
fill
className="object-cover group-hover:scale-110 transition-transform duration-300"
/>
) : (
<video
src={item.src}
poster={item.thumbnail}
className="w-full h-full object-cover"
controls
/>
)}
</div>
))}
</div>
);
}
6.2 히어로 섹션 (이미지 + 비디오 배경)
import Image from 'next/image';
function HeroSection() {
return (
<section className="relative w-full h-screen">
{/* 비디오 배경 */}
<video
className="absolute inset-0 w-full h-full object-cover"
autoPlay
muted
loop
playsInline
>
<source src="/videos/hero-background.mp4" type="video/mp4" />
</video>
{/* 오버레이 */}
<div className="absolute inset-0 bg-black/40" />
{/* 콘텐츠 */}
<div className="relative z-10 flex items-center justify-center h-full">
<div className="text-center text-white">
<h1 className="text-5xl md:text-7xl font-bold mb-4">환영합니다</h1>
<p className="text-xl md:text-2xl">최고의 경험을 제공합니다</p>
</div>
</div>
</section>
);
}
7. 성능 최적화
7.1 이미지 지연 로딩
import Image from 'next/image';
function LazyLoadedImage({ src, alt }) {
return (
<Image
src={src}
alt={alt}
width={800}
height={600}
loading="lazy"
className="rounded-lg"
/>
);
}
7.2 비디오 지연 로딩
'use client';
import { useState, useRef, useEffect } from 'react';
function LazyVideo({ src, poster }) {
const [shouldLoad, setShouldLoad] = useState(false);
const videoRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setShouldLoad(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (videoRef.current) {
observer.observe(videoRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={videoRef} className="w-full aspect-video">
{shouldLoad ? (
<video
src={src}
poster={poster}
className="w-full h-full rounded-lg"
controls
/>
) : (
<Image
src={poster}
alt="Video thumbnail"
fill
className="object-cover rounded-lg"
/>
)}
</div>
);
}
7.3 이미지 프리로딩
import Image from 'next/image';
import { useEffect } from 'react';
function PreloadImage({ src }) {
useEffect(() => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'image';
link.href = src;
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}, [src]);
return (
<Image
src={src}
alt="Preloaded"
width={800}
height={600}
/>
);
}
8. 접근성 고려사항
8.1 이미지 접근성
import Image from 'next/image';
function AccessibleImage({ src, alt, description }) {
return (
<figure className="w-full">
<Image
src={src}
alt={alt}
width={800}
height={600}
className="w-full rounded-lg"
/>
{description && (
<figcaption className="mt-2 text-sm text-gray-600">
{description}
</figcaption>
)}
</figure>
);
}
8.2 비디오 접근성
function AccessibleVideo({ src, title, transcript }) {
return (
<div className="w-full">
<video
src={src}
controls
className="w-full rounded-lg"
aria-label={title}
>
<track
kind="captions"
srcLang="ko"
src="/videos/captions.vtt"
default
/>
</video>
{transcript && (
<details className="mt-4">
<summary className="cursor-pointer font-semibold">전체 대본 보기</summary>
<div className="mt-2 p-4 bg-gray-100 rounded">
{transcript}
</div>
</details>
)}
</div>
);
}
9. 실전 예제: 갤러리 페이지
9.1 이미지 갤러리
'use client';
import Image from 'next/image';
import { useState } from 'react';
const galleryImages = [
{ id: 1, src: '/images/gallery/1.jpg', alt: 'Gallery image 1' },
{ id: 2, src: '/images/gallery/2.jpg', alt: 'Gallery image 2' },
{ id: 3, src: '/images/gallery/3.jpg', alt: 'Gallery image 3' },
{ id: 4, src: '/images/gallery/4.jpg', alt: 'Gallery image 4' },
];
function GalleryPage() {
const [selectedImage, setSelectedImage] = useState(null);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">갤러리</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{galleryImages.map((image) => (
<div
key={image.id}
className="relative aspect-square overflow-hidden rounded-lg cursor-pointer group"
onClick={() => setSelectedImage(image)}
>
<Image
src={image.src}
alt={image.alt}
fill
className="object-cover group-hover:scale-110 transition-transform duration-300"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all" />
</div>
))}
</div>
{/* 모달 */}
{selectedImage && (
<div
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
onClick={() => setSelectedImage(null)}
>
<div className="relative max-w-4xl max-h-full">
<Image
src={selectedImage.src}
alt={selectedImage.alt}
width={1200}
height={800}
className="rounded-lg"
/>
<button
onClick={() => setSelectedImage(null)}
className="absolute top-4 right-4 text-white bg-black/50 rounded-full p-2 hover:bg-black/70"
>
✕
</button>
</div>
</div>
)}
</div>
);
}
9.2 비디오 플레이리스트
'use client';
import { useState } from 'react';
const videos = [
{ id: 1, title: '비디오 1', src: '/videos/1.mp4', thumbnail: '/images/v1.jpg' },
{ id: 2, title: '비디오 2', src: '/videos/2.mp4', thumbnail: '/images/v2.jpg' },
{ id: 3, title: '비디오 3', src: '/videos/3.mp4', thumbnail: '/images/v3.jpg' },
];
function VideoPlaylist() {
const [currentVideo, setCurrentVideo] = useState(videos[0]);
return (
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 메인 비디오 */}
<div className="lg:col-span-2">
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden">
<video
key={currentVideo.id}
src={currentVideo.src}
controls
className="w-full h-full"
autoPlay
/>
</div>
<h2 className="text-2xl font-bold mt-4">{currentVideo.title}</h2>
</div>
{/* 플레이리스트 */}
<div className="space-y-4">
<h3 className="text-xl font-semibold">플레이리스트</h3>
{videos.map((video) => (
<div
key={video.id}
className={`cursor-pointer rounded-lg overflow-hidden border-2 transition-all ${
currentVideo.id === video.id
? 'border-blue-500'
: 'border-transparent hover:border-gray-300'
}`}
onClick={() => setCurrentVideo(video)}
>
<div className="relative aspect-video">
<img
src={video.thumbnail}
alt={video.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 20 20">
<path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
</svg>
</div>
</div>
<p className="p-2 font-medium">{video.title}</p>
</div>
))}
</div>
</div>
</div>
);
}
10. 모바일 최적화
10.1 모바일 친화적 이미지
import Image from 'next/image';
function MobileOptimizedImage() {
return (
<div className="w-full">
<Image
src="/images/mobile-hero.jpg"
alt="Mobile hero"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="w-full h-auto rounded-lg"
/>
</div>
);
}
10.2 모바일 비디오 최적화
function MobileVideo() {
return (
<div className="w-full">
<video
className="w-full rounded-lg"
controls
playsInline
preload="metadata"
>
<source src="/videos/mobile-video.mp4" type="video/mp4" />
</video>
</div>
);
}
11. 에러 처리
11.1 이미지 에러 처리
'use client';
import Image from 'next/image';
import { useState } from 'react';
function ImageWithErrorHandling({ src, alt }) {
const [hasError, setHasError] = useState(false);
const [imageSrc, setImageSrc] = useState(src);
const handleError = () => {
if (!hasError) {
setHasError(true);
setImageSrc('/images/placeholder.jpg');
}
};
return (
<Image
src={imageSrc}
alt={alt}
width={800}
height={600}
onError={handleError}
className="rounded-lg"
/>
);
}
11.2 비디오 에러 처리
'use client';
import { useState } from 'react';
function VideoWithErrorHandling({ src, poster }) {
const [hasError, setHasError] = useState(false);
const handleError = () => {
setHasError(true);
};
if (hasError) {
return (
<div className="w-full aspect-video bg-gray-200 rounded-lg flex items-center justify-center">
<p className="text-gray-500">비디오를 불러올 수 없습니다.</p>
</div>
);
}
return (
<video
src={src}
poster={poster}
controls
className="w-full rounded-lg"
onError={handleError}
/>
);
}
12. 결론
Next.js의 Image 컴포넌트와 Tailwind CSS를 활용하면 이미지와 비디오를 효과적으로 삽입하고 최적화할 수 있습니다. 자동 이미지 최적화, 반응형 처리, 성능 향상 등의 이점을 얻을 수 있으며, Tailwind CSS의 유틸리티 클래스를 통해 일관된 디자인을 쉽게 구현할 수 있습니다.
주요 포인트:
- Next.js Image 컴포넌트: 자동 최적화와 지연 로딩으로 성능 향상
- Tailwind CSS 스타일링: 빠르고 일관된 디자인 구현
- 반응형 디자인: 다양한 디바이스에서 최적의 경험 제공
- 접근성: alt 텍스트, 캡션, 자막 등으로 모든 사용자 접근 가능
- 성능 최적화: 지연 로딩, 프리로딩, 에러 처리로 안정적인 사용자 경험
이미지와 비디오는 웹사이트의 시각적 매력을 높이고 사용자 경험을 향상시키는 중요한 요소입니다. 올바른 도구와 기법을 사용하여 성능과 디자인을 모두 잡는 것이 중요합니다.