이미지 갤러리 최적화
- 4.1 레이아웃 이동 피하기 (CLS)
- 4.2 이미지 지연 로딩
- 4.3 리덕스 렌더링 최적화
- 4.4 병목 코드 최적화
이미지 갤러리를 열면 이미지들이 로드되면서 레이아웃이 들썩들썩 움직이는 경험 해본 적 있지? 이게 바로 **CLS(Cumulative Layout Shift)**야.
이미지가 로드되기 전에는 높이가 0이었다가, 로드되면서 갑자기 공간을 차지하니까 아래 콘텐츠가 밀려내려가는 거거든. Core Web Vitals 세 가지 지표 중 하나이고, Google 검색 랭킹에도 영향을 줘. 읽고 있던 텍스트가 갑자기 내려가거나, 클릭하려던 버튼이 밀려서 엉뚱한 걸 누르게 되는 사용자 경험 문제가 직접적으로 생기지.
해결 방법은 의외로 간단해 — 이미지 영역의 크기를 미리 잡아두면 돼. <img> 태그에 width와 height 속성을 지정하면 브라우저가 이미지 로드 전에 가로세로 비율을 계산해서 공간을 미리 확보해. CSS의 aspect-ratio 속성으로 고정하는 방법도 있고, placeholder로 블러 처리된 저해상도 이미지를 먼저 보여주는 Medium 스타일 기법도 있어. CLS를 유발하는 다른 원인들로는 동적으로 삽입되는 광고 배너, 웹 폰트 로드 후 텍스트 크기 변화, API 응답 후 리스트 추가 같은 것들도 있어.
이미지 갤러리는 이미지가 수십~수백 장이니까 지연 로딩의 효과가 더 크지. rootMargin을 설정하면 뷰포트 아래 200px까지 미리 감지해서, 사용자가 스크롤했을 때 이미 로드가 완료되어 있을 확률이 높아져. 네이티브 지연 로딩(<img loading="lazy" />)도 있지만, 세밀한 제어가 필요하면 Intersection Observer를 직접 쓰는 게 나아.
리덕스 쪽에서도 성능 문제가 숨어 있어. 이미지 하나를 클릭하면 모달이 열리는데 갤러리 전체가 리렌더링되는 거야. useSelector가 매번 새 객체를 반환하면 리덕스가 "값이 바뀌었네" 하고 리렌더링을 트리거하거든. 해결법은 세 가지야. useSelector를 분리해서 원시값을 반환하게 하거나, shallowEqual을 두 번째 인자로 넘겨서 얕은 비교를 하거나, Reselect의 createSelector로 메모이제이션하는 거지.
// 나쁜 예 - 매번 새 객체 생성
const { modalOpen, selectedImage } = useSelector(state => ({
modalOpen: state.modal.open,
selectedImage: state.modal.image
}));
// 좋은 예 - 원시값 반환
const modalOpen = useSelector(state => state.modal.open);
const selectedImage = useSelector(state => state.modal.image);
병목 코드 최적화도 한 단계 올라가. 1장에서는 비효율적인 코드를 효율적으로 바꾸는 수준이었는데, 여기서는 아키텍처 레벨까지 가거든. useMemo로 같은 이미지에 같은 필터를 다시 적용할 때 이전 결과를 캐싱하거나, Web Worker로 CPU 집약적 작업을 메인 스레드에서 빼서 UI가 안 얼게 만드는 거야. 이미지 처리 같은 무거운 작업은 메인 스레드에서 수백 ms 동안 점유하면서 UI를 얼려버리거든.
정리
4장 읽고 기억할 거 세 가지:
- CLS는 사전에 공간을 확보하면 해결된다. width/height 명시, aspect-ratio, placeholder — 핵심은 이미지 로드 전에 레이아웃이 확정되어 있어야 한다는 것
- 리덕스 리렌더링은 셀렉터 설계가 전부. 새 객체를 반환하는 셀렉터가 불필요한 리렌더링의 주범. useSelector 분리, shallowEqual, Reselect로 해결
- CPU 집약적 작업은 메인 스레드에서 빼라. 메모이제이션으로 반복 계산을 줄이고, 그래도 무거우면 Web Worker로 넘겨서 UI 응답성을 지켜야 한다