logo

DowanKim

LCP를 개선해보자.

2026년 9월 20일68
PLick

1. 문제인식

홈 화면의 핫이슈 캐러셀에는 사진이 들어갑니다. 그 사지은, 릴스 임베드를 받아오고 그 본문에 있는 이미지 Url을 기반으로 다시 받아오는 방식이기에, LCP가 늦어도 어쩔 수 없다는 생각을 가지고 있었습니다.

하지만 라이트하우스를 돌려보면 제 생각보다도 너무나 큰 LCP값과, 낮은 점수를 확인할 수 있었습니다.

<점수판 66점, LCP 7.7초> image.png

LCP란?

쉽게 말해 화면에서 제일 큰 조각이 언제 다 그려지느냐 입니다.(비유상 크다고 말하는 겁니다.)

구글은 2.5초 안을 좋음으로 판단하고 합니다. 저희 홈 화면에서 LCP대상은 캐러셀의 이미지 였습니다.

image.png TTFB는 서버가 첫 바이트를 주기까지, Load Delay는 브라우저가 사진이 필요하다는 걸 알기 까지, Load Time은 다운로드, Render Delay는 받고 나서 그리기 까지 입니다.

자세히 보면, 로드타임이 LCP 중 92퍼센트를 차지하고 있습니다.

즉 서버 응답이나 렌더링이 문제라기 보다는, 다운로드에 문제가 있다고 생각했습니다.

왜 오래걸리는거야 그럼

<렌더링 228x142 vs 고유 1179x852> image.png

= 카드는 작은데 받아오는 사진은 엄청 큼

<951KiB 중 936KiB 낭비> image.png

실제 기기에서도 느린지

<쓰로틀링 없는 실측에서 3.7초> image.png

원인 코드

<위쪽 사진은 핫이슈 카드가 쓰는 tweet-media.ts, 아래는 릴스 임베드가 쓰는 tweet.ts. 같은 트위터 사진을 한쪽은 Name=large, 다른 쪽은 name=small로 받음> image.png

image.png

트위터 신디케이션 API가 주는 사진 주소는 크기 옵션이 없는 원본 경로라서 무언가를 붙여야 하고, 그때 제일 큰 버전이라는 뜻으로 name=large를 상수로 넣어뒀습니다.(물론 제가 아니라 AI가.. 저는 이제 알게되었습니다..)

이 파일은 packages/core 에 있어서 홈, 팀 허브, 데스크톱 웹이 다같이 사용합니다. 모든 곳에서 전부 같은 크기의 사진을 받고 있습니다.

아래 사지은 릴스 임베드 코드 입니다. 주석에 나온 것처럼 첫 릴스의 사진을 preload 하기 위해 주소를 만드는데, 그 주소가 실제 임베드가 그리는 주소와 한글자라도 다르면 브라우저가 같은 사진을 두번 받습니다. 그래서 react-tweet 라이브러리가 내부에서 쓰는 small을 그대로 적은 것입니다.

사실상 같은 트위터 사진 주소를 만드는 규칙이 두곳에 다르게 적혀 있는 것입니다.

조금 더 쉽게 말하면 이렇습니다.

트위터 사진 주소는 뒤에 붙이는 옵션에 따라 다른 크기의 파일을 줍니다. 같은 사진을 name=small로 받으면 680px짜리, name=large로 받으면 2048px짜리가 옵니다.

문제는 그 뒤 핫이슈 카드가 화면 절반 크기로 줄었는데, 사진 옵션은 여전히 large였다는 겁니다.

2. 해결 방안

어느 크기로 받아야 할까

<트위터 CDN 제공 크기>

image.png

image.png

small 이면 괜찮을 거 같습니다. 물론 데스크톱 에서 사진이 확대될 수 있지만, 사실 저희에게 사진의 화질은 그렇게 중요한 문제가 아닙니다. 정적인 사진임과 동시에 사실 UI/UX 디자이너와의 소통에서 정보 제공을 위해 필수적으로 사진을 넣는다기 보단, 배치 상으로, 심심함을 없애기 위해 넣는 사진이기에 감수하기로 했습니다.

webp도 고려할 수 있습니다. 위 사진에는 클로드가 멋대로 jpg를 선택한 것으로 되어있지만, 표를 보면 small 중에서도 webp를 쓰면 더욱 효과적입니다.

srcset

srcset은 사진 주소를 여러개 주고 브라우저가 화면에 맞는걸 고르게 하는 기능입니다.

우리 상황에서는 사실 필요가 없다고 판단했습니다. 모든 상황에서 small 사진만 사용할 수 있기 때문입니다.

규칙 통일

image.png

앞서 클로드가 jpg를 추천했던 이유가 있습니다. 릴스 임베드는 react-tweet이라는 라이브러리로 사용하는데 , 이 라이브러리가 사진 주소를 jpg로 만듭니다.

그리고 릴스에는 첫 사진을 미리 받아두는 preload가 있다고 말씀드렸습니다. 이때 이 주소가 임베드가 실제로 그리는 주소와 한글자라도 다르면 브라우저가 같은 사진을 두번 요청합니다.

