💻용뇽 개발 노트💻
article thumbnail
반응형

프론트엔드에서 성능 최적화가 가능한 N가지 방법(feat.React)

1. 📖 들어가며

이번 글에서는 프론트엔드에서 성능 최적화를 하기 위해서는 어떤 방법이 있는지 살펴보겠습니다.
코드 예시는 React 코드 기반으로 보여드리지만, VanilaJS, 다른 프레임워크에서도 활용할 수 있는 기법들입니다.

해당 글에서는 각 방법에 대해서 동작원리와 같은 이론적인 부분에 대해서는 깊게 설명하지 않습니다!
크게 어떤 방법들이 있는지, 어떤 개념인지, 어떻게 하는 건지, 그래서 어떤 이점이 있는지 이 총 4가지에 집중하여 설명합니다.
그래서 처음 알게된 방법에 대해서는 "이런 것도 있구나! 바로 한번 적용해볼까?"라는 생각이 들었으면 좋겠어요.🤣


최적화를 진행하기 전에는 브라우저 개발자 도구 > 성능 탭 또는 네트워크 탭, Lighthouse, React-Dev-Tools와 같은 성능 측정을 통해서 전과 후 비교를 통해서 얼마나 개선이 되었는지 수치로 확인해보시길 적극 추천드립니다.

2. N가지 최적화 방법들

2.1. 🖼️ 이미지 지연 로딩(Lazy Loading)

웹 페이지에서 이미지는 무거운 리소스 중 하나입니다.
모든 이미지를 한 번에 로드하면 초기 로딩 시간이 길어지고 불필요한 데이터 사용이 발생하게 됩니다.


이미지 지연 로딩은 웹페이지의 모든 이미지를 초기 로드 시점에 한꺼번에 다운로드 하지 않고, 사용자의 viewport(화면에 보이는 영역)에 이미지가 들어올 때 또는 들어오기 직전 해당 이미지만 선택적으로 불러오는 기술입니다.

Intersection Observer API나 scroll event를 활용하여 이미지의 가시성을 감지하여 이미지 리소스를 요청하는 방법, 브라우저 기본 지원 속성인 loading="lazy"를 사용하는 방법들이 존재합니다.

예시를 알아볼게요.

이미지 슬라이더가 필요한 상황이라고 가정해봅시다.

2.1.1. 적용 전

<javascript />
// 지연 로딩 적용 ❌ {images.map((imageUrl, index) => ( <SwiperSlide key={index}> <img src={imageUrl} alt={`${index} + 이미지}`} /> </SwiperSlide> ))}

이미지 지연로딩을 적용하지 않은 경우에는 보이지 않는 영역의 이미지도 모두 불러오고 있습니다.

2.1.2. 적용 후

<javascript />
// 지연 로딩 적용 ✅ {images.map((imageUrl, index) => ( <SwiperSlide key={index}> <img src={imageUrl} alt={`${index} + 이미지}`} loading="lazy" /> </SwiperSlide> ))}

실제로 화면 영역에 보일 때 이미지 리소스를 요청하는 것을 확인할 수 있습니다!

하지만 눈치가 빠르신 분들은 보셨겠지만, loading="lazy"만 사용할 경우 이미지가 로드되기 전까지 빈 공간이나 흰색 영역만 보이는 문제가 발생합니다. 특히 네트워크가 느린 환경에서는 이런 빈 공간이 더 오래 노출되어 사용자 경험을 저하시킬 수 있어요.

브라우저에서 자체적으로 지원하는 loading="lazy"는 코드 한 줄로 정말 편리하지만 시각적 피드백 측면에서는 기능이 제한적입니다.
UX를 고려하시는 개발자 분들이라면! fallbackUI를 고려해 보세요.

그 방법으로  간단한 경우에는 CSS와 placeholder 이미지를 적절하게 조합해서 보여주거나, Intersection ObserverAPI를 직접 활용하여 커스텀 로딩 상태를 처리하거나, 이미지 지연 로딩 관련 라이브러리 를 사용하여 보다 쉽게 처리를 시도해볼 수 있어요.

