FE 성능개선기 2부: 카카오 비즈니스폼

안녕하세요, 비즈FE파트의 에단입니다. 

이번 글에서는 주문하기 서비스에 이어서, 작년 말부터 올해 초까지 카카오 비즈니스폼 프로젝트 멤버분들과 함께 비즈니스폼 서비스의 FE 성능을 개선했던 과정에 대해서 공유드리려고 합니다. 

어떤 개선점들을 발견했고, 이를 개선하기 위해 어떤 방법을 사용했는지, 그리고 성능 개선 작업을 진행하며 느낀 점들에 대해 말씀드리겠습니다.

비즈니스폼이란?

먼저, 성능 개선을 진행한 ‘비즈니스폼’이라는 서비스를 간단하게 소개하겠습니다.

비즈니스폼은 응모, 설문조사, 신청예약 등의 서비스 설계를 지원하는 비즈니스 도구로, 파트너 플랫폼과 유저 플랫폼이 있습니다. 파트너 플랫폼에서 파트너사가 설문을 만들면, 카카오톡 사용자는 유저 플랫폼을 통해 만들어진 설문에 참여할 수 있도록 하는 서비스입니다.

아래 이미지에서 볼 수 있듯 파트너사는 다양한 종류의 문항을 설계해 폼을 생성하고, 카카오 비즈니스 플랫폼에 연결하여, 카카오톡 사용자의 참여를 유도하고 참여자의 응답 결과를 받을 수 있습니다.

또한, 카카오톡 사용자는 톡메시지나 광고 지면 등을 통해서 카카오톡 어디에서나 유저플랫폼에 접근해 아래와 같이 해당 설문에 참여할 수 있게 됩니다.

성능 개선을 진행하게 된 이유 & 개선 목표치

카카오 FE플랫폼팀은 사용자가 느끼는 서비스의 품질 향상을 위해 다양한 노력을 하고 있는데요. 그중 하나로 팀에서 담당하는 서비스의 주요 페이지에 대한 정적 분석 측정 결과 중 성능 항목을 개선하는 작업을 별도 주제로 선정해 진행하고 있습니다. 

성능 측정은 FE 성능개선기 1부에서 소개되었듯 FE 개발에 도움이 되는 도구를 만드는 플랫폼FE파트에서 개발한 파루스(Pharus)라는 도구를 활용하여 진행했고, 성능 항목을 90점 이상 달성하는 것을 팀의 공통 목표로 설정하게 되었습니다.

이번 글에서는 비즈니스폼 서비스 중 ‘파트너 플랫폼’에 대한 성능 개선 경험과 과정을 소개하고자 합니다.

성능 개선 포인트 소개

번들 사이즈 최적화

웹페이지 로딩은 사용자에게 큰 영향을 주는 지표 중 하나입니다. 3초 안에 웹사이트에 접속한 사용자의 관심을 끌어야 한다는 ‘3초의 법칙’이라는 격언도 있는데요, 속도가 중요한 만큼, 저희 프로젝트에서도 웹사이트의 로딩 속도를 단축을 목표로 FCP(First contentful paint) 최적화를 위한 개선점을 모색해 보았습니다.

그 과정으로 먼저 webpack-bundle-analyzer를 이용해 비즈니스폼 앱 시작 시, 로딩하는 파일들의 크기를 분석해 보았습니다.

위 이미지에서 확인할 수 있듯, 분석을 통해, 서비스가 시작되는 데에 필요하지 않은 JavaScript 파일들이 다수 포함되어 있으며 그 크기가 크다는 점과 몇 가지 개선할 사항들을 확인할 수 있었습니다.

amCharts 지연 로딩

번들 이미지에서 사이즈가 가장 큰 파일인 빨간 네모 표시가 되어있는 부분인 amCharts는 특정 페이지에서만 나타나는 차트를 위한 라이브러리입니다. 확인 결과, 이를 사용하지 않는 페이지를 로딩하는 경우에도 라이브러리를 로딩하고 있는 것을 확인하였습니다.

이에 대한 해결책으로 아래와 같이, 실제로 로딩이 필요할 때 amCharts 라이브러리를 비동기적으로 로딩해 초기 번들링 파일에서 분리하는 작업을 진행했습니다.

				
					export const importAmCharts = async () => {
  const [
    am4core,
    ...
  ] = await Promise.all([
    import ('@amcharts/amcharts4/core'),
    ...
  ]);
    
  return {
    am4core,
    ...
  };
};
				
			

