Skip to main content

목차

  • 블로그 서비스 최적화
  • 올림픽 통계 서비스 최적화
  • 홈페이지 최적화
  • 이미지 갤러리 최적화

머릿말

이번에 처음으로 데보션 오픈랩 스터디에 참여하면서, 이 교재를 가지고 스터디를 진행하였고, 알게된 점과 전반적인 내용을 정리해보려고합니다.

book

1️⃣ 블로그 서비스 최적화

service

첫번째 장은 아티클 리스트와 아티클 상세페이지로 이루어져있는 작은 블로그를 통해 실습을 진행합니다.

라이트하우스 ( lighthouse )

구글에서 개발한, 웹 페이지의 품질을 개선할 수 있는 오픈 소스 형태의 자동화 도구
현업에서도 제일 많이 쓰이고 프론트엔드 개발자라면 자주 접하는 라이트하우스를 통해 부족한 최적화를 진행합니다.

lighthouse

고의적으로 최적화가 필요하도록 설계된 블로그이다 보니 아래와 같은 문제들이 있었습니다.

Properly size images

실제로 가져오는 이미지와 리스트에서 사용되는 이미지 사이즈가 많이 상이하기 때문에 발생합니다. 이 프로젝트에서는 12001200px 사이즈의 사진들을 전부 240240px로 고정이 되어있는 이미지로 사용되고 있습니다.

제일 먼저 떠오르는건 이미지 사이즈 자체를 맞게 수정해서 스토어해놓는 것이겠지만, 대부분의 이미지는 CDN을 통해 제공되기 때문에 그건 힘들 수 있습니다. 여기 실습에서는 unsplash라는 이미지 CDN 무료로 제공해주는 서비스를 통해 서빙되고 있습니다. 그리고 대부분의 이미지 CDN이 그렇듯 사이즈가 조절이 가능한 파라미터를 지원하기 때문에 이 예시에서는 아래와 같이 이미지를 가져옴으로써 해결했습니다.

https://images.unsplash.com/photo-345?q=80&w=240&h=240

TMI: 저희 회사에서는 imageKit이라는 서비스를 사용해서 최적화를 진행하고 있습니다.

info

이미지 CDN이란 ?
전 세계에 분산된 서버를 통해 사용자가 가장 가까운 서버에서 이미지를 받아 빠르고 효율적으로 로딩되도록 돕는 네트워크

병목 코드

Performance tab을 켜서 확인해보면 단순히 렌더링임에도 불구하고 1.4초 이상 걸리는 구간을 확인할 수 있습니다. 좀 더 자세히보면 해당 렌더링때 발생하는 함수이름을 알 수 있는데, 그거를 단서로 해당 함수의 로직을 보면 의도적으로 특수문자를 제거하는 함수가 비효율적으로 작성되어 있습니다.

자세히 읽어보면 removeSpecialCharacter 함수가 계속해서 반복된는데. 이는 일부러 굉장히 잘못된 로직으로 구성된 함수입니다.

코드를 리팩토링 함으로써 성능을 비약적으로 개선하는 그 과정을 체험할 수 있었습니다.

최적화 전최적화 후
최적화 전최적화 후

코드 분리 && 지연 로딩

처음 렌더링할때, 처음에 FCP 걸리는 걸 확인 할 수 있습니다. 로컬 서버에 단순한 서비스임에도 불구하구요. 라이트하우스에서 위와 같은 경고가 있는데 트레이싱해서 그 청크파일을 확인해보면 한 청크파일이 4MB가 넘는걸 알 수 있습니다.

이때 webpack-bundle-analyzer 패키지를 이용해서 번들파일의 구성을 쉽게 확인 가능합니다.

번들

실습코드는 lazy loading이나 코드 스플릿 같은게 하나도 적용이 안되어 있기때문에 위처럼 하나의 청크로 다 묶여있는 것을 볼 수 있습니다. 그렇기 때문에 분리할 수 있는 코드는 하나의 다른 모듈로 분류하여 컴포넌트화를 진행하고, 모달 혹은 라우트에 사용되는 컴포넌트 같은 경우는 동적으로 가져올 수 있으므로, react에서 지원하는 lazy, Suspense를 사용해서 청크를 여러개로 나누는 작업을 했습니다.

