![[실전 성능 07] 이미지를 가볍게](https://img.thenullpage.com/posts/5761/5761_1_2d7bb9.webp)
지난 편에서 이미지가 화면을 밀어내는 문제를 잡았다. 그런데 밀림을 없애도 이미지가 무거우면 여전히 로딩이 늦고 데이터가 낭비됐다. 이번 편은 이미지 자체를 가볍게 만드는 이야기다. 커뮤니티 사이트는 이미지가 많아서, 이 최적화의 효과가 사이트 전체 성능에 크게 반영됐다. 내가 실제로 적용한 방법들을 순서대로 풀어본다.
1. 원본을 그대로 쓰지 않기
초기의 가장 큰 실수는 사용자가 올린 원본 이미지를 그대로 보여준 것이었다. 요즘 폰으로 찍은 사진은 화면에 필요한 것보다 훨씬 크고 무거웠다. 목록의 작은 썸네일 자리에 수천 픽셀짜리 원본을 그대로 내려보내니, 브라우저는 그 큰 파일을 다 받아서 작게 줄여 그렸다. 다운로드는 오래 걸리고 데이터는 낭비됐다.
해법은 서버에서 미리 화면에 맞는 크기로 줄여두는 것이었다. 목록용 썸네일은 작게, 본문용은 적당한 크기로 여러 판본을 만들어뒀다. 그러면 각 자리에 딱 맞는 크기의 가벼운 이미지가 나갔다. 큰 원본은 보관만 하고, 실제로 화면에 나가는 건 용도에 맞게 줄인 판본이었다. 이것만으로 이미지 데이터가 크게 줄었다.
크기를 줄일 때는 화면에 실제로 표시되는 크기를 기준으로 삼았다. 작은 썸네일 자리에 큰 이미지를 넣는 건 낭비였고, 반대로 크게 보여야 할 곳에 너무 줄인 이미지를 넣으면 흐릿했다. 나는 각 자리의 표시 크기를 확인하고 그에 맞는 판본을 배정했다. 고해상도 화면을 위해 약간의 여유는 뒀지만, 필요 이상으로 크지는 않게 조절했다.
이 작업은 업로드 시점에 한 번만 해두면 됐다. 이미지를 받을 때 여러 크기로 변환해 저장해두니, 이후 조회할 때마다 다시 처리할 필요가 없었다. 앞 편들에서 반복된 미리 해두기 원리가 이미지에도 그대로 적용됐다. 한 번의 처리로 수많은 조회의 부담을 없앤 것이다. 저장 공간을 조금 더 쓰는 대신 전송 비용을 크게 아꼈다. 저장은 상대적으로 싸고 전송은 사용자의 시간과 데이터를 직접 갉아먹기에, 이 교환은 거의 언제나 남는 장사였고 나는 망설임 없이 여러 판본을 미리 만들어 쟁여뒀다.
2. 효율적인 포맷 쓰기
크기를 줄이는 것 다음은 포맷이었다. 오래된 이미지 포맷은 같은 화질을 더 큰 파일로 표현했다. 반면 요즘의 효율적인 포맷은 같은 화질을 훨씬 작은 파일로 담았다. 나는 사이트의 이미지를 이런 효율적인 포맷으로 변환해 내보냈다. 같은 사진이라도 포맷만 바꿔서 파일 크기가 눈에 띄게 줄었다.
포맷 변환은 화질과 크기의 균형점을 찾는 일이기도 했다. 압축을 세게 하면 파일은 작아지지만 화질이 뭉개졌고, 약하게 하면 화질은 좋지만 파일이 컸다. 나는 사람 눈에 화질 저하가 거의 안 느껴지는 선에서 최대한 압축하는 지점을 찾아 기준으로 삼았다. 이 균형점은 실제 이미지들을 눈으로 비교하며 정했다.
다만 모든 브라우저가 최신 포맷을 지원하는 건 아니라 대비가 필요했다. 나는 효율적인 포맷을 기본으로 쓰되, 지원하지 않는 환경에서는 널리 통하는 포맷으로 대체되도록 했다. 사용자 환경에 따라 자동으로 맞는 포맷이 나가게 하니, 최신 브라우저는 가벼운 파일을, 오래된 브라우저도 무리 없이 이미지를 받았다. 호환성을 지키면서 이득을 챙겼다.
애니메이션이 들어간 이미지나 영상 썸네일도 별도로 신경 썼다. 움직이는 이미지는 정지 이미지보다 훨씬 무거웠고, 무심코 큰 파일을 그대로 쓰면 로딩이 크게 늦어졌다. 나는 이런 무거운 자원일수록 포맷과 크기를 더 깐깐하게 관리했다. 무거운 것부터 손대는 것이 효과가 컸기 때문이다. 가벼운 아이콘 백 개를 다듬는 것보다 무거운 움직이는 이미지 하나를 잡는 편이 전체 전송량을 훨씬 크게 줄여줬으니, 여기서도 측정으로 가장 무거운 것부터 찾는 원칙이 그대로 통했다.
3. 지연로딩으로 나중에 받기
이미지를 가볍게 만들어도, 한 화면에 수십 개가 한꺼번에 로딩되면 부담이 컸다. 그래서 지금 화면에 보이지 않는 이미지는 나중에, 사용자가 스크롤해서 가까워질 때 불러오도록 했다. 지연로딩이라 부르는 이 방식으로 초기 로딩 부담을 크게 덜었다. 첫 화면에 필요한 이미지만 먼저 받으니 시작이 훨씬 빨라졌다.
지연로딩은 데이터 절약 측면에서도 좋았다. 긴 목록에서 사용자가 위쪽 몇 개만 보고 떠나면, 아래쪽 이미지는 아예 다운로드되지 않았다. 보지도 않을 이미지를 미리 받는 낭비를 없앤 것이다. 특히 모바일 데이터를 쓰는 사용자에게는 이 절약이 실질적인 배려였다. 안 보이는 걸 안 받는 것만으로 상당한 대역폭을 아꼈다.
다만 앞 편에서 말했듯 지연로딩은 밀림과 짝을 이뤄야 했다. 자리를 미리 예약하지 않고 지연로딩만 하면 스크롤 중에 이미지가 튀어나오며 화면이 출렁였다. 그리고 첫 화면의 주요 이미지는 지연로딩에서 제외해야 로딩 지표가 나빠지지 않았다. 나는 미루는 것과 즉시 받는 것의 경계를 신중하게 그었다.
지연로딩은 요즘 브라우저가 기본으로 지원해서 적용이 어렵지 않았다. 이미지에 미룸 표시만 해두면 브라우저가 알아서 뷰포트 근처에서 불러왔다. 복잡한 코드 없이 표시 하나로 큰 효과를 얻은 셈이다. 나는 목록의 모든 비첫화면 이미지에 이 표시를 붙였고, 그것만으로 초기 로딩이 가벼워졌다.
4. 전송 계층에서 더 아끼기
이미지를 가볍게 만든 뒤에는 전송 과정에서 더 아낄 여지를 찾았다. 같은 이미지를 여러 사용자가 반복해서 받는다면, 사용자 가까운 곳에 캐시해두고 재사용하는 것이 효율적이었다. 엣지 네트워크가 이미지를 캐시해주니, 두 번째 사용자부터는 원본 서버까지 가지 않고 가까운 곳에서 받았다. 전송 거리가 짧아지니 로딩도 빨라졌다.
브라우저 캐시도 활용했다. 한 번 받은 이미지는 사용자 브라우저에 저장해두고, 다시 방문하면 다운로드 없이 재사용하게 했다. 자주 바뀌지 않는 이미지일수록 이 캐시가 오래 유지되도록 설정했다. 재방문 사용자에게는 이미지가 즉시 뜨는 셈이라 체감 속도가 크게 좋아졌다. 한 번 받은 걸 또 받지 않게 하는 것이 핵심이었다.
화면 크기에 따라 다른 이미지를 보내는 것도 전송 절약이었다. 작은 모바일 화면에 큰 데스크탑용 이미지를 보내는 건 낭비였다. 브라우저가 화면에 맞는 판본을 골라 받도록 여러 크기를 제공하니, 각 기기가 자기에게 필요한 만큼만 다운로드했다. 특히 모바일에서 이 절약이 컸다. 화면이 작을수록 덜 받는 것이 합리적이었다.
이런 전송 최적화는 이미지를 가볍게 만드는 것과 곱해져서 효과를 냈다. 파일 자체를 줄이고, 필요한 것만 받고, 받은 건 재사용하니 이미지로 인한 부담이 여러 겹으로 줄었다. 한 가지 기법의 효과는 작아 보여도, 여러 겹이 쌓이니 전체 로딩이 확연히 가벼워졌다. 나는 이 여러 계층을 함께 관리했다.
5. 이미지 예산이라는 관점
이미지 최적화를 하며 갖게 된 관점 하나는 예산이라는 개념이었다. 페이지 하나가 쓸 수 있는 데이터 총량에 한도를 정해두고, 이미지가 그 예산을 넘지 않도록 관리하는 것이다. 예산을 정하니 무심코 큰 이미지를 넣는 습관이 잡혔고, 새 이미지를 추가할 때마다 이게 예산 안에 드는지 따지게 됐다.
예산 관점은 판단을 쉽게 해줬다. 어떤 이미지를 넣을지 말지, 얼마나 압축할지 고민될 때 예산이 기준이 됐다. 화려한 이미지를 잔뜩 넣고 싶어도 예산을 넘으면 덜어냈다. 이렇게 하니 디자인 욕심과 성능 사이에서 균형을 잡을 수 있었다. 무제한이라 생각하면 끝없이 무거워졌지만, 한도가 있으니 절제가 생겼다.
이 예산은 특히 느린 회선 사용자를 위한 것이었다. 빠른 회선에서는 무거운 페이지도 금방 뜨지만, 느린 회선에서는 이미지 하나하나가 시간이 됐다. 예산을 지키면 느린 환경에서도 페이지가 감당할 만한 무게로 유지됐다. 나는 항상 가장 불리한 사용자를 기준으로 예산을 잡았다. 그들이 견딜 수 있으면 나머지는 당연히 쾌적했다. 빠른 환경을 기준으로 잡으면 느린 사용자가 늘 소외되지만, 느린 환경을 기준으로 잡으면 모두가 만족하는 하한선이 자연스럽게 정해졌다.
이렇게 이미지를 가볍게 만드는 여러 방법을 살펴봤다. 이미지는 커뮤니티 사이트에서 가장 무거운 자원이라 여기서 아낀 효과가 컸다. 다음 편에서는 또 다른 흔한 성능 요소인 폰트를 다룬다. 폰트도 잘못 쓰면 화면을 늦추고 흔들리게 만드는데, 내가 데스크탑과 모바일을 나눠 접근한 이유를 함께 풀어보겠다.