결과적으로 차트 패키지를 분리한 후, webpack-bundle-analyzer에서 파일을 다시 분석했을 때, 아래 이미지와 같이 패키지가 필요하지 않을 때는 파일이 포함되지 않게 되었으며, 엔트리포인트 파일 사이즈가 약 2.2MB에서 1.2MB로 감소하는 효과를 얻을 수 있었습니다.

CSS 분리 및 최적화

CSS 파일도 webpack-bundle-analyzer를 사용해 확인해보았는데, Javascript 파일에 포함되어 있는 것을 확인할 수 있었습니다. CSS가 JavaScript 파일에 포함되면 로딩 시, 아래의 과정을 거치게 됩니다.

  1. JavaScript 파일 로딩
  2. CSS 문자열 파싱
  3. CSS가 HTML에 삽입된 후 파싱
  4. 폰트 로딩

위의 이미지에서 확인할 수 있는 것처럼, CSS가 JavaScript 파일에 종속되면 JavaScript 파일을 먼저 로딩한 후에야 CSS를 가져올 수 있고, CSS를 브라우저에서 바로 파싱하지 않고 JavaScript에서 CSS 문자열을 파싱하여 브라우저에 전달하는 과정에서 병목이 발생합니다.

 

이를 해결하기 위해 CSS 파일을 JavaScript 파일에서 분리하여, HTML 로딩 직후에 CSS를 불러오고 폰트 또한 빠르게 로딩하고자 했습니다. 

비즈니스폼 프로젝트는 Nuxt를 사용하고 있기 때문에 nuxt config 파일의 extractCSS 옵션을 이용하여, CSS 파일이 JavaScript 파일에서 제외되도록 설정했습니다. 그리고 분리된 CSS는 nuxt-purgecss 패키지를 사용해, 사용자가 머물고 있는 페이지에서 사용하고 있는 CSS만 독립적으로 제공하도록 하여, CSS의 사이즈를 최적화할 수 있었습니다.

				
					// nuxt.config.js

const config = {
  ...
  build: {
    extractCss: true,
  },
  purgeCSS: {
    enabled: true,
    paths: [
      'path 설정',
    ],
  },
};
				
			

이 외에도 크롬 브라우저의 HTTP 동시 요청 개수 제한으로 나타나는 병목 해결을 위해, 작은 크기의 스크립트 파일을 별도의 HTTP 요청 없이 불러올 수 있도록 HTML 파일에 인라인으로 구현하거나, 사용하지 않는 구글 웹폰트 리소스 요청의 제거 및 아이콘 이미지를 압축하는 등 파일 사이즈를 최적화하는데 필요한 작업들을 진행했습니다.

결과

작업 결과 모든 페이지에서 로딩 속도 개선 효과를 얻었고, 사용자가 가장 많이 접근하는 페이지 중 하나인 비즈니스폼 목록 페이지에서는 FCP 0.4초, LCP 0.6초가 감소되는 효과를 확인할 수 있었습니다. 결과적으로, 성능 점수가 67점에서 74점으로 상승하는 효과를 얻었습니다.

컴포넌트 로딩 방식 변경

레이아웃 시프트 개선

레이아웃 시프트는 페이지 내의 콘텐츠들이 이동하는 현상으로, 성능 지표 중 하나인 CLS(Cumulative layout shift) 점수를 통해 측정됩니다.

개선 이전에는 모든 컴포넌트에 Dynamic Import를 적용하여, 필요한 시점에 컴포넌트가 로딩되도록 했고, 이를 통해 JavaScript 번들의 크기를 줄이고 성능상의 이점을 얻으려는 목적이 있었습니다. 이 결과, 로딩 속도는 개선되었지만, 각 컴포넌트가 개별적인 순서로 로딩되어 아래와 같이 레이아웃 시프트가 다수 발생하는 문제가 발견되었습니다. 또한, 조건부 렌더링이 적용된 컴포넌트들은 API 응답을 받기 전까지는 데이터가 없어, 컴포넌트가 렌더링 되지 않은 상태로 유지되다가, 응답 이후에 렌더링 되면서 레이아웃 시프트가 발생하는 문제도 있었습니다.

위 두 가지 문제로 인한 레이아웃 시프트를 개선해 보고자, 첫 번째로 Dynamic Import로 인해 레이아웃 시프트를 일으키는 컴포넌트를 파악하고 즉시 로딩되도록 변경했습니다. 이에 따라 API 응답을 받은 후 렌더링되는 컴포넌트를 제외하고, 모든 컴포넌트가 한 번에 렌더링 되며 레이아웃 시프트가 줄었습니다.

두 번째로, 조건부 렌더링 문제를 해결하기 위해, 먼저 조건부 렌더링을 제거하였고 빈 값의 데이터로 컴포넌트를 노출하다가 API 응답을 받은 후, 다시 렌더링 하는 방식을 적용했습니다.