간단하게 코드를 추상화하면 아래와 같습니다.

// 전
import React from "react";
import LargeComponent from "./LargeComponent";

function App() {
return (
<div>
<h1>My App</h1>
<LargeComponent />
</div>
);
}

export default App;
// 후
import React, { Suspense } from "react";

const LazyLargeComponent = React.lazy(() => import("./LargeComponent"));

function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<div>Loading...</div>}>
<LazyLargeComponent />
</Suspense>
</div>
);
}

export default App;

enable text compression

HTML, CSS, Javascript와 같은 리소스는 전부 텍스트 압축이 가능합니다.

네트워크 응답 헤더를 확인해보면 Content-Encodinggzip이 들어가 있는데 메인 번들 파일의 응답헤더를 보면 Content-Encoding이 비어져 있는 것을 알 수 있습니다.

이또한 이 프로젝트에서는 disabled 처리해 놓았던걸 풀어서 해결 할 수 있었습니다.

info

텍스트 압축이란 ?
HTML, CSS, JavaScript 등 텍스트 기반 파일의 크기를 줄여 웹 페이지 로딩 속도를 높이는 최적화 기법입니다.

2️⃣ 올림픽 통계 서비스 최적화

하이라이트 모달설문 조사
lecture-1lecture-2

두번째 장은 리우올림픽과 런던올림픽의 사진을 비교하고 하단에는 그 설문조사 결과를 보여줍니다.

애니메이션 최적화

설문조사 부분을 부분을 클릭하게되면 Jank 현상이 발생합니다. 코드를 보면 아래와 같습니다.

info

Jank 현상이란 ?
사이트나 앱이 주사율에 맞추지 못하고 더듬거리며 요동치거나 잠시 멈춘다는 것을 사용자가 보는 것

const BarGraph = styled.div`
position: absolute;
left: 0;
top: 0;
width: ${({ width }) => width}%;
transition: width 1.5s ease;
height: 100%;
background: ${({ isSelected }) =>
isSelected ? "rgba(126, 198, 81, 0.7)" : "rgb(198, 198, 198)"};
z-index: 1;
`;

width 속성을 transition 속성을 통해서 percent prop에 따라 가로 길이를 조정하면서 올라가는 것을 구현되어 있습니다. 하지만 이는 별로 좋은 접근이 아니라는 것을 책에서 알려주는데요.

rendering process

먼저 witdh를 변경하게 되면 브라우저는

  1. 해당 요소의 가로, 세로를 다시 계산하여 화면을 새로 그릴 것입니다.
  2. CSSSOM을 새로 만듭니다.
  3. 다시 렌더 트리를 만들게 됩니다.
  4. 커밋된 화면 구성에 알맞게 색을 칠하고 분할된 레이어를 하나로 합성 (컴포지트)합니다.

이 과정이 리플로우입니다. 즉 width를 바꾸게 되면 위에서 보여주는 렌더링 사이클을 전부 돌게됩니다. 리플로우와 리페인트를 일으키는 요인들은 정말 알면알수록 많은데 잘 잘 정리된 문서가 있어서 첨부합니다.

const BarGraph = styled.div`
position: absolute;
left: 0;
top: 0;
width: 100%;
transform: scaleX(${({ width }) => width / 100});
transform-origin: center left;
transition: transform 1.5s ease;
height: 100%;
background: ${({ isSelected }) =>
isSelected ? "rgba(126, 198, 81, 0.7)" : "rgb(198, 198, 198)"};
z-index: 1;
`;

교재에서는 애니메이션 부분을 width -> transform에서 scale을 통해 GPU에 위임하여 효율적으로 처리하도록 가이드했습니다.

Performance를 통해 확인하면 Layout과 paint가 사라진것을 확인이 가능했습니다.

Before
before
After
after

컴포넌트 지연로딩과 사전로딩

프로젝트 코드를 살펴보면 이미지 모달 코드가 하나의 파일에 작성된 것을 알 수 있습니다.

사실 이 부분은 Nexjts/dynamic 이나 React/lazy import를 통해서 간단하게 코드 분리가 가능합니다.

