Skip to content

코드 스플릿팅

SeongYeon edited this page Dec 2, 2024 · 1 revision

이 글은 사이트 배포를 한 뒤 배포 사이트에서 측정해본 light house 의 성능을 확인 한 뒤 실망스러운 결과를 얻어서 light-house 의 성능을 올리기 위한 방법 중 하나로 코드 스플릿팅을 진행한 과정이다.

Lighthouse 란

Lighthouse는 구글에서 제공하는 웹 페이지 품질 개선을 위한 오픈 소스로 자동화 도구이다. Lighthouse는 웹 페이지의 성능, 접근성, 권장사항 등을 종합적으로 분석하고 점수를 매겨주는 도구로 웹 페이지의 성능을 측정하는 지표로 사용된다. Lighthouse 는 사이트의 성능, 접근성, SEO 등에 대한 전반적인 진단을 제공해준다.

주요 카테고리 분석

  • Performance(성능): 페이지 로딩 속도, 최적화 상태 등을 측정
  • Accessibility (접근성): 웹 접근성 기준 준수 여부 검사
  • Best Practices(모범 사례): 웹 개발 모범 사례 준수 여부 확인
  • SEO(검색엔진 최적화): 검색엔진에서의 노출도를 높이기 위한 요소들 분석

초기 Lighthouse 결과

image

배포 사이트 초기의 Lighthouse 결과로 성능 점수 64점에 매우 실망했었다….. 라이트하우스의 성능을 높이기 위해 코드 스플릿팅 방식을 시도해봤다.

Code Splitting 이란 ?

코드 스플릿팅은 애플리케이션의 코드를 여러 개의 번들로 나누어 필요한 시점에 동적으로 로드하는 기술이다. 웹 애플리케이션이 커질수록 Javascript 번들 크키도 함께 커지게 되는데, 이는 초기 로딩 시간을 증가시키는 원인이 된다. 코드 스플릿팅을 통해 이러한 문제를 해결할 수 있다.

Code Splitting 장점

  1. 초기 로딩 시간 단축
    1. 필요한 코드만 먼저 다운로드하여 초기 페이지 로딩 속도를 개선한다
    2. 즉 사용자가 실제로 방문하는 페이지의 코드만 로드한다
  2. 리소스 효율성
    1. 필요하지 않은 코드는 로드하지 않아 메모리 사용 최적화
    2. 네트워크 대역폭 절약

React 에서의 나의 코드 스플릿팅 기준

React 에서는 주로 React.lazy()Suspense 를 사용하여 코드 스플릿팅을 구현한다. 내 기준 코드 스플릿팅을 시도해볼 법 한 페이지들은 다음과 같았다.

라우트 기반 스플릿팅

  • 페이지 단위로 코드를 분할한다. 사용자 접속 시 모든 페이지를 한번에 가져올 필요는 없기에 코드 스플릿팅에 적합하다 생각했다.
  • 즉 해당 페이지 방문 시에만 관련 코드를 로드하면 된다

컴포넌트 기반 스플릿팅

  • 컴포넌트에 조건부 렌더링되는 컴포넌트에 코드 스플릿팅을 적용하면 매우 적절한 것이라 예상했다
  • 우리의 프로젝트에서 위에 사례에 적합한 예시는 사이드 바 컴포넌트, 거래 페이지의 주문 관련 폼이 존재했다.

Router

스플릿팅 전

function Router() {
	return (
		<Suspense>
			<Routes>
				<Route element={<Layout />}>
					<Route path="/" element={<Home />} />
					<Route path="/account" element={<Account />}>
						<Route path="balance" element={<Balance />} />
						<Route path="history" element={<History />} />
						<Route path="wait_orders" element={<WaitOrders />} />
					</Route>
					<Route path="/trade/:market" element={<Trade />} />
				</Route>
				<Route path="/auth/callback" element={<Redricet />} />
				<Route path="*" element={<NotFound />} />
			</Routes>
		</Suspense>
	);
}