2.1.3. Trade-off

  • 브라우저 네이티브 lazy 활용 ➡️ 코드 한 줄로 구현 가능, 브라우저 내장 최적화로 성능 우수, fallbackUI는 별도 구현 필요
  • Intersection Observer API 활용 ➡️ 세밀한 제어 가능, 다양한 로딩 전략 구현 가능, 적절한 성능, 코드양 증가
  • 관련 라이브러리 사용 ➡️ 로딩 지연과 fallbackUI 기능 통합 제공, 빠른 개발 가능,  의존성 추가

각자 상황에 맞는 방법을 선택하시면 될 것 같습니다.

2.2. 🗜️ 이미지 압축 및 사이즈 조절

이미지 압축 및 사이즈 조절은 웹사이트의 이미지 파일 크기를 최적화하는 종합적인 접근 방식입니다.
적절한 이미지 형식 선택(JPEG, PNG, WebP, AVIF 등), 품질 조정을 통한 손실 압축 등을 포함하죠.

화면 크기와 해상도에 맞게 최적화된 이미지도 제공할 수 있고, 차세대 이미지 포맷(WebP, AVFI)을 지원하는 브라우저에는 더 효율적인 압축률을 제공하는 형식으로 우선순위를 정할 수도 있어요.

이미지 CDN이나 자동화된 이미지 최적화 도구를 활용하여 서버 측에서 동적으로 이미지를 최적화하는 방법도 존재합니다.

2.2.1. 압축 및 사이즈 조절 전

해당 이미지는 11,375 × 8,992 사이즈약 1MB 크기를 가진 JPG 이미지입니다.
서비스에서 400x400 사이즈를 보여주기 위해서 직접 width와 height를 설정해 줬다고 가정해 볼게요.

<html />
<img src={largeImage} alt="image" width={400} height={400} />

압축 및 사이즈 조절 전

네트워크 탭을 확인해보면 실제로 약 1MB 정도의 이미지 리소스를 요청한 것을 확인할 수 있습니다.

2.2.2. 압축 및 사이즈 조절 후

압축 및 사이즈 조절 후

최대 약 98%까지 크기가 줄어든 것이 보이시나요?
네트워크탭 기준 이름 순서대로 원본 JPG > 압축 및 크기 조절 JPG > 압축 및 크기 조절 webp > 압축 및 크기 조절 avif 순서입니다.
실제 필요한 크기(400x400)만큼 크기를 조정하고 압축을 진행했을 뿐이에요.

주의할 점은 브라우저 지원에 따라서 최신 포맷(AVIF, WebP)을 우선적으로 제공하고 지원하지 않는 경우 이 외 포맷을 폴백 하는 방식으로 처리하는 것을 추천합니다.

<html />
<picture> {/* AVIF 지원 브라우저용 */} <source srcSet={largeImageAvif} type="image/avif" /> {/* WebP 지원 브라우저용 */} <source srcSet={largeImageWebp} type="image/webp" /> {/* 둘 다 지원하지 않는 브라우저용 폴백 */} <img src={largeImage2} alt="최적화 이미지" width={400} height={400} /> </picture>

이런 식으로 말이죠.
그럼 만약 AVIF를 지원하는 브라우저에서는 AVIF 이미지만 요청하여 브라우저에 보이게 됩니다.

 

압축 및 사이즈 조절을 진행함으로써 대역폭 사용량 감소 및 페이지 로드 시간을 줄일 수 있는 이점을 기대할 수 있습니다.

이미지 크기 조절 및 압축 사이트는 여러 서비스가 존재해요.
저는 아래 서비스를 주로 사용하고 있어요.
https://squoosh.app/

 

Squoosh

Simple Open your image, inspect the differences, then save instantly. Feeling adventurous? Adjust the settings for even smaller files.

squoosh.app

2.3. 🎬 동영상 파일 최적화

동영상 또한  파일 크기를 압축하고 효율적인 코덱(WebM)을 사용해서 브라우저 호환성을 고려한 소스 우선순위를 지정하는 방법으로 이미지 최적화 방식과 동일하게 활용이 가능합니다.

실제로 필요한 길이만 사용하고, 효율적인 코덱으로 변환, 압축을 진행하는 방식이죠.