코드 분리 전코드 분리 후
beforeafter

코드분리를 적용하면 번들이 작아지면서, 초기 로딩이나 자바스크립트 실행 타이밍이 빨라져서 화면이 더 빨리 표시되는 그런 장점이 있습니다. 하지만 이렇게 분리를 하게 되면 모달 컴포넌트를 로드하는데 약간의 지연이 있는 건 당연한 수순입니다.

dynamic loading

교재에서 제시한 사전로딩은 두가지가 있습니다.

  1. onMouseEnter을 통한 사전로딩
  2. mount가 끝났을때 useEffect를 통한 사전로딩
const LazyImageModal = lazy(() => import("./components/ImageModal"));

function App() {
const [showModal, setShowModal] = useState(false);

// 1. onMouseEnter를 통한 사전로딩
const handleMouseEnter = () => {
const component = import("./components/ImageModal");
};

// 2. Mount가 끝났을때 사전로딩
useEffect(() => {
const component = import("./components/ImageModal");
}, []);

return (
<div className="App">
<ButtonModal
onClick={() => {
setShowModal(true);
}}
onMouseEnter={handleMouseEnter}
>
올림픽 사진 보기
</ButtonModal>
</div>
);
}

이미지 사전로딩

실습에서 사용되는 이미지 모달을 띄울때, 이미지를 가져오는 동안 레이아웃이 깨지게 됩니다.

이미지모달 오픈 직전이미지모달 이미지 로드 후
오픈 직전이미지 로드 후

컴포넌트들 가져올때 이미지를 사전 로딩하면 이런 현상을 없앨수가 있겠죠. 이미지를 보면 모달 컴포넌트와 이미지를 사전 로드를 하는 것을 볼 수 있습니다.

const handleMouseEnter = () => {
const component = import("./components/ImageModal");

const img = new Image();
img.src = `<image url>`;
};

이미지 사전 로딩

이는 자바스크립트로 이미지 사전로딩하는 방법중에 하나고, 아래와 같은 방법들이 있습니다.

3️⃣ 홈페이지 최적화

project 3

세번째 장은 롱보드를 소개하는 홈페이지입니다.

이미지 지연 로딩

프로젝트를 시작하고 네트워크 탭을 확인해보면 뷰포트에 없는 이미지가 전부 다운되는 것을 확인할 수 있습니다.

교재에서는 이를 Intersection Observer API를 통해서 개선했습니다. 이미지 관련해서는 항상 언급되는 API죠.

그리고 webp로 원본 이미지를 최적화하면서, 기존 이미지는 fallback으로 webp를 우선적으로 로드하게 개선하였다.

entry.isIntersecting을 통해 이미지가 화면에 보일 때 data-src 속성의 실제 URL을 src로 설정합니다.

그리고 img 태그에 loading="lazy" 라는 어트리뷰트를 사용하는 것도 한 방법일것이다.

function MainPage(props) {
const imgRef = useRef(null);

useEffect(() => {
const options = {};
const callback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const sourceEl = entry.target.previousSibling;
sourceEl.srcset = sourceEl.dataset.srcset;
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
};

const observer = new IntersectionObserver(callback, options);
observer.observe(imgRef.current);

return () => observer.disconnect();
}, []);
return (
<div className="MainPage -mt-16">
<TwoColumns
bgColor={"#fafafa"}
columns={[
<picture>
<source data-srcset={"image_webp_url"} type="image/webp" />
<img data-src={"image_original_url"} ref={imgRef} alt="image" />
</picture>,
]}
mobileReverse={true}
/>
</div>
);
}

비디오 최적화

교재에서는 비디오에 대해서 webm으로 파일자체를 최적화하는 방법을 제시해줍니다. 그리고 위 이미지에서 진행됬던 것처럼

webm을 우선적으로 로드하고, mp4를 fallback으로 로드하게끔 수정했습니다.

<video autoplay loop muted>
<source src={video_webm} type="video/webm" />
<source src={video_original} type="video/mp4" />
</video>

하지만 영상은 최적화하게 되면 저화질이 되기때문에 교재에서는 팁으로 패턴과 필터를 적용해서 사용자가 그를 인지하게 못하게하는 tip을 언급합니다.