위 코드는 코드 스플릿팅 전 Router 컴포넌트로 모든 컴포넌트가 즉시 import 되어 있어 초기 번들 크기를 불필요하게 증가시킨다.

스플릿팅 후

const Layout = lazy(() => import('@/pages/layout/Layout'));
const Home = lazy(() => import('@/pages/home/Home'));
const Account = lazy(() => import('@/pages/account/Account'));
const Balance = lazy(() => import('@/pages/account/balance/Balance'));
const History = lazy(() => import('@/pages/account/history/History'));
const WaitOrders = lazy(() => import('@/pages/account/waitOrders/WaitOrders'));
const Trade = lazy(() => import('@/pages/trade/Trade'));
const Redricet = lazy(() => import('@/pages/auth/Redirect'));
const NotFound = lazy(() => import('@/pages/not-found/NotFound'));

function Router() {
	return (
		<Suspense>
			<Routes>
				<Route element={<Layout />}>
					<Route path="/" element={<Home />} />
					<Route path="/account" element={<Account />}>
						<Route path="balance" element={<Balance />} />
						<Route path="history" element={<History />} />
						<Route path="wait_orders" element={<WaitOrders />} />
					</Route>
					<Route path="/trade/:market" element={<Trade />} />
				</Route>
				<Route path="/auth/callback" element={<Redricet />} />
				<Route path="*" element={<NotFound />} />
			</Routes>
		</Suspense>
	);
}

위와 같이 lazy import 를 적용하면 첫 페이지 로드 시 필요한 컴포넌트만 다운로드 할 수 있으며 초기 번들 크기가 감소하여 첫 페이지 로딩 속도가 향상될 수 있다는 장점이 존재한다.

사이드 네비게이션

스플릿팅 전

import Interest from '@/components/sidebar/Interest';
import MyInvestment from '@/components/sidebar/MyInvestment';
import Realtime from '@/components/sidebar/Realtime';
import RecentlyViewed from '@/components/sidebar/RecentlyViewed';
import { SideBarCategory } from '@/types/category';

type SideDrawerProps = {
	isOpen: boolean;
	activeMenu: SideBarCategory;
};

function SideDrawer({ isOpen, activeMenu }: SideDrawerProps) {
	const activeMenuComponent = {
		MY_INVESTMENT: <MyInvestment />,
		INTEREST: <Interest />,
		RECENTLY_VIEWED: <RecentlyViewed />,
		REALTIME: <Realtime />,
	};

	return (
		<div className="overflow-hidden bg-gray-100">
			<div
				className={`
            h-full
            bg-gray-100
            transition-all duration-300 ease-in-out
			border-l border-gray-400 border-solid
            ${isOpen ? 'translate-x-0 w-80' : 'translate-x-full w-0'}
          `}
			>
				{activeMenu && activeMenuComponent[activeMenu]}
			</div>
		</div>
	);
}

export default SideDrawer;

기존 사이드 바 컴포넌트들의 요소들은 사용자가 선택을 하지 않은 요소들도 즉시 import 되어 불필요한 컴포넌트들의 로드들이 존재했다.

스플릿팅 후

import { lazy, Suspense } from 'react';
import { SideBarCategory } from '@/types/category';

const MyInvestment = lazy(() => import('@/components/sidebar/MyInvestment'));
const Interest = lazy(() => import('@/components/sidebar/Interest'));
const RecentlyViewed = lazy(
	() => import('@/components/sidebar/RecentlyViewed'),
);
const Realtime = lazy(() => import('@/components/sidebar/Realtime'));