차이 예시 이미지 및 코드는 위 이미지  압축 및 사이즈 조절 섹션과 동일하기 때문에 예시를 첨부하지는 않겠습니다.

간단하게만 보여드리자면,

<html />
<video controls preload="none" poster="thumbnail.jpg" width="640" height="360"> <!-- WebM이 지원되면 먼저 사용 (더 효율적인 코덱) --> <source src="video.webm" type="video/webm"> <!-- 대체 포맷으로 MP4 제공 --> <source src="video.mp4" type="video/mp4"> </video>

동영상도 source 태그를 지정하여 우선순위를 지정할 수 있어요.

동영상 압축 서비스는 media.io 서비스를 자주 이용하고 있습니다.

압축
https://www.media.io/apps/compressor/

 

Online Video, Audio and Image Compressor - Reduce Large Files Size Online

 

www.media.io

코덱 변환

https://www.media.io/apps/converter/

 

Online Video, Audio, and Image Converter with Batch Processing | Media.io Converter

 

www.media.io

2.4. 🔤 폰트 최적화

폰트 최적화는 웹 폰트 리소스 로딩 및 렌더링 방식을 개선하여 웹 성능과 UX를 향상시키는 기술입니다.
웹 폰트는 디자인의 중요한 요소지만, 최적화하지 않으면 로딩 시간 증가, 텍스트 깜빡임, Reflow 등의 문제를 일으킬 수 있습니다.


FOIT(Flash of Invisible Text)
폰트를 다운로드될 때까지 텍스트가 노출되지 않는 현상

FOUT(Flash of Unstyled Text)
폰트가 다운로드되기 전 기본 폰트를 노출하고 다운로드 후에 해당 폰트로 교체되는 현상


폰트 최적화 기법에는 여러 가지가 존재합니다.

아래에서 소개해드리는 기법들을 상황에 맞게 적용하여 최적화를 진행해 보세요!

2.4.1. font-display 속성 활용

font-display 속성은 폰트 로딩 중 텍스트를 어떻게 표시할지 제어합니다.

<css />
@font-face { font-family: 'MyWebFont'; src: url('myfont.woff2') format('woff2'), url('myfont.woff') format('woff'); font-display: swap; /* 핵심 속성 */ font-weight: normal; font-style: normal; }
  • swap: 폰트 로드 전까지 시스템 폰트로 표시 (FOUT, 가장 많이 사용됨)
  • block: 폰트 로드 될 때까지 텍스트 숨김, 3초 차단 기간 후 시스템 폰트 표시 (FOIT)
  • fallback: 매우 짧은 FOIT 후 시스템 폰트 표시, 3초 내 로드되면 웹폰트로 전환 (FOIT)
  • optional: fallback과 유사. 브라우저가 네트워크 상태에 따라 결정, 보통 한 번만 시도하고 캐시 (FOIT)
  • auto: 브라우저 기본값

2.4.2. 폰트 프리로딩

중요한 폰트는 프리로딩하여 페이지 로드 초기에 요청할 수 있어요.

<html />
<link rel="preload" href="fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>

"브라우저에게 이 리소스가 중요하니 먼저 가져와!"라고 알려주는 것이죠.
핵심 텍스트에 사용되는 폰트에만 사용하고, 남용하면 오히려 성능이 저하됩니다.

 

효율적인 폰트 포맷 사용

폰트에도 여러 포맷이 존재합니다.

  • WOFF2: 최신 포맷, 최고의 압축률 (원본 대비 30% 이상 감소), 최신 브라우저에서 지원
  • WOFF: 대부분의 브라우저에서 지원, 적절한 압축률
  • TTF/OTF: 레거시 지원용, 압축률 낮음
  • EOT: 오래된 IE 지원용, 거의 사용 안 함

WOFF2 -> WOFF -> TTF/OTF -> EOT 순으로 크기가 커집니다.

 

최신 포맷은 최신 브라우저에서만 지원하기 때문에 지원하는 브라우저에 맞는 포맷을 순차적으로 제공할 수 있어요.

<css />
@font-face { font-family: 'MyWebFont'; src: url('myfont.woff2') format('woff2'), url('myfont.woff') format('woff'), url('myfont.ttf') format('truetype'); font-display: swap; }

 

