[실전 성능 03] 첫 화면이 뜨는 시간

지난 편에서 핵심 지표 세 가지를 정리했다. 이번 편부터는 하나씩 깊이 들어간다. 첫 타자는 로딩 지표, 즉 화면에서 가장 큰 콘텐츠가 그려지기까지의 시간이다. 사용자가 사이트에 도착해 가장 먼저 겪는 순간이고, 느리면 아예 떠나버리는 지점이라 나는 여기에 가장 많은 공을 들였다.


1. 가장 큰 콘텐츠란 무엇인가

이 지표는 화면에 보이는 가장 큰 요소가 언제 그려지는지를 잰다. 보통은 큰 이미지나 큰 제목 텍스트, 또는 배경 영상 같은 것이 그 주인공이 된다. 사용자 눈에는 이 큰 덩어리가 떠야 비로소 페이지가 열렸다고 느껴진다. 작은 아이콘 몇 개가 먼저 떠도 사람은 아직 로딩 중이라고 여긴다.


내 사이트에서 이 주인공은 대개 글 목록의 첫 썸네일이나 게시글의 대표 이미지였다. 처음 측정했을 때 이 이미지가 뜨는 데 시간이 꽤 걸렸는데, 이유를 파고드니 여러 단계가 순서대로 쌓여 있었다. 서버가 응답하고, 문서가 파싱되고, 스타일과 스크립트가 처리되고, 그제야 이미지 요청이 나가고, 다운로드가 끝나야 그려졌다. 이 사슬 어느 한 곳만 늘어져도 전체가 늦어졌다.


그래서 나는 이 지표를 단일 문제로 보지 않고 사슬로 봤다. 가장 큰 콘텐츠가 뜨기까지의 경로를 처음부터 끝까지 그려놓고, 어느 마디가 가장 긴지 찾았다. 병목은 늘 한두 마디에 몰려 있었고, 거기만 손봐도 전체가 눈에 띄게 당겨졌다. 사슬 전체를 골고루 조금씩 줄이는 것보다 가장 긴 마디를 집중 공략하는 게 효과적이었다. 나는 각 마디에 걸린 시간을 나눠 적어놓고 가장 굵은 막대부터 손댔는데, 그렇게 하니 적은 노력으로도 전체 시간이 크게 당겨지는 마디를 놓치지 않았다.


흥미로운 건 주인공이 상황에 따라 바뀐다는 점이었다. 어떤 페이지에서는 텍스트 제목이, 어떤 페이지에서는 배너 이미지가 가장 큰 요소였다. 그래서 페이지마다 무엇이 주인공인지 먼저 확인하고, 그 요소를 빨리 그리는 데 집중했다. 엉뚱한 요소를 최적화하면 지표는 꿈쩍도 안 했다. 측정 도구는 어느 요소가 그 순간의 주인공이었는지 친절하게 알려줬는데, 나는 그 정보를 먼저 확인하지 않고 짐작으로 덤볐다가 시간을 버린 적이 여러 번이라 이 습관이 생겼다.


2. 서버가 첫 바이트를 늦게 줄 때

사슬의 맨 앞은 서버 응답이었다. 서버가 첫 바이트를 늦게 주면 그 뒤 모든 단계가 통째로 밀렸다. 아무리 이미지를 최적화해도 첫 응답이 1초 늦으면 화면도 1초 늦었다. 그래서 나는 사슬의 맨 앞부터 점검했다. 첫 바이트까지의 시간이 로딩 지표의 가장 큰 지렛대라는 걸 측정으로 확인했기 때문이다.


내 사이트는 엣지에서 코드가 도는 구조라, 사용자 가까운 곳에서 응답이 시작되는 이점이 있었다. 그런데도 첫 응답이 느린 경우가 있었는데, 대부분 서버가 응답을 만들려고 데이터베이스를 무겁게 조회하고 있었기 때문이다. 목록을 그리려고 매번 전체를 훑는 식이면 응답이 늦어졌다. 이건 뒤에 캐시 편에서 자세히 다룬다.


첫 바이트를 앞당기는 가장 확실한 방법은 미리 만들어 두는 것이었다. 매 요청마다 계산하지 않고, 자주 나가는 화면은 결과를 저장해 두었다가 그대로 내보냈다. 그러면 서버는 조회 없이 즉시 응답했고, 첫 바이트가 극적으로 빨라졌다. 계산을 요청 시점에서 미리 시점으로 옮기는 것이 핵심이었다. 사용자가 기다리는 순간에 무거운 일을 하지 않고, 한가할 때 결과를 만들어 쟁여두는 발상은 뒤에 나올 캐시 이야기 전체를 관통하는 원리이기도 했다.


또 하나, 응답이 시작되는 위치도 중요했다. 사용자가 지구 반대편인데 서버가 한 곳에만 있으면 왕복 시간만으로도 수백 밀리초가 깨졌다. 엣지 네트워크에서 사용자 가까운 지점이 응답하도록 하니 이 왕복이 줄었다. 물리적 거리는 코드로 못 줄이지만, 응답 지점을 사용자 쪽으로 옮기면 거리를 줄인 효과가 났다.


3. 렌더링을 막는 것들 걷어내기

서버 응답이 빨라도 브라우저가 그리기를 미루면 소용없었다. 문서를 받은 브라우저는 스타일과 스크립트를 처리하느라 화면 그리기를 멈추곤 했다. 특히 문서 머리에 있는 무거운 스타일 파일과 동기 스크립트가 그리기를 붙잡았다. 이걸 걷어내는 것이 사슬 중간 마디를 줄이는 일이었다.


