리액트로 만든 홈페이지, iframe 로딩 속도 개선하기: React.js 에서 Prefetching 구현

minkyeongJ·2024년 9월 25일
0

개발 tips

목록 보기
21/21
post-thumbnail

최근 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>

이렇게 하면 사용자가 메뉴 아이템에 마우스를 올릴 때마다 해당 페이지의 내용을 미리 불러오기 시작한다. 그러면 실제로 클릭했을 때 더 빠르게 페이지를 보여줄 것이다.

하지만 주의할 점도 있다.

  1. 이 방식은 단순히 HTML만 가져오는 거라서 Next.js의 프리페칭만큼 효과적이진 않을 수 있다.
  2. 서버 사이드 렌더링을 쓰는 경우엔 완전한 페이지 데이터를 프리페치하기 어려울 수 있다.
  3. 너무 많은 링크에 이 기능을 사용하면 불필요한 네트워크 요청이 엄청 늘어날 수 있으니 주의해야 한다.

이렇게 하면 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;

이 코드는 크게 두 부분으로 나눌 수 있다.

  1. 성능 측정 로직: measurePerformance 함수에서 각종 성능 지표를 수집한다. 페이지 로드 시간, FCP, LCP, Time to Interactive 등을 측정하고, iframe의 로딩 상태도 체크한다.

  2. 데이터 분석: 측정된 데이터를 바탕으로 평균 페이지 로드 시간을 계산하고, 콘솔에 로그를 출력한다.

이 컴포넌트를 사용하면, 페이지가 로드될 때마다 성능 데이터를 수집하고 분석할 수 있다. 프리페칭을 적용하기 전과 후의 데이터를 비교해보았을 때 pageLoadTime이 614ms에서 523ms로, 약 91ms가 줄어들었다. 거의 15% 개선이 되었다.

사용 팁
1. 프리페칭 적용 전에 이 컴포넌트를 사용해 기준 데이터를 수집하기.
2. 프리페칭을 적용한 후 다시 측정하기.
3. 두 데이터를 비교해 보기.

주의할 점

  • 개발 환경과 프로덕션 환경의 성능은 다를 수 있다. 실제 사용자 경험을 정확히 파악하려면 프로덕션 환경에서 테스트해야 한다.
  • 네트워크 상황, 디바이스 성능 등 다양한 요인이 측정 결과에 영향을 줄 수 있다. 여러 번 측정해서 평균을 내는 게 좋다.

React Router와 함께 프리페칭 사용하기

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;

이렇게 하면 사용자가 네비게이션 링크에 마우스를 올릴 때마다 해당 페이지의 내용을 미리 불러오기 시작한다. 그러면 실제로 클릭했을 때 페이지 전환이 훨씬 더 빠르게 느껴질 것이다.

하지만 여기서도 주의할 점이 있다.

  1. React Router는 클라이언트 사이드 라우팅을 사용하기 때문에, 이 방식으로 프리페치하는 건 주로 API 데이터나 동적 콘텐츠에 효과적이이다. 정적인 라우트 구조 자체는 이미 앱에 포함되어 있기 때문이다.

  2. 데이터 fetching 로직이 컴포넌트 내부에 있다면, 이 프리페치 방식으로는 그 데이터까지 미리 가져오기 어려울 수 있다. 이 경우에는 데이터 fetching 로직을 별도의 함수로 분리해서 프리페치 과정에서도 호출할 수 있게 만들어야 한다.

  3. 복잡한 앱의 경우, 모든 링크에 프리페치를 적용하면 불필요한 네트워크 요청이 많아질 수 있다. 중요한 페이지나 자주 방문하는 페이지 위주로 적용하는 게 좋다.

이런 방식으로 React Router와 함께 프리페칭을 구현하면, Single Page Application(SPA)에서도 페이지 전환이 훨씬 부드럽고 빠르게 느껴질 것이다. 특히 데이터를 많이 불러와야 하는 페이지라면 효과가 클 것이다.

profile
멋진 프론트엔드 개발자를 위하여!

0개의 댓글