<video autoplay loop muted style={{ filter: blue(10px) }}>
<source src={video_webm} type="video/webm" />
<source src={video_original} type="video/mp4" />
</video>

비디오 요소에 blur를 주면서 저화질의 단점을 조금 완화시킬 수 있겠네요.

반드시 고화질 영상이 사용되야할 상황이 있습니다. 예를 들면 랜딩페이지같이 고화질 영상을 고수해야할때, HLS ( HTTP Live Streaming ) 방식을 통해 개선했던 경험이 있습니다.

영상을 mp4 -> hls (m3u8, ts)으로 변환하는 방법은 많지만 저는 PC에 ffmpeg라는 오픈소스 소프트웨어로 CLI를 통해 변환했습니다.

info

ffmpeg란 ?
멀티미디어 데이터를 처리하기 위한 오픈 소스 소프트웨어로, 다양한 비디오 및 오디오 형식의 변환, 스트리밍, 편집, 인코딩, 디코딩을 가능하게 합니다.

  1. https://ffmpeg.org/download.html 에서 ffmpeg를 PC에 설치를 해줍니다.

  2. 영상 경로로 들어가 아래 커맨드를 실행시켜줍니다.

ffmpeg -i INPUT.mp4 -profile:v baseline -level 3.0 -s 1920x1080 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls OUTPUT.m3u8
옵션설명
-i INPUT.mp4입력 파일을 지정합니다.
-profile:v baseline -level 3.0호환성을 위해 H.264 프로파일을 baseline으로 설정하고, 레벨을 3.0으로 설정합니다.
-s 1920x1080출력 해상도를 1920x1080으로 설정합니다.
-start_number 0TS 파일의 시작 번호를 0으로 지정합니다.
-hls_time 10각 TS 파일이 10초의 비디오를 포함하도록 설정합니다.
-hls_list_size 0모든 세그먼트가 재생목록에 포함되도록 하며, 오래된 세그먼트를 제거하지 않습니다.
-f hls포맷을 HLS로 지정합니다.
  1. 위 명령어를 실행하면 m3u8 파일 하나, 다수의 ts 확장자파일이 변환되어 나오게 됩니다.

m3u8

  1. 파일들을 원하는 경로에 넣고, 클라이언트에서 가져와줍니다.
<ReactPlayer
url={`${IMAGE_ROOT}/banners/video/InfoCloud.m3u8`}
playing
muted
width={getCalculateSize.width}
height={getCalculateSize.height}
controls
loop
/>
결과물
1
2

이렇게하면 동영상을 작은 세그먼트로 분할하여 HTTP를 통해 전송합니다. ( 마치 유튜브처럼 )


폰트 최적화

폰트 적용전폰트 적용후
font-beforefont-after

이 프로젝트 뿐만아니라 간혹 폰트가 기본 폰트에서 딜레이가 어느정도 지난후에 폰트가 적용되는 상황을 보셨을 수 있습니다.

웹 폰트가 동작하는 방식
default-behavior

글꼴의 지연 로드에는 중요한 함축이 숨겨져 있어 텍스트 렌더링이 지연될 수 있습니다. 브라우저는 텍스트를 렌더링하는 데 필요한 글꼴 리소스를 인식하기 전에 DOM 및 CSSOM 트리에 종속된 렌더링 트리를 생성해야 합니다. 따라서 글꼴 요청은 다른 중요한 리소스 후 훨씬 지연되고 리소스를 가져올 때까지 브라우저가 텍스트를 렌더링하지 못할 수 있습니다. by web.dev

이때 브라우저마다 폰트가 준비되지 않았을때 처리하는 방식이 다릅니다.

구분FOUT (Flash of Unstyled Text)FOIT (Flash of Invisible Text)
브라우저EdgeChrome, Safari, Firefox 등
폰트 다운로드 전텍스트가 표시됨텍스트가 보이지 않음
폰트 다운로드 후폰트가 적용된 텍스트가 표시됨폰트가 적용된 텍스트가 표시됨
사용 권장 상황중요한 내용을 전달하는 텍스트꼭 전달할 필요가 없는 보조적인 텍스트