네트워크 속도를 Fast 3G 환경으로 제한하여 측정해 본 결과, 아래와 같이 개선 전보다 레이아웃 시프트 문제가 훨씬 개선된 것을 확인할 수 있었습니다.

결과적으로 다운로드하는 총 JavaScript 파일의 개수가 줄어들었고, 어떤 컴포넌트를 로딩해야 할지에 대한 정보가 줄어 파일의 총 용량도 소폭 감소했습니다. 결정적으로는 전체 다운로드 시간의 큰 차이 없이, 레이아웃 시프트를 줄이면서 CLS 지표가 0.359 -> 0.097로 개선되었고, 성능 점수가 73점 -> 82점으로 상승했습니다.

컴포넌트 로딩 방식 변경 중 겪은 트러블 슈팅

Dynamic Import를 제거하면서 겪었던 이슈도 하나 있었는데요, Dynamic Import를 이용한 지연 로딩과 Import를 이용한 즉시 로딩이 혼재하면서 순환 의존성(Circular dependencies) 이슈를 경험했었습니다.

순환 의존성 이슈란 두 개의 파일이 서로를 상호 참조하는 상황을 말합니다. 이에 따라, 컴포넌트가 정상적으로 렌더링 되지 않는 에러가 생겼고, 아래와 같이 순환 참조가 발생하는 부분을 분리해 문제를 해결했습니다.

GNB 모듈 지연로딩

비즈니스폼에서는 카카오 비즈니스 공통 GNB(Global Navigation Bar) 모듈을 사용하고 있습니다.

GNB 모듈은 로그인/세션 확인이 끝나고 나서야 실제로 렌더링 되는데, 확인 과정 이전에 로딩이 되면서 초기 번들 사이즈를 높이고 있었습니다. 개발자 도구에서 Performance 탭을 이용하여 확인해 본 결과, 아래 이미지와 같은 순서로 작업이 진행되는 모습을 볼 수 있었습니다.

이를 설명하면, 작업 순서에 의거해 GNB 관련 리소스를 먼저 불러오면서 다른 네트워크 요청을 블로킹하여, 비즈니스폼에서 우선순위가 높은 설문 데이터의 로딩이 늦춰지고 있었습니다. 설문 데이터 로딩이 완료되고 나서 LCP(Largest Contentful Paint)완료되기 때문에, 개선을 위해서는 설문 데이터 로딩 과정을 앞당길 필요가 있었습니다.

따라서, GNB 파일 로딩을 뒤로 미루기 위해 아래와 같이 지연로딩을 적용했고, 설문 데이터가 로딩된 후 GNB 모듈이 렌더링 되어야 하는 시점에 관련 리소스들이 로딩되도록 변경했습니다.

				
					/** 수정 전 */
import {Gnb, PHASE as GNB_PHASE} from '@kakao/biz-gnb';
...

/** 수정 후 */
const loadGnb = async () => {
  ...
  const {Gnb, PHASE: GNB_PHASE} = await import('@kakao/biz-gnb');
  ...
};
  
onMounted(() => {
  loadGnb();
});
				
			

그 결과, 네트워크 요청 순서가 아래 이미지와 같이 변경될 수 있었습니다.

결과적으로 GNB 파일이 초기 번들에 포함되지 않게 되었고, 설문 데이터를 로딩한 후에 GNB 관련 리소스를 로딩하고 렌더링 하게 되면서 LCP 지표가 개선되었습니다.

이미지 최적화

비즈니스폼에서 설문 문항 설계 시, 이미지를 위와 같이 다양하게 사용하여 여러 문항을 만들 수 있습니다. 기존에는 현재 화면에서 필요하지 않은 이미지를 모두 로딩하고 있었고, 이미지 용량과 관련한 조치도 되어있지 않아 성능에 영향을 미칠 수 있었습니다. 따라서 이러한 문제를 해결하기 위해 몇 가지 작업을 진행했습니다.

이미지 레이지 로딩

이미지 레이지 로딩을 구현하기 위해 사용자가 보고 있는 화면과 실제 요소들의 교차점을 확인하여 요소가 화면에 들어왔는지를 판단해 주는 IntersectionObserver를 활용했습니다. 이를 이용해 페이지 진입 시, 페이지 내 모든 이미지를 한 번에 로딩하는 대신, data-src 속성에 이미지 정보를 담고 있다가 이미지가 사용자가 보고 있는 화면 영역에 진입할 때 이미지 정보를 src 속성으로 이동시켜 해당 이미지를 로딩하도록 처리했습니다.

