SEO를 위한 FCP타임 줄이기

KB LEE·2022년 12월 14일
0

SEO와 FCP는 어떤 관계인가요?

검색엔진 알고리즘을 빈번히 업데이트하지만 모든 변경사항을 공개하지 않습니다.
하지만 웹 사이트의 속도는 SEO 평가지표 중 중요한 요소입니다.

https://developers.google.com/search/docs/fundamentals/get-started#manage-the-user-experience

저는 구글에서 제공하는 PageSpeed Insights를 통해 개별 페이지의 성능테스트 했고, 그 측정항목 중 FCP(First Contentful Paint)가 낮은 점수를 받고 있었습니다

FCP 를 줄이기 위해 확인해야할 내용

Eliminate render-blocking resources

FCP를 줄이기 위해 가장 중점으로 확인한 요소입니다.

웹 브라우저는 HTML을 parsing하면서 DOM 구조를 만듭니다.
이때 외부에서 CSS, JS를 불러오는 <link>, <script> 태그를 만나면 각각의 파일을 불러오게 됩니다.

Script는 parser blocking 하기때문에 파서가 대기하는 시간이 생기게 됩니다.
( 이전에 <script>태그를 <body> 최하단에 넣는 대안을 사용하는 이유가 그런 이유였습니다 )

심지어 파일을 로드하는데 네트워크 통신이 일어난다면 대기시간은 더 길어지게 됩니다.


세부내용

현재 최적화가 필요한 페이지

위 이미지는 현재 최적화가 필요하다고 생각하는 페이지의 <head>입니다.
React Source가 코드분할되어 적용되고 있는 모습과 kakao-js-sdk, jquery, iamport가 추가되어있습니다.

별 문제가 없어보일 수도 있지만 저의 요구조건과는 조금 다릅니다.
제가 요구하는 조건은 아래와 같습니다.

  1. 특정 페이지,컴포넌트에서만 iamport, jquery를 불러오고 싶다
  2. iamport를 불러오기 이전에 의존성 Lib인 jquery를 먼저 불러온다.

해당 조건을 만족시키기 위해 특정 페이지 또는 컴포넌트가 실행될 때, 동적으로 <script>태그를 생성해 넣어주는 방법을 대안으로 선택했습니다.

개선할 페이지는 React, TS로 작업되어있습니다. 그래서 해당 기능은 useIamportModule이라는 CustomHook으로 생성했습니다

// useIamportModule.ts
import React, { useEffect, useState, useCallback } from 'react';

// Helper function types
export interface ILoadScriptTag<T> {
  type?: keyof React.ReactHTML;
  elementAttribute: T;
  callbackAfterLoad?: () => void;
}

export const loadScriptTag = async <T extends Partial<HTMLScriptElement>>({
  type = 'script',
  elementAttribute,
  callbackAfterLoad,
}: ILoadScriptTag<T>): Promise<{ status: 'loaded' }> => {
  if (!elementAttribute) throw new Error('elementAttribute is required');

  const scriptElement = document.createElement(type) as HTMLScriptElement;
  Object.entries(elementAttribute).forEach(([key, value]) => {
    if (value !== undefined && value !== null) {
      scriptElement.setAttribute(key, value as string);
    }
  });

  return new Promise((resolve, reject) => {
    scriptElement.onload = () => {
      callbackAfterLoad?.();
      resolve({ status: 'loaded' });
    };

    scriptElement.onerror = () => {
      reject(new Error('Failed to load script: ' + (elementAttribute.src || 'unknown source')));
    };

    document.head.appendChild(scriptElement);
  });
};

// Extend global window interface
declare global {
  interface Window {
    jQuery: any;
    IMP: {
      init: (merchantCode: string) => void;
    };
  }
}

interface IUseIamportModule {
  initLoad?: boolean;
  errorCallback?: () => void;
}

export default function useIamportModule({ initLoad = false, errorCallback }: IUseIamportModule = {}) {
  const [isLoading, setIsLoading] = useState(false);

  const isJQueryLoaded = useCallback(() => !!window.jQuery, []);
  const isIamportLoaded = useCallback(() => !!window.IMP, []);

  const loadJQuery = useCallback(() => {
    if (isJQueryLoaded()) return Promise.resolve();

    return loadScriptTag<Partial<HTMLScriptElement>>({
      elementAttribute: {
        id: 'iamport-jquery',
        defer: true,
        src: process.env.REACT_APP_JQUERY_CDN as string,
      },
    });
  }, [isJQueryLoaded]);

  const loadIamport = useCallback(() => {
    if (isIamportLoaded()) return Promise.resolve();

    return loadScriptTag<Partial<HTMLScriptElement>>({
      elementAttribute: {
        id: 'iamport-sdk',
        defer: true,
        src: process.env.REACT_APP_IAMPORT_CDN as string,
      },
      callbackAfterLoad: () => {
        window.IMP?.init(process.env.REACT_APP_IAMPORT_CODE as string);
      },
    });
  }, [isIamportLoaded]);

  const initIamportModule = useCallback(async () => {
    setIsLoading(true);
    try {
      await loadJQuery();
      await loadIamport();
    } catch (error) {
      console.error('Failed to initialize Iamport module:', error);
      errorCallback?.();
    } finally {
      setIsLoading(false);
    }
  }, [loadJQuery, loadIamport, errorCallback]);

  useEffect(() => {
    if (initLoad) {
      initIamportModule();
    }
  }, [initLoad, initIamportModule]);

  return {
    isLoadingIamport: isLoading,
    IMP: window.IMP,
    load: initIamportModule,
  };
}

결과

FCP 최적화 작업 이전 지표

FCP 최적화 작업 이후 지표


마무리

FCP Time에 대한 문제를 해결하고, SEO에 도움이 되는 성능 개선을 하였습니다
물론 FCP Time만 줄인다고 SEO에 놀라운 향상을 이루어낼 순 없습니다.

하지만 구글의 SEO 랭킹 요소 중 웹사이트 성능이 큰 요소를 차지하기 때문에 유의미한 결과일 것입니다.
그리고 부가적으로 FCP Time을 줄이는 등 성능 개선을 통해 더 나은 UX를 제공할 수도 있다는 장점도 있을 것입니다

다음에도 SEO에 대한 더 나은 개선책이 있다면 포스팅하겠습니다 :)
읽어주셔서 감사합니다.

profile
한 발 더 나아가자

0개의 댓글