교재에서는 다음과 같이 진행했습니다.

  • font-display 속성에서 원하는 폰트 제어 방식 적용
  • 폰트 다운로드 완료 후, 애니메이션을 통해 자연스럽게 렌더링
  • 폰트 파일 크기 최적화 및 포맷 변경

font-display 적용

/* 웹에서 사용할 커스텀 폰트를 정의합니다. */
@font-face {
/* 폰트의 이름을 정의합니다. 이 이름은 이후 font-family에서 사용할 수 있습니다. */
font-family: BMYEONGSUNG;

/* 폰트 파일의 경로와 포맷을 지정합니다. */
src: url('./assets/fonts/BMYEONGS.woff2') format('woff2');

/* font-display 속성을 통해 폰트 로딩 방식을 설정합니다. */
font-display: block;
/*
font-display: block은 폰트가 로드될 때까지 텍스트를 숨깁니다.
폰트가 로드되면 숨겨진 텍스트가 폰트와 함께 표시됩니다.
이렇게 하면 플래시 효과 없이 폰트가 로드된 후 전체 텍스트가 한 번에 나타납니다.
*/
}
info

font-display 란? font-face가 표시되는 방법을 결정하는데, 적절히 활용하면 폰트 로딩 문제로 인한 사용자 경험 악화를 방지할 수 있습니다

font-display에 몇가지 속성들이 있는데 다음과 같습니다.

  1. auto

    • 동작: 브라우저 기본 설정을 따름 (예측 불가, 브라우저마다 동작 다름).
    • 사용 예시: 특별한 제어가 필요 없을 때, 기본 동작을 그대로 따르고 싶을 때.
  2. block

    • 동작: 최대 3초 동안 텍스트 숨김 → 로드 후 폰트 교체 (FOIT 가능성).
    • 사용 예시: 브랜드 아이덴티티가 중요하고, 로드 전까지 텍스트가 보이지 않아야 할 때.
  3. swap

    • 동작: 기본 폰트를 즉시 렌더링 → 로드 후 웹폰트 교체 (FOUT 발생 가능).
    • 사용 예시: 사용자 경험을 중시하고, 텍스트가 빠르게 렌더링되어야 할 때.
  4. fallback

    • 동작: 최대 100ms 숨김 → 기본 폰트로 렌더링 → 로드 후 교체(또는 대체 폰트 유지).
    • 사용 예시: 빠른 초기 렌더링이 중요하지만, 폰트 교체 지연을 최소화하고 싶을 때.
  5. optional

    • 동작: 최대 100ms 숨김 → 기본 폰트 렌더링 → 네트워크 상황에 따라 교체 여부 결정.
    • 사용 예시: 네트워크 성능이 중요한 상황에서 폰트 로드 실패 시 기본 폰트로 유지해도 되는 경우.

폰트 다운로드 완료 후, 애니메이션을 통해 자연스럽게 렌더링

교재에서는 애니메이션이 기본 폰트에서 다운로드 완료된 폰트로 넘어가는 swap 동작이 UI 적으로 좋지 않고 아무래도 메인화면 중앙에 큰 글자로 표시되기 때문에, block으로 변경해서 폰트 다운로드가 완료되면 애니메이션을 통해 자연스럽게 렌더링되도록 하였습니다.

폰트가 다운로드 되는 시점을 알기위해서는 일반적으로는 Broswer Native API를 통해서 구현할 수 있겠지만 fontfaceobserver라는 유명한 패키지를 통해서 다음과 같이 구현이 된것을 확인할 수 있었습니다.

import FontFaceObserver from "fontfaceobserver";

