이 장에서 학습할 최적화 기법
- CSS 애니메이션 최적화
- 브라우저가 어떻게 화면을 그리는지 학습하고, 이를 바탕으로 해결책을 찾아 적용
- 컴포넌트 지연 로딩
- 1장에서 코드 분할 기법을 배워 이 기법을 이용해 분할된 코드를 필요한 시점에 로드되도록 적용. 이번에도 비슷하지만 페이지 코드 자체를 분할하는 대신 단일 컴포넌트를 분할하여 컴포넌트가 쓰이는 순간에 불러오도록 변경
- 컴포넌트 사전 로딩
- 컴포넌트를 분할하여 지연 로딩을 적용하면, 첫 화면 진입 시 분할된 코드 중 당장 필요한 코드만 다운로드 하기때문에 첫 화면을 더 빠르게 그릴 수 있다. 하지만 서비스 이용 과정에서 분할된 컴포넌트를 사용하려고 할 때, 다운로드 되어 있지 않은 코드를 추가로 다운로드하는 시간만큼 서비스 이용에 지연이 생긴다. 이를 위해 첫 화면에는 다운로드 하지 않지만, 해당 코드가 필요한 시점보다는 먼저 로드하여 지연없이 사용하는 컴포넌트 사전 로딩에 대해 알아보자.
- 이미지 사전 로딩
- 이미지도 마찬가지로 필요한 시점에 로드하면 이미지가 로드되는 시간만큼 기다려야하는데 그래서 이미지도 시점보다 먼저 다운로드해 두고, 필요할 때 바로 이미지가 보일 수 있는 사전 로딩 기법에 대해 알아보자.
애니메이션 최적화
애니메이션의 끊김 현상을 쟁크(jank)라고 한다. 이 현상에 대해 알기 위해선 브라우저에서 애니메이션이 어떻게 동작하는지, 그리고 브라우저는 어떤 과정으로 화면을 그리는지 이해할 필요가 있다.
애니메이션의 원리는 여러 장의 이미지를 빠르게 전환하여 우리 눈에 잔상을 남기고, 그로 인해 연속된 이미지가 움직이는 것처럼 느껴지게 하는 것이다. 일반적으로 디스플레이의 주사율은 60Hz인데, 이는 1초에 60장의 정지된 화면을 빠르게 보여준다는 의미이다. 브라우저도 이에 맞춰 최대 60FPS(Frames Per Second)로 1초에 60장의 화면ㅇ르 그린다.
그렇다면 쟁크가 발생한 이유는 정상적으로 60FPS로 화면을 그리지 못했기 때문인데, 그래서 40장, 20장의 화면을 그린 화면은 애니메이션이 끊기는 느낌을 준다.
그렇다면 왜 브라우저는 60FPS를 제대로 그리지 못한 것일까?
이를 위해선 브라우저 렌더링과정을 알아야한다.
DOM + CCSOM ⇒ 렌더 트리 ⇒ 레이아웃 ⇒ 페인트 ⇒ 컴포지트
브라우저는 위와같은 과정을 거쳐 화면을 그린다. 이러한 과정을 주요 렌더링 경로(Critical Rendering Path) 또는 픽셀 파이프라인(Pixel Pipeline) 이라고 한다.
- DOM + CSSOMCSS도 마찬가지로 HTML과 비슷한 파싱 과정을 거쳐 브라우저가 이해할 수 있는 형태로 CSSOM(CSS Obejct Model)이라는 트리 구조가 생성된다.
- 가장 처음에는 HTML 파일과 CSS 등 화면을 그리는 데 필요한 리소스를 다운로드한다. 다운로드한 HTML은 브라우저가 이해할 수 있는 형태로 변환하는 파싱 과정을 거친다. 그렇게 해서 요소 간의 관계가 트리 구조로 표현되어 있는 DOM(Document Object Model)을 만든다.
- 렌더트리
- 렌더 트리는 DOM과 CSSOM의 결합으로 생성된다. 이 렌더 트리는 화면에 표시되는 각 요소의 레이아웃을 계산하는데 사용된다. 달리 말하면, display: none으로 설정되어 화면에 표시되지 않는 요소는 렌더 트리에 포함되지 않는다.
- 레이아웃
- 렌더트리가 완성 후, 레이아웃 단계로 넘어가는데 이 레이아웃 단계에서는 화면 구성 요소의 위치나 크기를 계산하고, 해당 위치에 요소를 배치하는 작업을 한다.
- 페인트
- 페인트 단계는 화면에 배치된 요소에 색을 채워 넣는 작업을 한다. 배경색을 채워넣거나 글자 색을 결정하거나 테두리 색을 변경한다. 이때 브라우저는 효율적인 페인트 과정을 위해 구성 요소를 여러 개의 레이어로 나눠서 작업한다.
- 컴포지트(composite)
- 각 레이어를 합성하는 작업을 한다. 브라우저는 화면을 그릴 때 여러 개의 레이어로 화면을 쪼개서 그린다. 그런 다음 마지막에 레이어를 하나로 합성하는데 그 단계가 컴포지트 단계이다
성능 탭에서 확인할 수 있다.
화면이 전부 그려진 후 설문 결과에서의 애니메이션처럼 일부 요소의 스타일을 변경하거나 추가, 제거하면 렌더링 경로에서 거친 과정을 다시 한번 실행하면서 새로운 화면을 그리는데, 이것을 리플로우, 리페인트라고 한다.
- 리플로우와 리페인트이처럼 리플로우는 주요 렌더링 경로의 모든 단계를 모두 재 실행한다. 그렇기 때문에 브라우저 리소스를 많이 사용한다.
- 이번에는 리페인트 과정을 알아보면 가로, 세로와 같은 레이아웃 관련 속성이 아니라 글자색, 배경색, 색상 관련 속성이 변경되었을 경우 CSSOM이 새로 생성되고 렌더트리도 새로 만들어지게 된다. 하지만 레이아웃 단계는 실행되지 않고 건너 뛰게 된다. 그렇기에 리플로우 과정보다 조금 더 빠르다. 하지만 리페인트 역시 거의 모든 단계를 거치기 때문에 리소스를 꽤 많이 잡아먹는다.
- 해당 lecture의 예시를 보면 bar 클릭을 통해 스타일이 변경되어 CSSOM을 새로 만들어야 한다. 이후 변경된 CSSOM을 이용하여 새로운 렌더트리를 만든다. 그리고 요소의 가로와 새로를 변경했으니 레이아웃 단계에서 당연히 요소의 크기와 위치를 다시 고려해야 한다. 그 다음 변경된 화면 구성에 알맞게 색을 칠하고 분할된 레이어를 하나로 합성해야 한다. 이것을 리플로우라고 한다.
정리해보면, 요소의 상태 변화가 일어나면 리플로우나 리페인트 과정을 거치는데, 이는 브라우저의 리소르를 많이 잡아먹기 때문에 결국 화면을 새로 그리는 것이 느릴 수 밖에 없다는 뜻이다. 하지만 이를 피하기 위한 방법이 있는데 바로 transform과 opacity와 같은 속성을 사용하는 방법이다. 이런 속성을 사용하면 해당 요소를 별도의 레이어로 분리하고 작업을 GPU에 위임하여 처리함으로써 레이아웃 단계와 페인트 단계를 건너 뛸 수 있다. 이를 하드웨어 가속이라고 한다.
하드웨어 가속(GPU 가속)
하드웨어 가속은 CPU에서 처리해야할 일을 GPU에 위임하여 더욱 효율적으로 처리하는 방법을 말한다. GPU는 애초에 그래픽 작업을 처리하기 위해 만들어진 것으로 화면을 그릴 때 활용하면 굉장히 빠르다.
특정 요소에 하드웨어 가속을 이용하려면 요소를 별도의 레이어로 분리하여 GPU로 보내야하는데, 앞서 얘기한 것처럼 transform 속성과 opacity 속성이 이 역할을 한다. 분리된 레이어는 GPU에 의해 처리되어 레이아웃 단계와 페인트 단계없이 화면상의 요소의 스타일을 변경할 수 있다. 따라서 리플로우와 리페인트 대신 해당 속성 2개를 이용하면 애니메이션 성능이 더 좋을 수 밖에 없다.
다시 lecture 예시를 보면 width 변경시 리플로우가 발생하는데 1 frame을 넘어가서 화면을 제때 그리지 못하여 쟁크 현상이 발생하는 것이다.
원인과 결과를 알았으니 tranform으로 변경하자. transform에는 위치를 이동시키는 translate, 크기를 변경하는 scale, 요소를 회전시키는 rotate가 대표적인데, 여기서는 scale을 사용하자.
컴포넌트 지연로딩
모달 코드에서 image-gallery.js 청크파일을 불러오는데 첫 화면부터는 필요하지 않다. 이를 lazy loading 처리를 하자.
import React, { useState, lazy, Suspense } from 'react';
import styled from 'styled-components';
import Header from './components/Header';
import InfoTable from './components/InfoTable';
import SurveyChart from './components/SurveyChart';
import Footer from './components/Footer';
// 변경!!
// import ImageModal from './components/ImageModal'
const LazyImageModal = lazy(() => import('./components/ImageModal'));
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div className='App'>
<Header />
<InfoTable />
<ButtonModal
onClick={() => {
setShowModal(true);
}}
>
올림픽 사진 보기
</ButtonModal>
<SurveyChart />
<Footer />
// 변경!!
{/* {showModal ? <LazyImageModal closeModal={() => { setShowModal(false) }} /> : null} */}
<Suspense fallback={null}>
{showModal ? (
<LazyImageModal
closeModal={() => {
setShowModal(false);
}}
/>
) : null}
</Suspense>
</div>
);
}
이렇게 변경시 모달 클릭시 새로운 청크파일 2개가 로드되는 것을 확인할 수 있다.
컴포넌트 사전 로딩
앞에서 지연로딩을 사용하였는데 이 기법은 초기화면 로딩시에는 효과적일지 몰라도 모달을 띄우는 시점에는 한계가 있다. 모달 코드를 분리했기 때문에 모달을 띄우는 시점에 네트워크 로드가 완료되어야 모달을 띄울 수 있다. 즉 모달이 무겁다면 뜨기까지 약간의 지연이 발생할 수 있다.
이를 해결하기 위해 우리는 사전 로딩이 필요하다.
사전 로딩은 나중에 필요한 모듈을 필요해지기 전에 미리 로드하는 기법이다. 요는 언제 모달 코드를 미리 로드해두는지인데 여기서 고려할 타이밍은 2개로 하나는 버튼 위에 마우스를 올려놓았을때(mouseenter)이고 다른 하나는 최초에 페이지가 로드되고 모든 컴포넌트 마운트가 끝났을 때이다.
- MouseEnter
function App() { const [showModal, setShowModal] = useState(false); // 변경!! const handleMouseEnter = () => { const component = import('./components/ImageModal'); }; return ( <div className='App'> <Header /> <InfoTable /> <ButtonModal onClick={() => { setShowModal(true); }} // 변경!! onMouseEnter={handleMouseEnter} > 올림픽 사진 보기 </ButtonModal> <SurveyChart /> <Footer /> <Suspense fallback={null}> {showModal ? ( <LazyImageModal closeModal={() => { setShowModal(false); }} /> ) : null} </Suspense> </div> ); }
- 모달을 띄우기 위해서는 선행적으로 마우스를 버튼 위에 올려두어야 한다. 그렇기에 해당 타이밍을 지정하였고 onMouseEnter 이벤트를 사용하면 된다. 이 방식은 찰나의 순간이긴 하지만 브라우저가 새로운 파일을 로드하기에는 충분하다
- 컴포넌트의 마운트 완료 후 사전 로딩
이렇게 추가해주면 페이지가 로드된 후 모달 코드를 다운로드할 수 있다.useEffect(() => { const component = import('./components/ImageModal'); }, []);
- 모달의 크기가 크다면 로드하는데 1초 또는 그 이상의 시간이 걸릴 수 있다. 이런 경우엔 마우스 커서를 올렸을 때보다 더 먼저 파일을 로드해야한다. 이때 생각해볼 수 있는 타이밍은 모든 컴포넌트의 마운트가 완료된 후로, 브라우저에 여유가 생겼을 때 뒤이어 모달을 추가로 로드하는 것이다. 클래스 컴포넌트라면 componentDidMount 시점이고 함수형 컴포넌트는 useEffect 시점이다.
2가지 방법으로 사전 로딩 기법을 사용하였다. 이 두가지 말고도 다른 방법들이 있고 서비스나 기능의 특성에 따라 다양한 방법으로 적용할 수 있다. 중요한 것은 어느 타이밍에 사전 로드하는 것이 해당 서비스에서 가장 합리적인지 판단하는 일이다.
이미지 사전 로딩
느린 이미지 로딩
이미지 사이즈가 커서 다운로드 시간이 오래걸려 이미지가 제때 뜨지 않아 dom이 이상하게 겹치는 현상을 발견할 수 있는데 이를 해결하기 위해 이미지 사전 로딩을 적용해보자.
컴포넌트는 import 함수를 이용하여 로드했는데, 이미지는 이미지가 화면에 그려지는 시점, 즉 HTML 또는 CSS에서 이미지 사용하는 시점에 로드된다. 이를 해결하기 위한 한가지 방식으로 js 로 직접 이미지를 로드하는 방식으로 new 연산자를 이용하여 생성할 수 있다.
const img = new Image()
img.src = '{image src}'
//example
useEffect(() => {
const component = import('./components/ImageModal');
const img = new Image()
img.src = '<https://stillmed.olympic.org/media/Photos/2016/08/20/part-1/20-08-2016-Football-Men-01.jpg?interpolation=lanczos-none&resize=*:800>'
}, []);
이렇게 추가 시 페이지가 로드되는 과정에서 모달 코드와 함께 이미지가 다운로드 된다.
추가로 고민해보아야 할 점은 바로 몇장의 이미지까지 사전 로드해 둘 것인가이다. 브라우저 리소스를 잘 분배하는 것이 다른 성능 문제 이슈도 피할 수 있고 개발자의 몫이라고 생각한다. 정말 사전 로딩이 필요한지 고민해보고 사용하자.
'개발 > Front-end' 카테고리의 다른 글
e.preventDefault();의 역할과 언제 사용해야 할까? (0) | 2025.02.13 |
---|---|
웹 개발 스킬을 한단계 높여주는 프론트엔드 성능 최적화 - 3장 홈페이지 최적화 (0) | 2025.01.21 |
웹 개발 스킬을 한단계 높여주는 프론트엔드 성능 최적화 - 1장 블로그 서비스 최적화 (0) | 2025.01.16 |
ARIA 속성에 대하여 - 웹 접근성 (1) | 2024.10.09 |
웹 개발 스킬을 한단계 높여주는 프론트엔드 성능 최적화 - 0 (1) | 2024.09.24 |