개발

confeti 웹 성능 최적화 여정

hskkkk 2025. 5. 27. 05:39

성능 최적화가 왜 중요할까?

웹에서 이미지는 페이지 로딩 속도와 사용자 경험에 큰 영향을 미친다. 최적화되지 않은 이미지는 페이지 로딩을 느리게 하고 사용자 이탈률을 높이며, SEO에도 부정적인 영향을 미칠 수 있다. 여러 요소들을 개선하여 이미지를 비롯한 전체적인 성능 최적화를 진행하려고 한다.

 

성능 개선에 앞서, 간단하게라도 브라우저 렌더링 과정을 알아보자.

브라우저 렌더링 과정
HTML 파싱 → DOM 생성 → CSS 파싱 → CSSOM 생성 → 렌더 트리 구성 → 레이아웃(리플로우) → 페인트 → 컴포지트


이때 HTML을 파싱하다가 CSS나 JavaScript를 만나면 브라우저는 파싱을 중단하고 해당 리소스를 처리한다. 특히 CSS는 CSSOM 구성을 위해 완전히 다운로드되고 파싱될 때까지 렌더링을 차단하며, JavaScript는 DOM 조작 가능성 때문에 실행이 완료될 때까지 파싱을 멈춘다. 이것이 바로 렌더링 블로킹의 주요 원인이다.

또한 페이지가 로드된 후에도 DOM이나 스타일이 변경되면 레이아웃 재계산(리플로우)과 다시 그리기(리페인트)가 발생하는데, 이는 렌더링 성능에 직접적인 영향을 준다.

 

confeti 성능 측정 결과

LightHouse로 콘페티 웹 사이트의 성능을 측정해봤다.

 

참고로 배포된 사이트에서 Desktop 기준으로 측정했다. 

 

다음 결과를 보니, LCP의 개선이 시급하다고 생각했고, 홈페이지의 캐러셀에서 서버에서 받아온 s3 이미지를 그대로 렌더링하고 있었는데 이미지의 크기와 용량이 상당히 커 load delay를 유발하고 있다고 파악했다.

가장 큰 병목이라고 판단되는 LCP를 개선하면, 나머지 지표도 어느정도 상승할 것이라 판단하여 LCP 개선을 먼저 진행하였다.

LCP에 영향을 주는 4가지 주요 요인

1. TTFB (첫 바이트까지의 시간)
서버 응답 속도 문제
해결: 캐싱 최적화, CDN 활용

2. 리소스 로드 지연
중요한 이미지/폰트 요청이 늦게 시작
해결: preload, 우선순위 조정

3. 리소스 로드 시간
큰 파일 다운로드 시간
해결: 이미지 압축, WebP 포맷, CDN

4. 요소 렌더링 지연
다운로드 후 화면 표시까지 지연
해결: 렌더링 블로킹 요소 제거, JS 최적화

 

그래서 해결 방안을 하나 하나 실행해보기로 했다. 

 

개선 작업

1) Rendering block 요소 제거

performance 탭을 확인해보니, rendering block하는 요소가 GA script, font CSS, 그리고 google login script로 세 가지 있었다. (왜 캡처는 안 해놨을까..) 기본적으로 브라우저는 외부 css를 동기적으로 로드하기 때문에 css가 다운로드되고 파싱되는 동안 모든 페이지 렌더링을 중단하는데, 폰트 css는 특히 폰트를 다운받는 시간이 길어서 rendering block을 유발하고 있었다. 

 

기존 방식

global.css 다운로드 ➡️ CSS 파싱 중 @import 발견 ➡️ 렌더링 중단하고 폰트 CSS 다운로드 ➡️  폰트 CSS 파싱 중 폰트 파일들 발견 ➡️  또 렌더링 중단하고 폰트 파일들 다운로드 ➡️  모든 다운로드 완료 후에야 렌더링 시작 

 

이렇게 CSS를 파싱해야만 다음 리소스를 알 수 있고, 각 단계마다 렌더링이 멈추는 연쇄적인 렌더링 차단을 유발하기에 @import 방식이 성능에 좋지 않다고 한다.

 

수정 방식

 폰트 CSS를 index.html에서 link태그로 다음과 같이 불러오도록 해서 해결했다.

<link
      href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.min.css"
      rel="preload"
      as="style"
    />
    <link
      href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-dynamic-subset.min.css"
      rel="stylesheet"
      media="print"
      onload="this.media='all'"
    />

 

