
리우 올림픽과 런던 올림픽 정보와 사진을 비교하여 보여 주고, 하단에는 그에 대한 설문 조사 결과를 보여줍니다. 여기에서 설문조사 결과는 막대 그래프로 나타나는데, 항목을 클릭하면 해당 항목에 대해 필터링되어 그래프가 애니메이션과 함께 변합니다.




컴포넌트 지연 로딩
1장에서 학습한 코드 분할 기법이랑 유사하지만 아주 살짝 다릅니다.
이번에는 페이지 코드 자체를 분할하는 대신, 단일 컴포넌트를 분할해서 컴포넌트가 쓰이는 순간에 불러오도록 만들어 봅시다!!
컴포넌트 사전 로딩
컴포넌트 코드를 분할하여 지연 로딩을 적용하면, 첫 화면 진입 시 분할된 코드 중 당장 필요한 코드만 다운로드하기 때문에 첫 화면을 더 빠르게 그릴 수 있게 됩니다. 하지만 서비스 이용 과정에서 분할된 컴포넌트를 사용하려고 할 때, 다운로드되어 있지 않은 코드를 추가로 다운로드하는 시간만큼 서비스 이용에 지연이 발생합니다.
위와 같은 문제를 어떻게 해결할 수 있을까요? 바로 컴포넌트 사전 로딩 기법을 활용하면 됩니다!
코드를 분할하여 첫 화면 진입 시에는 다운로드하지 않지만, 이후 해당 코드가 필요한 시점보다는 먼저 코드를 로드하여 해당 코드를 지연 없이 사용할 수 있도록 하는 컴포넌트 사전 로딩 기법을 살펴 보겠습니다.
이미지 사전 로딩
이미지도 컴포넌트처럼 이미지를 필요한 시점에 로드하면 이미지가 로드되는 시간만큼 기다려야 합니다. 그래서 이미지도 필요한 시점보다 먼저 다운로드해 두고, 필요할 때 바로 이미지를 보여 줄 수 있도록 하는 이미지 사전 로딩 기법을 적용해 보겠습니다.
애니메이션의 원리부터 이해해 봅시다!!
여러 장의 이미지를 빠르게 전환하여 우리 눈에 잔상을 남기고, 그로 인해 연속된 이미지가 움직이는 것처럼 느껴지게 하는 것