function Background() {
// 폰트 로드 상태를 관리하기 위한 상태값 선언 (초기값: false)
const [isFontLoaded, setIsFontLoaded] = useState(false);

useEffect(() => {
// 'FONTNAME'이라는 폰트를 감시하기 위한 FontFaceObserver 객체 생성
const font = new FontFaceObserver("FONTNAME");

// 폰트 로드 시도를 시작, 최대 20초(20000ms)까지 기다림
font
.load(null, 20000)
.then(() => {
// 폰트가 성공적으로 로드되었을 때 실행
console.log("FONTNAME has loaded");
setIsFontLoaded(true); // 폰트 로드 상태를 true로 업데이트
})
.catch(() => {
// 폰트 로드에 실패했을 때 실행
console.error("FONTNAME failed to load");
});
}, []);

return (
<div className="container">
{/* 폰트가 로드되었는지 여부에 따라 스타일 변경 */}
<div
className="font-wrapper"
style={{
opacity: isFontLoaded ? 1 : 0, // 폰트가 로드되었으면 불투명(1), 로드되지 않았으면 투명(0)
transition: "opacity 0.3s ease", // 투명도 변화 시 0.3초 동안 부드럽게 전환
}}
>
{/* 텍스트 콘텐츠 */}
<span className="text content">Your content here</span>
</div>
</div>
);
}

export default Background;

폰트 파일 크기 최적화 및 포맷 변경

이미지에서도 Webp, avif 확장자가 후발주자로 웹에서의 이미지 최적화가 되어있듯이 font도 후발주자로 나온 WOFF, WOFF2 확장자가 있습니다.

[transfonter](https://transfonter.org/)가 대표적인 폰트 변환 사이트입니다.

여기서 기존의 ttf -> woff2로 변환을 해줍니다. 그렇게 거의 절반으로 크기가 줄어든 것을 확인할 수 있습니다.

extendsion opt

더욱 극단적으로 간다면 사용하는 언어, 더 나아가서 사용되는 문자만 가져오도록 서브넷 폰트로 만들 수 있습니다.

subset

용량과 다운로드 시간을 보시면 많이 줄어드신 것을 확인 할 수 있습니다.

4️⃣ 이미지 갤러리 최적화

메인 화면이미지 클릭
mainimage-click

네번째 장은 이미지 간단한 이미지 갤러리입니다. 헤더를 통해서 이미지들을 필터링합니다. 이미지를 클릭하면 평균 픽셀값을 계산하면 오버레이로 띄어줍니다.

CLS (Cumulative Layout Shift ) 발생요인

CLS 발생퍼모먼스 측정
Screen-Recording2024-10-26at5-44-04-AM-ezgif-com-video-to-gif-converterimage

이번장에서는 위와 같이 lighthouse를 통해서 CLS가 발생하는 것을 명확히 알 수 있고, performance Tab을 통해서도 확인 할 수 있습니다.

CLS는 아래와 같은 요인들로 발생하는데요, 이번 장에서는 이미지에 치수가 들어가 있지않아 발생하는 경우에 대해 다룹니다.

  • 광고, 임베드와 같은 동적 컨텐츠
  • 애니메이션
  • 웹 폰트
  • 치수가 없는 이미지

치수가 없는 이미지에 대한 대응

치수가 정확하게 한 사이즈로만 렌더링이 된다면, 문제는 간단하게 해결될 것입니다.

<img src="/image/url.png" alt="placeholder" width="300px" height="300px" />

이렇게 먼저 widthheight 속성을 추가하면 브라우저가 이미지를 가져오기전에 공간을 먼저 할당해 놓기때문에, Reflow가 최소화될 것입니다.

하지만 반응형 웹 디자인의 도입으로 widthheight를 생략하고 아래와 CSS로 이미지 크기를 제어하고자 하기 시작합니다.

img {
width: 100%;
height: auto;
}

하지만 이미지 크기가 지정되지 않으므로 브라우저에서 다운로드를 시작하고 크기를 확인할 때까지는 공간을 할당할 수 없게됩니다. 이런 상황때문에 비율이라는 개념이 들어오게 되는데, 크기 중 하나를 알면 브라우저가 관련 영역에 충분한 공간을 잡아 줄 수 있습니다. 둘중에 하나만이라도 특정값을 명시해놓으면 브라우저는 이를 기반으로 이미지의 비율을 추가합니다.

<!-- 16:9 비율을 위한 width, height 설정 -->
<img src="devocean.png" width="640" height="360" alt="Devocean open lab" />

하지만 저희는 자동으로 계산되는 것보다 디자인이 나온대로 대응을 해주어야합니다. 저희가 원하는 비율로 나오게끔 해야하는 거죠. 대부분의 경우 반응형으로 디자인이 설계가 되어있고, 이미지의 비율이 그와 맞지않을때가 많습니다.

교재에서는 두가지 방식을 제안합니다.

Padding-hack 기법Aspect Ratio 프로퍼티 사용
padding-hackaspecct-ratio
오래된 브라우저에서도 호환성이 좋고, 원하는 비율을 직접 설정가능합니다. 하지만 유지보수가 어렵고, 코드가 길어질 수 있습니다.코드가 간결하고 직관적이며, 요소가 비율을 유지하면서 자동으로 조정됩니다. 비교적 최신 CSS라서 오래된 브라우저는 지원이 안될 수도 있기 때문에 폴리필을 고려해야합니다.

각각의 트레이드오프가 있는 것 같아서, 개인적으로 좋아하는 Radix UI에서는 어떻게 적용하고 있는지 궁금해서 찾아봤습니다. Aspect Ratio라는 래퍼 컴포넌트에서는 Padding-hack 기법을 통해서 구현되있는 것을 확인할 수 있었어요. 아무래도 호환성 때문인 것 같습니다.

const AspectRatio = (props, forwardedRef) => {
const { ratio = 1 / 1, style, ...aspectRatioProps } = props;
return (
<div
style={{
position: "relative",
width: "100%",
paddingBottom: `${100 / ratio}%`,
}}
>
<Primitive.div
{...aspectRatioProps}
ref={forwardedRef}
style={{
...style,
position: "absolute",
top: 0,
right: 0,
bottom: 0,
left: 0,
}}
/>
</div>
);
};

⚛️ React Dev Tool 톺아보기

dev-tool

앞장에서 사용한 network, performance, lighthouse 특히 performance는 써본적 없을 수 있지만 프론트엔드 개발자라면 써본적이있는 React Developer Tools를 이번 챕터에서 다룹니다.

하지만 safari나 지원하지 않는 브라우저에서는 패키지를 다운받아 사용할 수 있습니다. ( 특히 React native )

다운로드 및 사용법

# yarn
yarn global add react-devtools

# npm
npm install -g react-devtools

# 실행
react-devtools

global로 설치한다음 react-devtools 커맨드를 입력해서 실행합니다.

usage2

위와 같이 실행이되고, React Native같은 경우는 이 가이드이 가이드에 따라서 진행하시면 될 것 같습니다. 그 외, safari 환경에서 실행해야하는 경우는 위에서 요구하는 script 태그를 본문에 넣어주면 해당 응용 애플리케이션에서 실행이 됩니다.

<!DOCTYPE html>
<html lang="en">
<head>
<script src="http://localhost:8097"></script>
<title>React App</title>
<body></body>
</html>

⚛️ Components

  • 컴포넌트 계층 구조를 시각적으로 확인
  • 컴포넌트 상태와 프롭스 조회 및 조작
  • 컴포넌트 훅 검사 가능 ex) useState, useMemo