폰트 css가 렌더링을 차단하지 않고 비동기적으로 로드되도록 하는 방법으로, preload는 HTML에서 미리 모든 리소스를 알려주어 브라우저가 병렬로 다운로드 시작하도록 한다. 페이지 렌더링과 폰트 로딩이 병렬처리 되도록 해주었다.

 

HTML 파싱 중 preload 발견 ➡️  즉시 폰트 CSS 다운로드 시작 ➡️  global.css도 병렬로 다운로드 ➡️  렌더링 블로킹 없이 동시에 처리 

 

그리고 또다른 rendering block 요소인 ga script와 google login script에 각각 defer, async 속성을 추가해주어 파싱과 병렬로 script 다운로드가 이루어지도록 해주었다. 

 

브라우저가 html과 css를 파싱하는 과정에서 script 태그를 마주치면 렌더링이 멈추는데, 다음과 같은 속성은 병렬 처리를 도와준다.

위 작업을 거치고 나니, performance 탭에서 rendering block이라고 뜨는 요소는 없어졌다.

 

2) 이미지 CDN 구축 및 연결

앞서 말했듯, LightHouse 측정 결과를 봤을 때 가장 큰 병목이라고 생각되는 건 바로 홈 페이지의 캐러셀 이미지의 용량이다. 서버에서 받아온 s3 url을 그대로 렌더링하고 있었는데 개발자 도구를 확인해보니 실제 렌더링 되어야 하는 크기에 비해 이미지의 크기가 매우 컸고, 용량 또한 커서 로드 시간이 매우 긴 것을 확인했다.

홈 캐러셀 이미지의 Content Download 시간..

 

로드 delay의 가장 큰 원인이라, 용량과 리사이징이 시급하다고 판단됐다! 서버에서 받아오는 이미지이기 때문에 이미지 cdn을 구축하여 최적화를 진행하기로 결정했다.

 

이미지 cdn이란?

cdn은 전 세계 여러 곳에 서버를 두고, 사용자와 가장 가까운 서버에서 이미지를 보내주는 서비스를 말한다.

이미지 cdn은 이미지에 특화된 cdn이라고 볼 수 있다. 기본적인 cdn 기능을 더불어, 이미지를 사용자에게 보내기 전에 특정 형태로 가공하여 전해주는 기능까지 있다. 

 

Imgix 이미지 CDN 써서 성능 최적화해보기

 

사용자가 이미지를 요청 ➡️ 가장 가까운 Imgix 서버로 라우팅 ➡️ 캐시된 이미지 버전을 신속하게 전달

 

요청된 이미지가 변환이 필요한 경우(예: 크기 조정, 형식 변환 또는 최적화) Imgix 서버는 원본 서버에서 이미지를 가져와 실시간으로 처리를 한 후 사용자에게 전달한다. 즉, 이미지 저장소의 개념보단 원래 있던 이미지를 최적화하여 캐시된 상태로 전달해준다고 보면 된다.

 

1) Imgix 사이트 가입

Imgix는 사진 1000장까지 무료로 제공한다.

 

2) web folder 방식 선택

여러 방식이 있지만 나는 web folder 방식을 선택했다. aws S3 버킷에 직접 연결하는 방법도 있었으나 서버측에 IAM 계정을 요청해야 하고, web folder 방식은 그냥 내려주는 s3 url을 키값으로 하여 cdn url로 갈아 끼우면 되기 때문에 언제든지 클라쪽에서 cdn을 해체하기도 쉽고 추가적인 소통이 필요하지 않다고 생각하여 채택했다.

 

3) base url 설정 및 코드 변경

안내에 따라 base url을 설정해주고, 실제 코드를 변경해주면 된다.

url의 파라미터를 통해 포맷 형식과 품질, 크기 등을 최적화할 수 있다.

(적용할 수 있는 parameter 참고: https://docs.imgix.com/en-US/getting-started/setup/serving-assets#applying-parameters )

 

실제 코드:

`${CONFIG.IMAGE_CDN_URL}${cleanPath}?w=${width}&h=${height}&auto=format,enhance&q=${quality}`

 

auto=format 은 브라우저가 webp 형식을 지원한다면, 이미지를 webp 형식으로 변환하라는 뜻이다. 위와 같이 파라미터를 통해 최적화 설정을 가능하게 한다.

 

before

 

after

 

홈 캐러셀 이미지의 용량과 로드 시간이 매우 단축되었다! 모두 webp로 변환되어 렌더링되고 있고, 이미지 사이즈도 설정한 대로 렌더링되는 것을 확인했다.

 

3) 번들 사이즈 축소

