Docker
이미지 로딩 중에 입력이 먹히지 않는 UI 버그
사용자가 폼을 입력하는데 도중에 큰 이미지 로딩이 간섭해서 UI가 응답 안 할 때의 원인 파악.
리액트 폼을 채우고 있는데, 배경에서 이미지가 로딩되고 있으면 UI가 "끊긴다". 인풋 필드를 클릭해도 반응이 느리고, 이미지가 다 로드될 때까지 버벅거린다.
이 문제는 메인 스레드가 이미지 디코딩과 렌더링으로 블로킹되기 때문이다. JavaScript는 싱글 스레드인데, 큰 이미지를 처리하느라 사용자 입력을 무시하게 된다.
1단계: 병목 지점 확인
Chrome DevTools의 Performance 탭을 사용한다:
1. F12 열기 → Performance 탭
2. 폼 입력하면서 이미지 로딩 시작
3. 기록 시작 (Ctrl+Shift+E)
4. 입력하면서 느려지는 순간 기록 중단
5. 플레임 차트 분석
보통 두 가지가 보인다:
- Long tasks: 50ms 이상 걸리는 메인 스레드 작업
- Rendering: 레이아웃, 페인트 같은 브라우저 작업
이미지 로딩 중에 Long task가 생기면, 그게 원인이다.
2단계: 어느 이미지가 느린지 확인
# Network 탭에서 이미지별 크기와 다운로드 시간 확인
DevTools의 Network 탭에서:
- 이미지 크기: 수 MB라면 너무 크다
- 다운로드 시간: 3G 네트워크라고 가정해도 5초 이상이면 긴 편
// 또는 JavaScript에서 확인
const img = new Image()
img.onload = () => {
console.log(`Image loaded in ${Date.now() - start}ms`)
console.log(`Image size: ${img.naturalWidth}x${img.naturalHeight}`)
}
img.src = url
const start = Date.now()
3단계: 이미지 포맷 최적화
// 나쁜 예: 5MB PNG
<img src="/hero.png" />
// 좋은 예: 여러 포맷 제공
<picture>
<source srcSet="/hero.webp" type="image/webp" />
<source srcSet="/hero.jpg" type="image/jpeg" />
<img src="/hero.jpg" alt="Hero" />
</picture>
WebP는 JPEG보다 30-40% 작다. Next.js Image 컴포넌트는 자동으로 최적화한다:
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority // 중요한 이미지면 우선순위 높임
/>
4단계: 이미지 로딩을 Non-blocking으로 만들기
// 나쁜 패턴: await로 블로킹
async function loadImage(url) {
const img = new Image()
return new Promise((resolve) => {
img.onload = () => resolve(img)
img.src = url // 여기서 블로킹 가능
})
}
// 좋은 패턴: 백그라운드에서 로딩
function preloadImage(url) {
const img = new Image()
img.src = url // 비동기로 로딩
return new Promise((resolve) => {
img.onload = () => resolve(img)
})
}
사용자 입력에 직접 영향을 주지 않는 이미지는 preload로 미리 준비하되, 기다리지 않는다.
5단계: Web Workers로 이미지 처리
큰 이미지를 여러 장 처리해야 한다면, 메인 스레드가 아닌 웹 워커에서 처리한다:
// worker.js
self.onmessage = async (event) => {
const { imageUrl } = event.data
const img = new Image()
await new Promise((resolve) => {
img.onload = resolve
img.src = imageUrl
})
// 이미지 처리 (메인 스레드와 무관)
self.postMessage({ status: 'done', imageUrl })
}
// 메인 스레드
const worker = new Worker('worker.js')
worker.postMessage({ imageUrl: '/large-image.jpg' })
worker.onmessage = (event) => {
console.log('Image ready:', event.data)
}
메인 스레드는 자유로우므로 사용자 입력에 즉시 반응한다.
6단계: 사용자 입력에 즉각적인 피드백 주기
// onChange 핸들러
const handleChange = (e) => {
// 1. 즉시 상태 업데이트 (동기)
setValue(e.target.value)
// 2. 유효성 검사를 debounce로 미룸 (비동기)
clearTimeout(validationTimer)
validationTimer = setTimeout(() => {
validateValue(e.target.value)
}, 500)
}
입력은 즉시 반영되지만, 무거운 작업 (검증, API 호출)은 나중에 처리한다.
7단계: Lazy loading 이미지
<img
src="/placeholder.jpg"
loading="lazy"
alt="Content"
/>
// 또는 Intersection Observer
const imageRef = useRef()
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
imageRef.current.src = imageRef.current.dataset.src
observer.unobserve(imageRef.current)
}
})
observer.observe(imageRef.current)
}, [])
<img
ref={imageRef}
src="/placeholder.jpg"
data-src="/actual-image.jpg"
alt="Content"
/>
뷰포트에 진입할 때만 이미지를 로드한다.
8단계: 용량 줄이기
# ImageMagick으로 이미지 압축
convert large.jpg -quality 80 -resize 1920x1080 optimized.jpg
# 또는 ffmpeg
ffmpeg -i large.jpg -q:v 5 optimized.jpg
체감상 품질 저하가 크지 않으면서도 파일 크기를 50-70% 줄일 수 있다.
9단계: 서드파티 스크립트 확인
// Analytics, ads 같은 스크립트가 이미지 로딩을 간섭하는가?
// DevTools Network 탭에서 타이밍 확인
Google Analytics, Facebook Pixel 같은 스크립트가 메인 스레드를 블로킹하면, 이미지 로딩과 상관없이 UI가 느려진다.
필요하면 이들을 async 또는 defer 속성으로 로드하자:
<script async src="https://analytics.google.com/..."></script>
최종 체크리스트
- DevTools Performance 탭에서 Long task 확인
- Network 탭에서 이미지 크기와 로딩 시간 확인
- 이미지 포맷 최적화 (WebP, 압축)
- 폼 입력이 즉시 반영되도록 구조 변경
- 무거운 작업은 debounce로 미룸
- 필요한 이미지만 미리 로드 (priority 속성)
- 불필요한 이미지는 lazy loading
- 메인 스레드를 블로킹하는 JavaScript 없는지 확인
대부분의 "느린 UI" 문제는 이미지 최적화와 작업 분산으로 해결된다.