나는 첫 화면에 꼭 필요한 스타일만 문서 안에 직접 넣고, 나머지는 나중에 불러오게 했다. 당장 필요 없는 스크립트는 실행을 미루도록 표시해서 그리기를 막지 않게 했다. 이 작업만으로도 큰 콘텐츠가 뜨는 시점이 앞당겨졌다. 브라우저가 일단 그림을 그리고 나머지를 뒤에서 처리하도록 순서를 바꾼 것이다.


이 부분은 워낙 중요해서 나중에 렌더링 블로킹만 다루는 편을 따로 뒀다. 여기서는 원리만 짚는다. 첫 화면을 그리는 데 필요 없는 것은 전부 뒤로 미룬다는 원칙이다. 머리에 있는 모든 동기 자원은 곧 첫 그림을 늦추는 세금이라고 생각하면 판단이 쉬웠다.


미룬다고 다 좋은 건 아니었다. 첫 화면을 구성하는 스타일까지 미루면 오히려 화면이 뒤늦게 완성되며 흔들렸다. 그래서 첫 화면에 필요한 최소한은 즉시, 나머지는 나중이라는 경계를 잘 긋는 것이 관건이었다. 이 경계를 잘못 그으면 로딩과 안정성이 함께 나빠졌다. 나는 첫 화면에 실제로 보이는 것이 무엇인지 눈으로 확인한 뒤 그 경계를 그었고, 확신이 안 서면 미루기 전후를 각각 측정해 어느 쪽이 사용자에게 이득인지 숫자로 판단했다.


4. 주인공 이미지를 우대하기

큰 콘텐츠가 이미지일 때는 그 이미지를 특별 대우했다. 기본적으로 브라우저는 이미지를 만나는 순서대로 처리하는데, 정작 첫 화면의 주인공 이미지가 뒤늦게 발견되면 늦게 떴다. 그래서 나는 주인공 이미지는 미리 불러오도록 신호를 줘서 브라우저가 일찍 요청하게 했다.


반대로 첫 화면 밖에 있는 이미지들은 지연로딩으로 미뤘다. 스크롤을 내려야 보이는 썸네일까지 처음부터 다 받으면 정작 주인공 이미지의 대역폭을 빼앗겼다. 중요한 것은 앞당기고 덜 중요한 것은 미루는 이 우선순위 조정이 로딩 지표를 크게 흔들었다. 같은 자원을 어떤 순서로 받느냐의 문제였다. 대역폭은 한정돼 있어서, 지금 안 보이는 것을 먼저 받으면 지금 보여야 할 것이 그만큼 늦어지는 제로섬 게임에 가까웠다.


이미지 자체를 가볍게 만드는 것도 당연히 중요했다. 큰 원본을 화면 크기에 맞게 줄이고, 효율적인 포맷으로 바꾸니 다운로드 시간이 짧아졌다. 다만 이건 이미지 최적화 편에서 따로 깊이 다루기로 하고, 여기서는 로딩 사슬에서 이미지가 차지하는 비중이 크다는 점만 강조해둔다.


결국 주인공 이미지 하나를 위해 여러 조치를 겹쳤다. 일찍 요청하게 하고, 크기를 줄이고, 경쟁 자원을 미뤘다. 이 조합이 맞아떨어지자 큰 콘텐츠가 뜨는 시점이 초 단위로 당겨졌다. 하나의 조치보다 여러 조치의 합이 만든 결과였다.


5. 측정으로 마무리 짓기

이 모든 작업 뒤에는 항상 측정이 따라왔다. 나는 변경 전 로딩 시간을 기록하고, 각 조치마다 다시 재서 어느 것이 얼마나 기여했는지 확인했다. 그러다 보면 효과가 클 줄 알았는데 미미한 것도 있었고, 사소해 보였는데 크게 당긴 것도 있었다. 측정 없이는 이 순위를 몰랐을 것이다.


특히 현장 데이터로 확인하는 게 중요했다. 내 기기에서는 이미 빨라서 더 개선할 게 없어 보여도, 현장 75 백분위수는 여전히 통과선 밖일 수 있었다. 느린 기기와 느린 회선을 쓰는 사용자들의 로딩이 진짜 과제였다. 나는 내 화면이 아니라 그 분포를 보고 판단했다. 내 좋은 장비에서만 확인하고 끝냈다면 통과선 밖의 사용자들은 영영 방치됐을 것이고, 그들이야말로 조용히 떠나 다시 오지 않는 사람들이었다.


로딩을 통과선 안으로 넣고 나니 사용자 반응이 눈에 띄게 달라졌다. 첫 화면이 빨리 뜨는 것만으로도 사이트가 살아 있다는 인상을 줬고, 이탈이 줄었다. 첫인상을 좌우하는 지표라, 여기에 쏟은 노력의 보상이 가장 직접적이었다. 사용자는 뜨는 순간부터 사이트를 평가하고 있었다.


로딩은 빨라졌지만 아직 안심하긴 일렀다. 화면이 빨리 떠도 그 화면이 흔들리면 사용자는 또 다른 종류의 짜증을 낸다. 다음 편에서는 시각 안정성, 즉 떠 있는 화면이 갑자기 밀리는 현상을 다룬다. 내가 이미지 때문에 크게 데였던 그 문제의 정체를 파헤쳐 보겠다.