폰트 포맷 변환 사이트 에서 쉽게 하나의 폰트로 여러 포맷에 대한 폰트 파일을 변환할 수 있습니다!

2.4.3. 서브셋 폰트 사용

서브셋 폰트(Subset Font)는 필요한 문자만 다운로드해서 폰트 크기를 최소화 한 폰트입니다.

예를 들어, 한글 같은 경우 궳, 뛟, 쌻 등등 이런 사용하지 않는 문자들은 과감히 버리고 정말 사용할 글자만 폰트에 포함시키는 것이죠.

서브셋 웹 폰트로 변환하는 사이트 에서 변환을 해서 크기가 작은 새로운 폰트 파일을 생성할 수 있습니다.

폰트를 업로드하고 변환만 해주면 간단하게 변환이 되는데요.

해당 사이트는 한글을 지원하지 않기 때문에 직접 사용할 문자들을 넣어줘야 합니다.


구글에 "한글 서브셋 리스트"라고 검색하시면 쉽게 찾을 수 있습니다.

2.4.4. unicode-range 속성 사용

서브셋 폰트와 같이 조합해서 사용하면 최적화를 극대화할 수 있습니다!

이 방법은 동일한 폰트 파일을 사용하되, CSS에서 각 폰트 선언이 어떤 문자에 적용될지 지정하는 것입니다.

<css />
/* 라틴 문자용 폰트 선언 */ @font-face { font-family: 'MyFont'; src: url('myfont.woff2') format('woff2'); unicode-range: U+0000-00FF; /* 라틴 문자 범위 */ font-display: swap; } /* 한글용 폰트 선언 */ @font-face { font-family: 'MyFont'; src: url('myfont.woff2') format('woff2'); unicode-range: U+AC00-D7AF; /* 한글 범위 */ font-display: swap; }

원본 폰트 파일은 그대로 사용하고, 특정 폰트 선언이 적용될 유니코드 범위를 지정합니다.

그리고 브라우저가 페이지에 해당 범위의 문자가 있을 때만 폰트를 다운로드하게 되죠.

 

이렇게 하면 폰트 파일 크기도 작아지고, 필요한 경우에만 다운로드되는 최적의 폰트 전략을 구현할 수 있습니다.

2.5. 🌐 CDN 사용

서버와 사용자 간의 물리적 거리가 멀수록 데이터 전송 시간이 길어지고, 단일 서버는 트래픽 증가 시 병목 현상이 발생할 수 있어요.

 

CDN(Content Delivery Network)은 전 세계 여러 서버에 콘텐츠를 분산 저장하여 사용자와 가까운 위치에서 컨텐츠를 제공하는 서비스입니다.

정적 자산(이미지, CSS, JavaScript, 비디오 등)뿐만 아니라 동적 콘텐츠까지 처리할 수 있고, 고급 기능으로 자동 파일 압축, 이미지 최적화와 같은 추가적인 것들까지 처리할 수 있어요.

CDN 서비스 중에는 Cloudinary, Cloudflare, AWS CloudFront, Vercel, Netlify 등이 존재합니다.
CDN의 이미지를 활용한다고 가정했을 때 아래와 같이 사용할 수 있어요.