또한, Vue에는 요소에 디렉티브라는 속성을 추가해 요소를 조작할 수 있도록 하는 기능이 있는데요, 저희도 이 기능을 이미지 레이지 로딩에 사용하기 위해 custom vue directive를 생성한 후, 이미지 레이지 로딩이 필요한 img 요소들에 편리하게 적용했습니다.

				
					/** Intersection Observer를 이용해 레이지 로딩 구현 및 custom vue directive 생성 */
const imageLazyLoad = {
  inserted: (element: HTMLElement) => {
    const loadImage = () => {
      if (element instanceof HTMLImageElement) {
        element.src = element.dataset.src || '';
      }
    };

    const setIntersectionObserver = () => {
      const options = {
        once: true,
        rootMargin: '0px',
        threshold: 0,
      };
      /** Intersection Observer 세팅 */
      observeIntersection(element, loadImage, options);
    };

    setIntersectionObserver();
  },
};
/** Vue directive 등록 */
Vue.directive('img-lazy-load', imageLazyLoad);
				
			
				
					/** 컴포넌트 내 이미지 요소에서 레이지 로딩 custom directive 사용 예시 */
<img
  v-img-lazy-load
  :data-src="imgUrl"
  class="img"
  alt=""
>
				
			

이미지 사이즈 최적화

이미지 사이즈 최적화를 위해, 사용자가 등록한 이미지들을 서비스 내부에서 압축하여 제공하기로 했습니다. 이미지 사이즈를 최적화하기 위해서 imagemin 라이브러리와 함께 사용할 수 있는 imagemin-pngquant 플러그인을 사용해 이미지를 압축했습니다.

이미지를 압축하는 데에는 손실 압축과 무손실 압축이 있습니다. 손실 압축은 특정 이미지 정보를 누락시켜 파일 크기를 줄이는 방법이고, 무손실 압축은 정보 손실을 허용하지 않고 압축시키는 방법입니다.

비즈니스폼에서는 손실 압축을 하는 imagemin-pngquant를 사용하여 이미지 크기를 줄이는 방법을 선택했습니다. 그 근거로는, ‘웹 성능 최적화 기법’ 도서 내용 중 사람이 품질 저하를 눈치채기 쉽지 않은 압축 범위가 75-100% 의 사이라는 내용을 참고했으며, imagemin-pngquant 플러그인은 압축 이미지 품질 설정이 가능하기 때문에, 아래와 같이 압축률을 최소 85% 최대 90%로 설정하여 압축했습니다.

				
					imagePathList.forEach(async imagePath => {
  await imagemin([imagePath], {
    destination: imagePath,
    plugins: [
      imageminPngquant({
        options: {
          quality: [0.85, 0.9],
        },
      }),
    ],
  });
});
				
			

그 결과, 이미지 압축을 통해, 비즈니스폼 메인 페이지인 목록 페이지 기준 413KB였던 이미지 크기를 128KB로 줄여 로딩 속도를 개선할 수 있었습니다.

마치며

지금까지 비즈니스폼 서비스의 성능을 개선하기 위해 적용한 개선점과 그 과정에서 겪은 경험에 대해 공유드렸습니다.

개선 이전에는 평균 성능 지표가 60점대였지만, 현재는 85~93점 수준으로 안정적으로 유지되고 있으며, 팀의 내부 기준과 목표에도 도달했습니다.

전체적인 내용을 요약하면 아래와 같습니다.

 

[ 초기 로딩속도 향상 ]

  • 지연 로딩을 이용해 불필요하거나 병목을 일으키는 번들 분리
  • 미사용 CSS 제거
  • 이미지 레이지 로딩
  • 이미지 사이즈 최적화

 

[ 네트워크 병목 개선 ]

  • html 인라인 스크립트 사용
  • 특정 모듈 지연 로딩

 

[ 레이아웃 시프트 개선 ]

  • 동적으로 로딩되던 컴포넌트를 상황에 맞게 즉시 로딩되도록 변경

 

위에서 소개해 드린 성능 개선 주제들이 어느 프로젝트에서든지 적용할 수 있는 개선 방법은 아니겠지만, 독자분들이 웹 서비스 성능을 개선하실 때, 하나의 참고 자료가 될 수 있었으면 좋겠습니다.

마지막으로 함께 성능 작업을 진행하며 고생해 주신 lumi.kim, john.222, mark.down에게도 감사하다는 말씀을 전하며 글을 마무리하겠습니다.

감사합니다.

참고

📚 관련 글 목록:

카카오톡 공유 보내기 버튼

Latest Posts