function App() {
return (
<ErrorBoundary fallback={'Error...'}>
<Suspense fallback={'Loading...'}>
<Child>
</Suspense>
</ErrorBoundary>
)
}

컴포넌트 탭에서가면 우측상단에 디버그 아이콘이 최대 5개까지 확장되는 것을 보실 수 있습니다. 모르시고 넘어갈 수 있어서 각각 어떤 버튼인지 보겠습니다.

위와 같은 코드가 있다고 가정했을 때 총 버튼이 5개가 생깁니다.

Components Tab
debugging
1️⃣ Error Toggle: Error 상태를 활성화하여 화면에 "Error..." 메시지를 표시합니다.
2️⃣ Suspense Toggle: Suspense를 활성화하여 화면에 "Loading..." 메시지를 표시합니다.
3️⃣ Jump to Elements: 선택한 컴포넌트를 개발자 도구의 Elements 탭으로 이동합니다.
4️⃣ Log Component Info: 현재 컴포넌트의 Props, Hooks, Nodes를 console에 출력합니다.
5️⃣ View Source Code: 소스맵을 통해 컴포넌트의 원본 코드로 바로 접근합니다.

그리고 최근에는 서버컴포넌트도 지원이 된다고 합니다 ~ !

server

⚛️ Profiler

  • 컴포넌트 렌더링 시간 측정 및 시각화
  • Flame Graph와 Ranked Chart를 통해 렌더링 비용 확인
  • Timeline을 통해 렌더링 이벤트와 CPU 사용량 분석
  • 렌더링 이벤트의 원인 확인 (props, state 변경 등)
  • 불필요한 리렌더링 감지 (컴포넌트의 과도한 재렌더링 파악)

