최근 React.js로 회사 홈페이지를 만들고 있는데, 특정 웹사이트를 iframe으로 불러와 보여주는 기능을 구현해야 했다. 근데 iframe의 로딩 속도가 시간이 꽤 걸려 어떻게 하면 이 로딩 속도를 빠르게 만들 수 있을지 고민하다가 문득 Next.js의 <Link/>
컴포넌트에서 사용하는 Prefetching 기능이 떠올랐다.
Next.js 문서를 보면 <Link/>
컴포넌트를 사용할 때 자동으로 Prefetching이 일어난다고 적혀있다.
(https://nextjs.org/docs/app/api-reference/components/link)
Prefetching이 뭘까? 간단히 말하면, 사용자가 링크에 마우스를 올리면 "아, 이 사람 이 페이지로 갈 것 같은데?" 하고 예측해서 미리 html 파일을 불러오는 것이다. 그리고 Next.js에서는 뷰포인트에 있는 링크들을 자동으로 프리패칭 한다고 한다. 이러한 과정이 있기에 사용자가 실제로 다른 페이지에 접속할 때 이미 불러온 파일을 보여주니 FCP(First Contentful Paint)가 훨씬 빨라진다.
"Next.js가 React 기반이니까 우리도 만들 수 있지 않을까?"하는 생각이 들었고, 그래서 한번 Prefetching을 직접 구현해 보았다.
먼저 usePrefetch
라는 커스텀 훅을 만들었다.
import { useState, useCallback } from "react";
const usePrefetch = () => {
const [prefetchedUrls, setPrefetchedUrls] = useState(new Set());
const prefetch = useCallback(
async (url) => {
if (prefetchedUrls.has(url)) return;
try {
const res = await fetch(url, {
method: "GET",
mode: "no-cors",
headers: {
Purpose: "prefetch", // 이 헤더로 프리페치 요청임을 알려줘요
},
});
if (res.ok) {
setPrefetchedUrls((prev) => new Set(prev).add(url));
}
} catch (error) {
console.error("앗, 프리페치 실패:", error);
}
},
[prefetchedUrls]
);
return prefetch;
};
export default usePrefetch;
이 훅을 사용하면 특정 URL을 프리페치할 수 있다. 그리고 이미 프리페치한 URL은 다시 요청하지 않도록 한다.
그 다음엔 이 훅을 실제로 사용하는 방법이다. 예를 들어, 메뉴 아이템에 마우스를 올렸을 때 프리페치가 시작되도록 할 수 있다.
<MenuItem
key={index}
$isHome={isHome}
$isAtTop={isAtTop}
$hoverColor="#00aeef"
onMouseEnter={() => {
setMouseOver(item.title);
prefetch(item.link); // 여기서 프리페치 시작!
}}
onMouseLeave={() => setMouseOver("")}
>
{/* 메뉴 아이템 내용 */}
</MenuItem>
이렇게 하면 사용자가 메뉴 아이템에 마우스를 올릴 때마다 해당 페이지의 내용을 미리 불러오기 시작한다. 그러면 실제로 클릭했을 때 더 빠르게 페이지를 보여줄 것이다.
하지만 주의할 점도 있다.
이렇게 하면 Next.js의 <Link/>
컴포넌트처럼은 아니지만, 순수 React에서도 어느 정도 프리페칭 효과를 낼 수 있다. 특히 iframe으로 불러오는 페이지의 경우, 이런 방식으로 미리 로드를 시작하면 사용자 경험을 크게 개선할 수 있을 것이다.
이렇게 프리패칭 코드를 구현해 iframe을 사용하는 페이지의 링크에 마우스를 올리면 iframe에 사용하는 주소를 미리 불러오도록 만들었다.
이제 프리페칭을 적용했으니 실제로 얼마나 성능이 개선되었는지 확인해 보자. 프리패칭 특성상 lighthouse를 사용해서 성능을 측정하기에 어려움이 있었기에, 성능을 측정할 수 있는 코드를 구현해야 했다.
다음은 간단한 성능 측정 코드이다. 이 코드를 사용하면 페이지 로드 시간, First Contentful Paint (FCP), Largest Contentful Paint (LCP) 등 다양한 성능 지표를 측정할 수 있다.
import React, { useState, useEffect } from 'react';
const PerformanceMeasurement = () => {
const [performanceData, setPerformanceData] = useState([]);
const [loading, setLoading] = useState(true); // iframe 로딩 상태
const [lcp, setLcp] = useState(null);
useEffect(() => {
let lcpObserver;
const measurePerformance = () => {
const performanceEntries = performance.getEntriesByType("navigation")[0];
const paintEntries = performance.getEntriesByType("paint");
const fcpEntry = paintEntries.find(
(entry) => entry.name === "first-contentful-paint"
);
const newPerformanceData = {
navigationType: performanceEntries.type,
pageLoadTime:
performanceEntries.loadEventEnd - performanceEntries.startTime,
firstContentfulPaint: fcpEntry ? fcpEntry.startTime : "N/A",
largestContentfulPaint: lcp || "N/A",
timeToInteractive:
performanceEntries.domInteractive - performanceEntries.startTime,
iframeLoadTime: loading ? "Not loaded yet" : "Loaded",
};
setPerformanceData((prevData) => [...prevData, newPerformanceData]);
console.log("New Performance Measurement:", newPerformanceData);
};
// LCP 측정을 위한 PerformanceObserver 설정
lcpObserver = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
setLcp(lastEntry.renderTime || lastEntry.loadTime);
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
// 컴포넌트 마운트 시 즉시 성능 측정 시작
measurePerformance();
// 페이지 완전 로드 후 다시 한 번 측정
window.addEventListener("load", measurePerformance);
return () => {
window.removeEventListener("load", measurePerformance);
if (lcpObserver) {
lcpObserver.disconnect();
}
};
}, [loading, lcp]);
// 성능 데이터 변경 시 평균 계산
useEffect(() => {
if (performanceData.length > 0) {
const avgPageLoadTime =
performanceData.reduce((sum, data) => sum + data.pageLoadTime, 0) /
performanceData.length;
console.log("Performance Data Updated:");
console.log("Total Measurements:", performanceData.length);
console.log("Average Page Load Time:", avgPageLoadTime.toFixed(2), "ms");
console.log("All Measurements:", performanceData);
}
}, [performanceData]);
// 컴포넌트의 나머지 부분...
};
export default PerformanceMeasurement;
이 코드는 크게 두 부분으로 나눌 수 있다.
성능 측정 로직: measurePerformance
함수에서 각종 성능 지표를 수집한다. 페이지 로드 시간, FCP, LCP, Time to Interactive 등을 측정하고, iframe의 로딩 상태도 체크한다.
데이터 분석: 측정된 데이터를 바탕으로 평균 페이지 로드 시간을 계산하고, 콘솔에 로그를 출력한다.
이 컴포넌트를 사용하면, 페이지가 로드될 때마다 성능 데이터를 수집하고 분석할 수 있다. 프리페칭을 적용하기 전과 후의 데이터를 비교해보았을 때 pageLoadTime이 614ms에서 523ms로, 약 91ms가 줄어들었다. 거의 15% 개선이 되었다.
사용 팁
1. 프리페칭 적용 전에 이 컴포넌트를 사용해 기준 데이터를 수집하기.
2. 프리페칭을 적용한 후 다시 측정하기.
3. 두 데이터를 비교해 보기.
주의할 점
React Router를 사용하여 프리패칭을 구현할 수도 있다.
React Router를 사용할 때는 Link
컴포넌트를 커스텀해서 프리페칭 기능을 추가할 수 있다. 이렇게 하면 Next.js의 Link
컴포넌트와 비슷한 효과를 낼 수 있다.
먼저, 커스텀 Link
컴포넌트를 만들어 보자.
import React from 'react';
import { Link } from 'react-router-dom';
import usePrefetch from './usePrefetch'; // 아까 만든 훅을 import
const PrefetchLink = ({ to, children, ...props }) => {
const prefetch = usePrefetch();
const handleMouseEnter = () => {
prefetch(to); // 마우스가 올라갔을 때 프리페치 시작
};
return (
<Link
to={to}
onMouseEnter={handleMouseEnter}
{...props}
>
{children}
</Link>
);
};
export default PrefetchLink;
이렇게 만든 PrefetchLink
컴포넌트는 기존 React Router의 Link
컴포넌트와 거의 똑같이 사용할 수 있다. 단, 마우스를 올렸을 때 자동으로 프리페칭이 시작된다는 점이 다르다.
다음과 같이 컴포넌트를 사용해 볼 수 있다.
import React from 'react';
import PrefetchLink from './PrefetchLink';
const Navigation = () => {
return (
<nav>
<PrefetchLink to="/">홈</PrefetchLink>
<PrefetchLink to="/about">소개</PrefetchLink>
<PrefetchLink to="/products">제품</PrefetchLink>
<PrefetchLink to="/contact">문의</PrefetchLink>
</nav>
);
};
export default Navigation;
이렇게 하면 사용자가 네비게이션 링크에 마우스를 올릴 때마다 해당 페이지의 내용을 미리 불러오기 시작한다. 그러면 실제로 클릭했을 때 페이지 전환이 훨씬 더 빠르게 느껴질 것이다.
하지만 여기서도 주의할 점이 있다.
React Router는 클라이언트 사이드 라우팅을 사용하기 때문에, 이 방식으로 프리페치하는 건 주로 API 데이터나 동적 콘텐츠에 효과적이이다. 정적인 라우트 구조 자체는 이미 앱에 포함되어 있기 때문이다.
데이터 fetching 로직이 컴포넌트 내부에 있다면, 이 프리페치 방식으로는 그 데이터까지 미리 가져오기 어려울 수 있다. 이 경우에는 데이터 fetching 로직을 별도의 함수로 분리해서 프리페치 과정에서도 호출할 수 있게 만들어야 한다.
복잡한 앱의 경우, 모든 링크에 프리페치를 적용하면 불필요한 네트워크 요청이 많아질 수 있다. 중요한 페이지나 자주 방문하는 페이지 위주로 적용하는 게 좋다.
이런 방식으로 React Router와 함께 프리페칭을 구현하면, Single Page Application(SPA)에서도 페이지 전환이 훨씬 부드럽고 빠르게 느껴질 것이다. 특히 데이터를 많이 불러와야 하는 페이지라면 효과가 클 것이다.