<javascript />
function OptimizedProductImage({ productId, alt }) { const baseUrl = "https://res.cloudinary.com/your-cloud-name/image/upload"; // 업로드 된 uri const transformations = "f_auto,q_auto,w_800,c_fill"; // 이미지 크기 동적으로 받기 const imagePath = `products/${productId}.jpg`; return ( <img src={`${baseUrl}/${transformations}/${imagePath}`} alt={alt} loading="lazy" /> ); }


CDN을 활용했을 때 이점은 다음과 같아요.

  • 로딩 속도 향상: 사용자와 가까운 서버에서 콘텐츠 제공
  • 서버 부하 분산: 원본 서버의 부담 감소
  • 가용성 향상: 일부 서버에 장애가 발생해도 서비스 유지
  • 보안 강화: DDos 방어, WAF 등 보안 기능 제공

2.6. 📜 대량의 데이터 리스트에 가상화 적용

가상화는 대량의 데이터를 효율적으로 표시하기 위한 렌더링 최적화 기술로, 실제로 화면에 보이는 요소와 그 주변의 일부만 실제 DOM 노드로 렌더링 하고, 화면 밖의 요소는 데이터만 메모리에 유지한 채 DOM 생성을 지연시키는 방식입니다.

적용하기 좋은 상황은 긴 목록 데이터 즉, 상품 페이지, 소셜 미디어 피드, 채팅 메시지 목록등 대량의 데이터를 보여줘야 하는 상황에서 적합해요. 그리고 무한 스크롤과 결합하여 사용하는 경우도 많습니다.

가상화를 직접 구현할 수 있지만 가상화를 지원하는 react-window, react-virtualized, react-virtuoso 등 잘 만들어진 라이브러리들도 존재합니다.
저는 예시에서 react-virtuoso 를 사용할 거예요.

 

2.6.1. 가상화 적용 전

예시로, 1만 개의 데이터를 가지고 있고, 이 데이터를 리스트로 보여줘야 하는 상황을 가정해 볼게요.

<javascript />
function Test() { // 대량의 데이터 (예: 10,000개 항목) const data = Array.from({ length: 10000 }).map((_, index) => ({ id: index, text: `항목 ${index + 1}`, })); return ( <div style={{ height: '400px', width: '100%', overflowY: 'auto' }}> {data.map((item) => ( <div key={item.id} style={{ padding: '12px', borderBottom: '1px solid #eee' }} > {item.text} </div> ))} </div> ); } export default Test;

가상화 적용전

화면이 렌더링 될 때 1만 개의 DOM 노드를 추가하여 렌더링 하는 것을 확인할 수 있습니다.
즉, 현재 보이지 않는 영역의 데이터까지 DOM 노드에 추가된 상황이죠.

초기에 대량의 데이터를 모두 불러와서 DOM 노드에 추가하여 렌더링 하기 때문에 초기 로딩 속도 또한 당연히 느릴 것입니다.

2.6.2. 가상화 적용 후

<javascript />
import { Virtuoso } from 'react-virtuoso'; function Test2() { // 대량의 데이터 (예: 10,000개 항목) const data = Array.from({ length: 10000 }).map((_, index) => ({ id: index, text: `항목 ${index + 1}`, })); return ( <Virtuoso style={{ height: '400px', width: '100%' }} totalCount={data.length} itemContent={(index) => ( <div style={{ padding: '12px', borderBottom: '1px solid #eee' }}> {data[index].text} </div> )} /> ); } export default Test2;

 

실제 화면에 보이는 요소 및 주변 일부만 DOM 노드에 추가된 것을 확인할 수 있습니다.

주의할 점으로는 실제 DOM에 그려지기 전에 스크롤을 빠르게 내리면 흰 화면이 잠깐 보일 수 있어요.
이는 UX를 저하시키기 때문에 스켈레톤 로딩과 같은 fallbackUI를 추가하시길 추천드립니다.
각 가상화 라이브러리 별로 fallbackUI 처리를 위해 지원하는 옵션들이 존재하니 한번 확인해보세요.

 

이점을 정리하면 다음과 같습니다.

  • 실제 DOM 노드 수를 크게 줄여 메모리 사용량 최적화
  • 초기 렌더링 및 스크롤 성능 향상 (렌더링 연산 최소화)
  • 수천, 수만 개의 항목도 부드럽게 스크롤 가능

2.7. ⚡ 애니메이션엔 requestAnimationFrame(rAF)를 활용해보기

requestAnimationFrame(rAF)은 브라우저의 리페인트 주기에 맞춰 애니메이션이나 시각적 업데이트를 실행하는 Javascript API입니다.

브라우저의 렌더링 엔진과 동기화되어 작동하므로, 일반적으로 초당 60 프레임(60 fps, 약 16.7ms)의 속도로 실행됩니다.

 

 

위 예제는 setIntervalrAF를 사용하여 width를 증가시키는 예제입니다.
"시작" 버튼을 눌러보면 rAF는 부드럽게 증가되는 반면, setInterval은 중간중간 끊기는 느낌이 존재합니다.

 

간단하게 차이점을 알아볼게요.

 

특성 setTimeout/setInterval requestAnimationFrame
타이밍 지정된 시간 후 실행 브라우저 리페인트 직전에 실행
프레임 레이트 고정 간격 (불규칙할 수 있음) 화면 주사율에 최적화 (보통 60fps)
백그라운드 동작 계속 실행 (제한될 수 있음) 비활성 탭에서 일시 중지 (리소스 절약)
정확성 브라우저 제한 있음 (최소 4ms) 렌더링 파이프라인과 정확히 동기화

 

rAF는 특히 애니메이션, 스크롤 이벤트 처리, 캔버스 그리기, 데이터 시각화 등 부드러운 시각적 업데이트가 필요한 상황에서 최고의 성능을 발휘해요.

2.8. 📦 React 코드 스플리팅과 지연 로딩 (Lazy Loading)

여러 개의 작은 청크(chunk)로 분할하고, 필요한 시점에 해당 청크만 동적으로 로드하는 최적화 기법입니다.

초기 로딩 시 필요한 코드만 다운로드하고, 나머지는 사용자가 해당 기능이나 페이지를 요청할 때 로드할 수 있어요.

 

React는 React.lazy()Suspense 컴포넌트를 통해 코드 스플리팅과 지연로딩을 쉽게 구현할 수 있도록 지원합니다!

<javascript />
import React, { Suspense, lazy } from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import Navbar from './components/Navbar'; // 페이지 컴포넌트 지연 로딩 const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); const Dashboard = lazy(() => import('./pages/Dashboard')); const UserProfile = lazy(() => import('./pages/UserProfile')); function App() { return ( <Router> <Navbar /> <Suspense fallback={<div className="page-loader">페이지 로딩 중...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/profile/:userId" element={<UserProfile />} /> </Routes> </Suspense> </Router> ); } export default App;

위와 같이 손쉽게 적용할 수 있습니다.

페이지를 기준으로 lazy loading을 적용했어요.

 

리액트 프로젝트를 빌드하고 첫 페이지에 진입했을 때의 예시를 한번 볼까요?

2.8.1. 적용 전

적용 전 빌드 결과
적용 전 첫 페이지 진입

적용 전의 빌드 결과물 중에 js 파일은 하나의 js파일로 모든 코드가 합쳐진 것을 확인할 수 있어요.
이 js파일을 첫 페이지 시에 모두 불러오는 상황입니다.

 

앱 크기가 커질수록 단일 번들의 크기가 켜져서 초기 로딩 시간이 증가하고, 모바일 환경이나 느린 네트워크 환경에서는 특히 더 느릴 수가 있어요.

2.8.2. 적용 후

적용 후 빌드 결과
적용 후 첫 페이지 진입

lazy loading을 적용한 결과 각 페이지, 컴포넌트 별로 js가 분리되었어요!
즉, 정말 필요한 js파일만 요청할 수 있게 되었습니다.

 

이렇게 했을 때 이점들은 뭘까요?

  • FCP(First Contentful Paint), TTI(Time to Interaction) 개선
  • Core Web Vitals 점수 향상으로 SEO에도 👍
  • 특정 기능만 업데이트 시 해당 청크만 다시 다운로드
  • 사용자가 실제로 필요로 하는 코드만 다운로드하여 대역폭 절약

사용자 경험을 크게 향상시킬 수 있습니다!

 

조금 더 깊게 들어가면 조건부 코드 스플리팅, 프리로딩 활용, 청크 크기 최적화까지 고려해 볼 수 있어요.

3. 📕 마치며

프론트엔드에서 당장 적용할 수 있는 최적화 방법들을 살펴보았습니다.

제가 소개한 최적화 기법들은 시작에 불과하다고 생각합니다.

연구에 따르면 페이지 로딩 시간이 3초를 넘어갈 때마다 이탈률이 32% 증가한다고 하네요.🥶

 

여러분의 서비스에서 사용자 경험을 실질적으로 개선하고 비즈니스 목표까지 달성하는 데 도움이 되었으면 좋겠어요.

 

감사합니다.

 

반응형
profile

💻용뇽 개발 노트💻

@용뇽

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!