profiler

간단하게 4개만 정리해보았습니다.

1️⃣ FlameGraph2️⃣ Ranked
flamGraphranked
컴포넌트가 렌더링에 걸린 시간을 시각적으로 표현하여, 어떤 컴포넌트가 가장 많은 렌더링 시간을 차지하는지 쉽게 확인할 수 있습니다. 컴포넌트를 클릭하면 오른쪽 패널에 해당 구성 요소의 props및 state이 커밋 시점의 정보가 표시됩니다.순위가 매겨진 차트 뷰는 단일 커밋을 나타냅니다. 차트의 각 막대는 React 구성 요소(예: App, Nav)를 나타냅니다. 차트는 렌더링하는 데 가장 오래 걸린 구성 요소가 맨 위에 오도록 정렬됩니다.
3️⃣ Timeline4️⃣ Commit Page
timelinecommit page
컴포넌트의 렌더링 이벤트와 그에 소요된 시간을 시간순으로 시각화하여 보여줍니다. 이를 통해 특정 시점에서 어떤 작업이 이루어졌는지, 또 어떤 컴포넌트가 성능 병목을 일으켰는지 파악할 수 있습니다.React는 렌더커밋 두 단계로 동작합니다. 커밋이 일어나 시점을 기준으로 각 막대는 하나의 커밋을 의미합니다.
info

렌더 단계란 ?
DOM에 필요한 변경 사항을 결정하고, render 호출 및 이전 렌더와 비교합니다.

info

커밋 단계란 ?
렌더 단계에서 결정된 변경 사항을 적용하며, 이 과정에서 componentDid Mount, componentDidUpdate 라이프사이클 메서드가 호출됩니다.

Profiler Component ( 번외 )

프로그래밍 방식으로도 React 트리의 렌더링 성능을 측정할 수 있습니다.

import React, { Profiler } from "react";

function onRenderCallback(
id, // Profiler의 "id" prop에 전달된 값
phase, // "mount" (처음 렌더링) 또는 "update" (재렌더링)
actualDuration, // 해당 업데이트에 소요된 렌더링 시간
baseDuration, // 메모이제이션 없이 렌더링하는 데 걸리는 예상 시간
startTime, // 렌더링이 시작된 시점
commitTime, // 렌더링이 커밋된 시점
interactions, // 렌더링과 관련된 상호작용들
) {
console.log({
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
interactions,
});
}

function MyComponent() {
return (
<Profiler id="test-app" onRender={onRenderCallback}>
<div>프로파일링할 컴포넌트 내용</div>
</Profiler>
);
}
id: test-app, phase: update, actualDuration: 5.30ms, baseDuration: 8.00ms, startTime: 8482.50ms, commitTime: 8487.90ms

후기

막상 현업에서는 백오피스 업무를 많이 다루다보니, 최적화에 관련되서 공부할 기회가 없었습니다. 우연히 이 교재를 골라서 DevOcean에서 스터디를 진행하게 되었는데, 너무 작아서 지나쳤던 부분이나, Nextjs에서는 추상화되어서 볼 수 없는 것들을 다시 한번 볼 수 없었습니다. 분명 현업하면서 지나쳤거나 알았던 것을 다시 한번 좀 더 원론적으로 볼 수 있었던 책이였고, 아쉬웠던 점은 자료 자체가 조금 버전이 오래되었지만 핵심은 그게 아니기 때문에 괜찮은 것 같습니다.

회사 코드 베이스에도, 한번 font나 병목적인 코드가 있는지 한번 쯤 찾아보고 싶습니다.

주변 지인한테 담 기수 넣어보라구 해야겠습니다 ! 읽어주셔서 감사합니다.

Reference