function SideDrawer({ isOpen, activeMenu }: SideDrawerProps) {
	const activeMenuComponent = {
		MY_INVESTMENT: <MyInvestment />,
		INTEREST: <Interest />,
		RECENTLY_VIEWED: <RecentlyViewed />,
		REALTIME: <Realtime />,
	};

	return (
		<div className="overflow-hidden bg-gray-100">
			<div
				className={`
          h-full
          bg-gray-100
          transition-all duration-300 ease-in-out
          border-l border-gray-400 border-solid
          ${isOpen ? 'translate-x-0 w-80' : 'translate-x-full w-0'}
        `}
			>
				<Suspense>{activeMenu && activeMenuComponent[activeMenu]}</Suspense>
			</div>
		</div
	);
}

export default SideDrawer;

위와 같이 lazy import 를 적용하여 각각 독립적으로 로드되어 필요한 시점에만 데이터를 가져오도록 개선하였다.

매수/매도/대기 탭

스플릿팅 전

import OrderBuyForm from '@/pages/trade/components/order_form/OrderBuyForm';
import OrderSellForm from '@/pages/trade/components/order_form/OrderSellForm';
import OrderWaitForm from '@/pages/trade/components/order_form/OrderWaitForm';
import NotLogin from '@/components/NotLogin';

type CreateOrderTabProsp = {
	currentPrice: number;
	selectPrice: number | null;
};

export const createOrderTabs = ({
	currentPrice,
	selectPrice,
}: CreateOrderTabProsp) => {
	return [
		{
			value: '구매',
			id: 'buy',
			activeColor: 'text-red-500',
			component: (
				<OrderBuyForm currentPrice={currentPrice} selectPrice={selectPrice} />
			),
			notLogin: <NotLogin size="md" />,
		},
		{
			value: '판매',
			id: 'sell',
			activeColor: 'text-blue-600',
			component: (
				<OrderSellForm currentPrice={currentPrice} selectPrice={selectPrice} />
			),
			notLogin: <NotLogin size="md" />,
		},
		{
			value: '대기',
			id: 'wait',
			activeColor: 'text-green-500',
			component: <OrderWaitForm />,
			notLogin: <NotLogin size="md" />,
		},
	] as const;
};

스플릿팅 후

import { lazy, Suspense } from 'react';
import NotLogin from '@/components/NotLogin';

const OrderBuyForm = lazy(
	() => import('@/pages/trade/components/order_form/OrderBuyForm'),
);
const OrderSellForm = lazy(
	() => import('@/pages/trade/components/order_form/OrderSellForm'),
);
const OrderWaitForm = lazy(
	() => import('@/pages/trade/components/order_form/OrderWaitForm'),
);

type CreateOrderTabProsp = {
	currentPrice: number;
	selectPrice: number | null;
};

export const createOrderTabs = ({
	currentPrice,
	selectPrice,
}: CreateOrderTabProsp) => {
	return [
		{
			value: '구매',
			id: 'buy',
			activeColor: 'text-red-500',
			component: (
				<Suspense>
					<OrderBuyForm currentPrice={currentPrice} selectPrice={selectPrice} />
				</Suspense>
			),
			notLogin: <NotLogin size="md" />,
		},
		.... 아래 코드는 동일
	] as const;
};

기존 코드와 달리 주문 관련 폼(매수 폼, 매도 폼, 대기 폼) 을 분리하여 사용자가 선택한 탭의 내용만 로드하도록 개선하였다.

성능 개선 결과

image

성능 점수

  • 64점 → 89점 (25점 상승 🚀)

FCP

  • 2.0 초 → 1.3초 (0.7초 감소)
  • FCP 개선을 통해 사용자가 첫 컨텐츠를 더 빨리 볼 수 있다

LCP

  • 7.8초 → 1.7초 (6.1초 감소)
  • LCP가 크게 개선되어 페이지의 주요 컨텐츠가 훨씬 빠르게 로드된다.

💻 개발 일지

💻 공통

💻 FE

💻 BE

🙋‍♂️ 소개

📒 문서

☀️ 데일리 스크럼

🤝🏼 회의록

Clone this wiki locally