
이미지 리플레이스 전략은 웹사이트나 애플리케이션에서 기존 이미지를 다른 이미지로 교체하는 기술적 방법들을 의미한다.
핵심은 개발자가 의도적으로 설정한 조건에 따라 이미지가 교체되는 것이다.
개발자가 미리 조건과 교체 규칙을 선언해두는 방식
<img
src="image-800w.jpg"
srcset="image-400w.jpg 400w,
image-800w.jpg 800w,
image-1200w.jpg 1200w"
sizes="(max-width: 600px) 400px, 800px"
/>
srcset의 경우 이미지 리플레이스 전략인가에 대해 애매한 경계선에 있다.
관점에 따른 분류
1. 이미지 리플레이스가 아니라는 관점
- 개발자가 직접 교체를 제어하지 않는다
- 브라우저가 자동으로 최적 이미지를 선택한다
- 사용자나 개발자의 의도적인 "교체" 액션이 없다
2. 이미지 리플레이스라는 관점
- 실제로는 다른 이미지 파일이 로드된다
- 조건(화면 크기, 해상도)에 따라 이미지가 바뀐다
- 결과적으로 "교체"가 일어난다
이렇게 갈리는데 개인적으로는 이미지 리플레이스 전략이라고 생각한다.
이유는 개발자가 직접 교체를 제어하거나 교체 명령으로 액션을 일으키는 것은 아니지만,
“이런 조건에서는 이 이미지로 교체해라”라고 전략을 세워 놓은 것이니까 이미지 리플레이스 전략이라고 생각한다.
<picture>
<source media="(max-width: 768px)" srcset="mobile.jpg" />
<source media="(min-width: 769px)" srcset="desktop.jpg" />
<img src="fallback.jpg" alt="반응형 이미지" />
</picture>
<picture>
<source srcset="image.avif" type="image/avif" />
<source srcset="image.webp" type="image/webp" />
<img src="image.jpg" alt="최적화된 이미지" />
</picture>
개발자가 런타임에 직접 제어하는 방식
const [isHovered, setIsHovered] = useState(false);
return (
<img
src={isHovered ? 'hover-image.jpg' : 'normal-image.jpg'}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
alt="인터랙티브 이미지"
/>
);
// 테마에 따른 교체
<img src={isDarkMode ? 'dark-logo.png' : 'light-logo.png'} alt="로고" />
// 언어에 따른 교체
<img src={lang === 'ko' ? 'korean-banner.jpg' : 'english-banner.jpg'} alt="배너" />
const [selectedImage, setSelectedImage] = useState(images[0]);
return (
<div>
<img src={selectedImage} alt="선택된 이미지" />
{images.map((img, index) => (
<button
key={index}
onClick={() => setSelectedImage(img)}
>
이미지 {index + 1}
</button>
))}
</div>
);
.button {
background: url(normal-button.png);
}
.button:hover {
background: url(hover-button.png); /* 다른 이미지 파일로 교체 */
}
/* 이것은 이미지 교체가 아니라 같은 이미지의 다른 부분을 보여주는 것 */
.button {
background: url(sprite.png) 0 0;
}
.button:hover {
background-position: 0 -50px; /* 같은 이미지 내에서 위치만 변경 */
}
const AccessibleImage = ({ src, alt, decorative = false, longDescription }) => {
const descriptionId = useId();
if (decorative) {
return <img src={src} alt="" role="presentation" />;
}
return (
<>
<img
src={src}
alt={alt}
aria-describedby={longDescription ? descriptionId : undefined}
/>
{longDescription && (
<div id={descriptionId} className="sr-only">
{longDescription}
</div>
)}
</>
);
};
const AccessibleImageReplace = ({ normalSrc, hoverSrc, alt }) => {
const [currentSrc, setCurrentSrc] = useState(normalSrc);
const [isLoading, setIsLoading] = useState(false);
const handleMouseEnter = () => {
setIsLoading(true);
const img = new Image();
img.onload = () => {
setCurrentSrc(hoverSrc);
setIsLoading(false);
};
img.src = hoverSrc;
};
return (
<div>
{isLoading && (
<span className="sr-only">이미지를 변경하는 중입니다</span>
)}
<img
src={currentSrc}
alt={alt}
onMouseEnter={handleMouseEnter}
onMouseLeave={() => setCurrentSrc(normalSrc)}
/>
</div>
);
};
interface SmartImageReplaceProps {
normalSrc: string;
hoverSrc?: string;
alt: string;
className?: string;
priority?: boolean;
}
const SmartImageReplace: React.FC<SmartImageReplaceProps> = ({
normalSrc,
hoverSrc,
alt,
className,
priority = false
}) => {
const [currentSrc, setCurrentSrc] = useState(normalSrc);
const [isHovered, setIsHovered] = useState(false);
const [preloadedImages, setPreloadedImages] = useState<Set<string>>(new Set());
// 호버 이미지 프리로드
useEffect(() => {
if (hoverSrc && priority) {
const img = new Image();
img.onload = () => {
setPreloadedImages(prev => new Set(prev).add(hoverSrc));
};
img.src = hoverSrc;
}
}, [hoverSrc, priority]);
const handleMouseEnter = () => {
if (hoverSrc) {
setIsHovered(true);
if (preloadedImages.has(hoverSrc)) {
setCurrentSrc(hoverSrc);
} else {
const img = new Image();
img.onload = () => setCurrentSrc(hoverSrc);
img.src = hoverSrc;
}
}
};
const handleMouseLeave = () => {
setIsHovered(false);
setCurrentSrc(normalSrc);
};
return (
<img
src={currentSrc}
alt={alt}
className={`transition-opacity duration-200 ${
isHovered ? 'opacity-90' : 'opacity-100'
} ${className}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
loading={priority ? 'eager' : 'lazy'}
/>
);
};
이미지 리플레이스 = 이미지 교체
선언적 vs 명령적
접근성 필수 고려
성능 최적화와 구분