프론트엔드 개발을 공부하며 리액트 같은 프레임워크도 중요하지만, 결국 '브라우저가 리소스를 어떻게 받아오고 렌더링하는가'에 대한 기본기가 성능을 좌우한다는 걸 깨달았다. 오늘은 프로젝트 배포 후 겪었던 '아이콘 깜빡임' 현상을 해결하기 위해 이미지 스프라이트 기법을 도입하여 네트워크 비용을 최적화했던 과정을 정리해보려고 한다.
몇 주 전, 기능 구현을 마치고 배포된 사이트를 확인하던 중 이상한 현상을 목격했다. 로컬에서는 파바박 뜨던 아이콘들이, 실제 배포 환경에서는 텍스트가 먼저 뜨고 나서 한참 뒤에야 '툭, 툭, 툭' 하며 시간차를 두고 나타나는 것이었다.
그리고 점입가경으로 순차적으로 뜨는 아이콘 때문에 레이아웃이 미세하게 밀리는 현상까지 발생했다.
이유를 파악하기 위해 개발자 도구의 Network 탭을 열어 원인을 파악했다.

페이지 내에 사용된 SNS 아이콘, 화살표, 메뉴 버튼 등 자잘한 이미지 파일이 20개가 넘었는데, 브라우저가 이 20개의 파일을 서버에 '각각' 요청하고 있었기 때문이다.
게임 개발에서 유래된 이미지 스프라이트 기법은 여러 개의 작은 이미지를 하나의 큰 이미지 파일(Sprite Sheet)로 합친 뒤, CSS의 background-position 속성을 이용해 필요한 부분만 창문처럼 보여주는 기술이다.
이미지를 이렇게 처리하면 20번의 가량의 HTTP 요청이 무려 1번의 요청으로 획기적 감소하고, 이미지가 한 번에 로드되므로 :hover 시 이미지가 교체될 때 발생하던 깜빡임 현상이 사라진다.
찾아보니까 이미지를 직접 합쳐주는 툴이 되게 많았다. 나는 웹 기반의 CSS Sprite Generator를 사용했다.
이미지를 합치고 나면 아래와 같은 하나의 거대한 PNG 파일이 생긴다. 이제 CSS로 각 아이콘을 불러와야 한다. 여기서 음수(-) 좌표값에 대한 개념을 이해하는 것이 중요하다.
(설명: 뷰포트(div)는 고정되어 있고, 배경 이미지가 왼쪽/위쪽으로 이동하는 개념도)
/* 공통 스타일: 한 번만 불러온다 */
.icon {
display: inline-block;
background-image: url('./assets/images/sprite_icons.png');
background-repeat: no-repeat;
}
/* 개별 아이콘: 좌표만 이동시킨다 */
.icon-instagram {
width: 24px;
height: 24px;
/* x축으로 -10px, y축으로 -10px 이동 */
background-position: -10px -10px;
}
.icon-facebook {
width: 24px;
height: 24px;
/* 옆에 있는 아이콘을 끌어오기 위해 더 많이 이동 */
background-position: -44px -10px;
}
왜 마이너스일까?
배경의 이동 방향이라고 이해하면 쉽다.
'배경 이미지를 왼쪽으로 10px 당겨야'<div>에 해당 아이콘이 보이기 때문이다.
이미지를 적용하고 뿌듯해하던 찰나 아이콘이 뿌옇게 보이는 문제가 발생했다. 검색해보니 맥북은 고해상도라 물리 픽셀이 2배 더 촘촘하기 때문이라고 한다. 많이 겪는 문제라 해결 방법은 꽤 많이 알려져 있었다. 그래서 다음과 같이 정석적인 방법으로 문제를 해결했다.
해결 방법:
background-size 속성을 이용해 이를 절반 크기로 압축한다. (예: 250x250px).icon {
background-image: url('./assets/images/sprite_icons@2x.png');
/* 핵심: 원본 이미지 크기의 정확히 '절반' 값을 적어줘야 한다 */
background-size: 250px 250px;
}
주의: background-position 좌표값도 2배 큰 이미지 기준이 아니라, 압축된(절반) 크기를 기준으로 계산해서 넣어야 한다.
스프라이트를 쓰면 <img> 태그 대신 <span>이나 <i> 태그에 배경을 입히게 된다. 즉, alt 속성을 쓸 수 없다. 스크린 리더 사용자는 여기가 무슨 버튼인지 알 도리가 없다.
웹 퍼블리싱할 때까지만 해도 text-indent: -9999px로 글자를 화면 밖으로 넘겼는데 이는 검색엔진 최적화(SEO)나 성능상 좋지 않다는 것을 깨닫고 요즘은 이미지 대체 기법인 IR 중 Clip 패턴을 사용하려고 한다.
/* 접근성을 위한 유틸리티 클래스 */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: polygon(0 0, 0 0, 0 0); /* 최신 브라우저 */
clip: rect(0, 0, 0, 0); /* 구형 브라우저 호환 */
}
<button class="btn-search">
<span class="icon icon-search"></span>
<span class="visually-hidden">검색하기</span>
</button>
이렇게 처리해야 시각장애인 사용자도 "검색하기 버튼"이라고 명확하게 인식할 수 있다.
이미지 스프라이트를 적용하며 내내 든 생각은 요즘 HTTP/2는 멀티플렉싱이 돼서 여러 개 요청해도 괜찮지 않나라는 의문이 들기도 했다. 왜냐면 나 네트워크 환경에서 이미지가 뚝뚝 끊겨 나온 것은 의도적으로 '3G'나 'slow 4G'로 했을 경우에 발생했던 문제였기 때문이다.
그래서 여러 커뮤니티를 통해 알아보니 실제로 HTTP/2 환경에서는 스프라이트의 중요도가 예전보다는 낮아졌다고 한다.
하지만 그럼에도 내가 느낀 바는