=> 2번에서 4번으로 넘어갈 때 어색한 느낌이 들 것이다!
일반적으로 사용하는 디스플레이의 주사율 - 60Hz
→ 1초에 60장의 정지된 화면을 빠르게 보여 줌
→ 브라우저도 동일하게 최대 60FPS로 1초에 60장의 화면을 새로 그림
끊김 현상(jank)가 발생하는 원인 - 브라우저가 초당 60장의 화면을 그리지 못했기 때문
초당 60장의 화면을 그리지 못하는 이유를 알아보기 위해 브라우저 렌더링 과정을 다시 살펴봅시다!
렌더링 과정을 통해 화면이 전부 그려진 후, 일부 요소의 스타일을 변경/추가/제거한다면 주요 렌더링 경로에서 거친 과정을 다시 한 번 실행하면서 새로운 화면을 그리게 됩니다. 이렇게 새로운 화면을 그리는 것을 리플로우 혹은 리페인트라고 합니다.
특정 작업을 CPU가 아닌 GPU에 위임함으로써 성능을 향상시키는 기술
리플로우와 리페인트를 피하는 방법으로, transform, opacity 같은 속성을 사용하여, 해당 요소를 별도의 레이어로 분리하고 작업을 GPU에 위임하여 처리함으로써 레이아웃 단계와 페인트 단계를 건너뛸 수 있다!!
GPU는 애초에 그래픽 작업을 처리하기 위해 만들어진 것이기 때문에 화면을 그릴 때 활용하면 매우 빠릅니다.
width 의 변경으로 인한 리플로우 발생
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;
`;
transform 으로 변경하여 최적화미리 막대의 너비를 100%로 채워 두고 scale을 이용하여 비율에 따라 줄이는 방식을 활용하면 아래와 같이 성능을 개선할 수 있습니다.

책에서 사용한 애니메이션 효과가 적용된 막대 그래프 컴포넌트
const BarGraph = styled.div`
position: absolute;
left: 0;
top: 0;
width: 100%;
transform: scaleX(
${({ width }) => width / 100}
); // 여기서 직접 width를 쓰는게아니라, scaleX함수를 통해 화면에 표현되는 비율을 수정
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;
`;
기존에는 쟁크 현상(살짝 끊김)이 발생했지만, 하드웨어 가속 기법을 통해 문제 해결 성공!
transform(translate, scale, rotate) 속성을 변경할 때 다른 요소들에 대한 레이아웃 계산을 다시 할 필요가 없기 때문이러한 방식은 렌더트리 생성, 레이아웃 계산, 페인트 등의 복잡한 과정을 건너뛰게 해 성능을 크게 향상시킬 수 있다고 합니다.
아닙니다!!
- 움직임/애니메이션 - transform 사용 (성능 개선 가능) 권장
- 고정 크기/배치 조절- width, margin 등 기본 속성 사용 권장
transform은 주로 애니메이션이나 전환 효과에 사용되며, 이 경우에는 레이아웃 단계와 페인팅 단계를 건너뛸 수 있어 성능 이점이 있음.
그러나 정적인 요소의 너비를 설정하는 데 transform을 사용하는 것은 불필요할 수 있으며, 코드의 가독성을 저하시킬 수 있다.
transform 속성은 요소의 시각적인 모양만 바꾸며, 실제 레이아웃에는 영향을 주지 않기때문에 이는 레이아웃을 예상한 대로 조절하기 어렵게 만들 수 있다.
위 Bar컴포넌트처럼 정해진 크기 + 애니메이션처리에 대해서는 직접 width를 조절하기보다 transform을 사용하자.
올림픽 통계 서비스의 번들 분석 결과

위의 이미지에서 Image-gallery.js 라이브러리는 모달창이 띄워질 때만 필요합니다. 따라서 첫 화면부터 필요하지 않습니다.
기존코드 : ImageModal을 App.js에서 바로 import해서 사용
-> Image-gallery.js 라이브러리를 처음부터 가져옴
import ImageModal from './components/ImageModal'
function App() {
const [showModal, setShowModal] = useState(false)
return (
<div className="App">
<Header />
<InfoTable />
<ButtonModal onClick={() => { setShowModal(true) }}>올림픽 사진 보기</ButtonModal>
<SurveyChart />
<Footer />
{showModal ? <ImageModal closeModal={() => { setShowModal(false) }} /> : null}
</div>
)
}
수정 코드: 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 />
<Suspense fallback={null}>
{showModal ? (
<LazyImageModal
closeModal={() => {
setShowModal(false);
}}
/>
) : null}
</Suspense>
</div>
);
}

위의 번들 분석 결과를 통해 기존과 다르게 분리되었다는 것을 확인할 수 있습니다.
즉 showModal이 true가 되는 시점에 동적으로 라이브러리를 호출하도록 수정하게 되었다.
네트워크 탭에서도 [올림픽 사진 보기] 버튼을 누른 시점에 청크 파일 두 개가 로드되는 것을 볼 수 있습니다. 두 개의 청크 파일이 바로 ImageModal 컴포넌트와 react-image-gallery 라이브러리 파일입니다.
그러나 지연로딩을 사용하면, 분리했던 컴포넌트를 호출하는 시점에 데이터를 받아오기때문에 약간의 지연이 발생한다는 단점이 있습니다.
→ 해당 단점은 컴포넌트 사전 로딩으로 해결 가능
로딩 시점
1. 사용자가 누르기 직전 (마우스를 올려두었을때)
2. 최초에 페이지가 로드되고 컴포넌트 마운트가 끝났을때
버튼을 클릭하기 위해서는 먼저 마우스를 버튼 위에 올려 두어야 한다는 점을 이용하여 마우스가 버튼에 올라오면 아직 버튼을 클릭하지는 않았지만, 곧 클릭할 것으로 예측하고 모달 컴포넌트를 미리 로드하는 방식
목적
const handleMouseEnter = () => {
import("./components/ImageModal").catch();
// 마우스를 올리는 순간 ImageModal을 백그라운드에서 불러옴
// (브라우저는 같은 모듈이면 네트워크 요청을 반복하지 않고 캐시를 사용함)
// import()는 동적으로 모듈을 불러오는 함수
// 여기서 미리 불러놓으면, 나중에 실제로 쓸 때 빠르게 보여줄 수 있습니다.
};
return (
<div className="App">
<Header />
<InfoTable />
<ButtonModal
onMouseEnter={handleMouseEnter} // hover시 미리 ImageModal 컴포넌트를 사전에 로딩한다.
onClick={() => {
setShowModal(true);
}}
>
올림픽 사진 보기
</ButtonModal>
// 마우스를 버튼 위에 올리면 handleMouseEnter가 실행되어 ImageModal을 사전 로딩합니다.
// 버튼을 클릭하면 setShowModal(true)가 실행되어 모달이 열립니다.
<SurveyChart />
<Footer />
<Suspense fallback={null}>
{showModal ? (
<LazyImageModal
closeModal={() => {
setShowModal(false);
}}
/>
) : null}
</Suspense>
useEffect(() => {
import("./components/ImageModal").catch();
// Hover보다 더 빠르게, 모든 컴포넌트가 마운트 완료되면 추가로 로드
}, []);
이제 모달창을 클릭한 뒤에 기다리지 않고 바로 데이터를 확인할 수 있습니다!
흐름 정리
컴포넌트와 다르게 이미지는 화면에 그려지는 시점에 로드됩니다. (단순 import X)
import 함수를 이용하지 않고, HTML 또는 CSS에서 이미지를 사용하는 시점에 로드됩니다.
<img src="..." />처럼 사용되는 순간에 다운로드!!
사전로딩을 적용해 봅시다!
useEffect(() => {
// 컴포넌트는 미리 import로 로드
import("./components/ImageModal").catch();
// 이미지는 이렇게 미리 객체로 만들어서 src를 지정
const img = new Image();
img.src = "https://...이미지경로";
}, []);
정리
<img src=”…”>로 직접 사용하는 경우new Image()로 미리 객체를 생성해 src 설정하는 경우사전 로딩을 하는 순간, 브라우저의 리소스를 그만큼 많이 사용하기 때문에 다른 성능 문제를 야기할 수 있습니다. 따라서 어떤 콘텐츠를 사전 로드할 때는 정말 사전 로딩이 필요한지 고민해야 합니다.