마지막으로 번들 사이즈를 체크하기 위해 먼저 트리맵을 확인했다.

cf) 위 이미지는 로컬에서 캡처한 트리맵 사진으로, 실제 배포된 사이트에는 포함되지 않는 번들 또한 포함 되어있다.

 

제일 첫 번째 박스를 보면, 3D logo svg 파일이 무려 500kib도 넘게 차지하고 있다. 따라서 public으로 옮겨 img 태그를 사용해 public에서 불러오도록 수정해주었더니, 이 박스는 사라졌다.

 

또한 하나의 컴포넌트에서만 사용하고 있던 mui를 삭제했다. mui는 peer dependency로 Emotion 라이브러리를 필수 설치해야 하는데, 이미 Vanilla Extract를 스타일링 라이브러리로 사용하는 앱에서 오로지 하나의 컴포넌트를 위해 추가 스타일링 라이브러리를 도입하는 것은 비효율적이라고 판단하여 mui와 emotion을 삭제했더니 거의 400kib를 차지하고 있던 번들이 날라갔다.

 

마지막으로 로딩 컴포넌트에서 쓰이는 로띠 파일이 매우 큰 번들 사이즈를 갖고 있음을 확인했는데, Suspense로 앱 전체를 감싸고 fallback ui로 이 로띠 애니메이션을 불러오고 있어 동적 import 해주기도 애매하다. 모든 페이지의 로딩화면에서 큰 용량의 로띠 파일을 불러오고 있기 때문에,, 일단은 추후 개선으로 미뤘다. 이외에 특별히 코드 스플릿팅을 적용해야 할 컴포넌트 파일은 보이지 않았다.

 

로컬 vs 배포 환경, Mobile vs Desktop 성능 차이

로컬에서 최적화해도 점수가 안 오른 이유

이미지 CDN 연결, 렌더링 블로킹 요소 제거 등 다양한 최적화를 진행했지만, 로컬 환경에서 Lighthouse 점수가 단 1점도 오르지 않았다.

그 이유를 파헤쳐보니, 로컬 개발 서버는 텍스트 압축이 적용되지 않으며 HTTP/2를 지원하지 않고 있었다.

특히 JavaScript와 CSS 파일의 텍스트 압축이 없어서 번들 크기가 실제 배포 환경보다 2-3배 더 컸고, 이 병목이 너무 커서 다른 최적화 효과가 전혀 드러나지 않았던 것이다! 휴..

 

배포 환경에서 드러난 최적화 효과

따라서 일단 머지하고 배포된 사이트에서 다시 확인해보았다.

Vercel은 자동 텍스트 압축(Gzip 등), HTTP/2 지원 등을 제공하기 때문에 기본 인프라 최적화가 이미 적용된 상태이므로, 내가 처음에 배포된 사이트에서 파악한 원인을 바탕으로 개선해주었더니 예상했던대로 LCP 및 다른 지표들이 많이 개선이 된 것을 확인했다!

배포된 사이트에서 Desktop 기준으로 측정한 결과

추측하건대 로띠 파일만 개선하면 100점도 나올 것 같다. 개선 전과 비교해 무려 40점 가량을 상승시켰고, LCP를 6.5s → 1.5s로 약 97% 단축시켰다.

 

만약 프로젝트를 Vercel로 배포했다면 개발 중에는 Chrome DevTools의 Performance 탭으로 렌더링 최적화를 확인하고, 최종 검증은 배포 후 Lighthouse CI로 다시 검증하는 것을 추천한다 !

 

Mobile vs Desktop 성능 격차

또다른 이슈로는, mobile 기준으로 측정하면 Desktop과 점수 차이가 많이 난다는 것을 확인할 수 있다.

사실 모바일에서 성능 점수는 좀 떨어질 수 밖에 없다고 한다. 그 이유는 Lighthouse에서 모바일 환경에서 측정하면 기본적으로 네트워크 환경이 좀 떨어지는 환경을 상정하고 성능을 측정하기 때문이다.

google.com만 봐도 Desktop은 98점인데 Mobile로 측정하면 70점이 나오는 걸 확인할 수 있다..!

 

현재 주요 최적화는 완료된 상태로, 투입 노력 대비 '모바일' 성능 지표 개선 효과는 미미할 것으로 예상된다.. (로띠 제외)

그러나 배포 환경의 모바일 환경 트리맵에서 나타나는 React DevTools 이슈는 기묘해서 추후 개선이 필요할 것 같긴 하다.