그래서 클로드에게 물어봤습니다. "react-tweet에서도 webp로 받게 하면 되는거 아냐? 그걸 지원안하지 않을텐데...?"

역시나 webp도 지원했습니다. 예외는 영상 릴스의 포스터 이미지인데, 이건 라이브러리가 직접 그려서 바꿀 수 없고 사진만 webp로 맞췄습니다.

3. 적용

주소를 만드는 함수를 하나로 합치기

근본 원인은 트위터 사진 주소를 만드는 규칙이 두곳에 있었던 것이었습니다.

이에 앞서 정한 규칙을 한곳으로 모았습니다.

<주소에서 확장자를 떼어 format= 으로 옮기고 name= 으로 크기 붙임> image.png

이 함수가 하는 일은 단순히 원본 주소를 받으면 .jpg를 떼서 뒤에 format과 name을 붙입니다. 포맷을 안주면 원래 확장자를 그대로 두고, 앞서 영상 포스터 같은 케이스를 챙겼습니다.

이렇게 공용으로 옮기면서 앞서 상수로 되어있던 name=large를 삭제하고, react-tweet 을 사용하는 코드도 변경했습니다.

결과

사진 전송량 감소

먼저 코드를 바꾸기 전과 후를 같은 조건에서 측정해야합니다.

prod는 배포 전이라 비교가 어려우니, 로컬에서 각각 같은 방식으로 빌드해 띄우고 라이트하우스를 3번씩 돌려 중앙값을 측정했습니다.

<홈 핫이슈 사진 세장 925KiB -> 170KiB 82% 감소>

home-image-delivery-insight-twitter.png

점수가 왜 오르지 않을까

그러나 예상 밖의 결과가 나왔습니다. 전송량이 82% 줄었지만, 라이트하우스 성능 점수는 78에서 79로, LCP는 6.0초에서 5.6초 입니다.

home-largest-contentful-paint-element.png

리포트를 분석해달라고 클로드에게 요청하니, 사진 3장과 같은 순간에 322 KiB 짜리 글꼴 파일이 같은 우선순위로 출발하고 있었습니다. 라이트하우스 기본 측정은 실제로 느린 네트워크를 쓰는게 아닌, 4g라면 이정도 걸렸을 것이라고 계산하는 시뮬레이션이고, 이 계산은 동시에 출발한 요청들이 대역폭을 나눠쓴다고 봅니다. 그래서 사진이 아무리 작아져도 글꼴이 다 내려올 때까지 사진도 같이 기다리는 것으로 계산됩니다. 사진을 해결하니 글꼴 문제가 파악이 된 것입니다.

그러나 이후 실질적으로 분석해보니, 이미 저희는 가변 글꼴 자체 호스팅을 하고 있고, 다른 방식을 고려하는, 예를들어 굵기별로 가져오는 것은 실험 결과 더 큰 용량을 불러들이고, 웹폰트를 호스팅하는 것은 더더욱 큰 병목을 만들어냅니다. 지금이 최선임을 감안하고 넘어갑니다.

실제 네트워크 측정

시뮬레이션이 사진 변화를 못 보여줘, 같은 4g 수치를 크롬에 실제로 걸고 재는 방식인 --throttling-method=devtools 으로 재측정한다고 클로드가 말했습니다.

before

home-scores.png

after

home-scores.png

점수가 크게 올랐습니다.

home-largest-contentful-paint-element.png

다운로드가 2.75초 입니다. 글꼴과 대역플을 나눠쓰는 몫. 최선.

로컬 이미지들도 동일한 문제

트위터 사진 묶음이 사라지자 그 아래 있던 저희 서버 파일 묶음이 눈에 들어왔습니다. 헤더 로고 PNG 39KiB, 팀 로고 3장 31KiB, 그리고 전 화면이 받는 앱 아이콘 153KiB. 이번에 같이 해결했습니다.

image.png

중요한 점을 깨달았습니다.

디자이너한테 받은 svg가 진짜 svg가 아닐 수 있습니다

로고 svg파일이 실제로는 png를 단순히 감싼 형태였습니다. path 태그가 없는 걸로 확인할 수 있었습니다.

또한 저희는 이때까지 public폴더의 파일은 s3로 가지 않고, ec2에 자체적으로 저장하고 있었습니다. 넥스트는 public 파일에는 해시를 만들지 않기 때문에 이전에 그렇게 판단하고 서버에서 서빙하는 전략을 유지했었습니다. 하지만 사실 로고와 팀 로고들은 바뀔 일이 없기 때문에, 전략을 변경했습니다

해결방법은 로고 파일들을 public에서 제외하고 코드에서 Import 하는 것입니다. 이렇게 되면 넥스트가 파일 이름에 해시를 붙여 static 빌드 결과물에 들어가고 s3에서 서빙하게 되며, cloudfront에 캐싱됩니다.

결과적으로 LCP가 크게 개선이 되었습니다.

AI를 사용한 개발에서 사실상 단기간에 이해하지 못하고 정말 많은 개발을 하게 되어, 다양한 알지 못했던 문제가 발생하고 있는 것 같습니다.

기본적으로 라이트하우스를 돌렸을 때 나오는 문제들은 하나씩 집어가며 해결을 하고자 노력해야 함을 알았습니다.