[Next.js] 웹 성능 향상을 위해 First Load JS 용량 줄이기

강경서·2024년 6월 3일
0
post-thumbnail

Intro

진행중인 사이드 프로젝트의 기능을 추가하는 과정에서 Firebase를 사용하여 인증 기능을 추가했습니다. 기능을 추가하고 배포 전에 빌드를 테스트하는 과정에서 First Load JS의 용량이 커서 웹의 초기 로딩 시간이 늘어나는 문제가 생겨 First Load JS의 용량을 줄여 이를 해결하고자 하였습니다.


First Load JS

First Load JS는 웹 애플리케이션을 처음으로 로드할 때 필요한 JavaScript 파일을 의미합니다. 이는 사용자가 웹 사이트를 처음 방문할 때 필요한 JavaScript 리소스를 가리키며, 전체 애플리케이션의 초기 렌더링 및 실행에 필요한 로직이 포함됩니다.


First Load JS의 용량이 커진 이유

First Load JS 가 커지는 경우는 다양합니다. 단순히 코드가 축적 되면서 커질 수 도 있고
이미지 및 기타 리소스가 추가할 때에도 커질 수 있습니다. 그리고 의존성 추가, 즉 라이브러리를 사용하면 커질 수 있습니다.

@next/bundle-analyzer

next/bundle-analyzer를 사용하면 웹을 빌드 후 각 모듈의 번들링된 사이즈를 확인할 수 있습니다. 이를 이용하여 현재 어떤 모듈들이 First Load JS에 큰 비중을 차지하는지 알 수 있습니다.

진행중인 사이드 프로젝트에 next/bundle-analyzer를 사용하여 모듈의 번들링된 사이즈를 확인해 보니 웹의 인증 기능 및 데이터베이스를 관리하는 Firebase 가 큰 용량을 차지하고 있었습니다.

Next.jsApp Router를 사용하면 서버에서 Data Fetching이 가능하여 Firebase를 사용하여 데이터를 가져오는 것은 First Load JS의 용량과 상관이 없었습니다.
하지만 클라이언트에서 사용되는 Firebase와 같은 라이브러리들은 First Load JS의 용량에 영향을 미치므로 잘 확인해야합니다.


First Load JS의 용량 줄이기

First Load JS의 용량 줄이기 위해서는 모듈에 Dynamic Import를 적용하거나, 모듈이 사용되는 컴포넌트에 Next.jsLazy Loading을 적용하여 용량을 줄일 수 있습니다.

Dynamic Import

Dynamic Import(동적 불러오기)는 기존의 정적으로 모듈과 달리 필요한 시점에 모듈을 가져올 수 있습니다.

import('./module.js')
  .then((module) => console.log(module))
  .catch((err) => console.log(err));

프로젝트에 적용

import { onAuthStateChanged } from 'firebase/auth';
import { auth } from '@/libs/server/firebase';

useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, user => {
      if (user) {
        setUser(user);
      } else {
        setUser(null);
      }
      setIsUserLoading(false);
    });

    return () => {
      unsubscribe();
    };
  }, []);

기존의 코드는 정적 가져오기를 통해 Firebase로 부터 onAuthStateChanged라는 로그인 상태를 확인하는 함수와 인증 객체인 auth를 가져와서 사용했습니다.

 useEffect(() => {
    const loadAuth = async () => {
      const { onAuthStateChanged } = await import('firebase/auth');
      const { auth } = await import('@/libs/server/firebase');

      const unsubscribe = onAuthStateChanged(auth, user => {
        setUser(user);
        setIsUserLoading(false);
      });
      return () => unsubscribe();
    };

    loadAuth();
  }, []);

이를 Dynamic Import를 사용하여 동적으로 불러와서 사용함으로 초기 랜더링시 모듈을 가져오지 않고 필요할때 가져오게 되므로써 First Load JS의 용량을 줄일 수 있습니다.

Lazy Loading

Next.jsLazy Loading을 구현하는 방법 중 하나인 next/dynamicReact.lazy()Suspense를 이용한 Dynamic Imports입니다. React.lazy()Suspense을 직접 사용하지 않고 더 간편하게 컴포넌트의 동적 가져오기가 가능합니다.

const ComponentA = dynamic(() => import('../components/A'))

프로젝트에 적용

import React, { useState } from 'react';
import { BrandType } from '@/types';
import BrandList from './BrandList';
import EditBrandModal from './EditBrandModal';
import AddBrandModal from './AddBrandModal';

interface BrandContainerProps {
  brands: BrandType[];
}

const BrandContainer = ({ brands }: BrandContainerProps) => {
  const [selectedBrand, setSelectedBrand] = useState<BrandType | null>(null);
  const [isAddBrandModalOpen, setIsAddBrandModalOpen] = useState(false);
  const [isEditBrandModalOpen, setIsEditBrandModalOpen] = useState(false);
  return (
        <div className='mx-auto mt-20 min-h-[700px] w-[600px] border-2'>
          <div className='relative bg-black/80 py-2 text-center'>
            <span className='text-white'>브랜드</span>
            <button
              onClick={() => setIsAddBrandModalOpen(true)}
              className='absolute right-4 top-[50%] translate-y-[-50%] rounded-sm bg-emerald-600 px-2 py-[2px] text-sm text-white hover:bg-emerald-700'
            >
              추가하기
            </button>
          </div>
          <BrandList
            brands={brands}
            setSelectedBrand={setSelectedBrand}
            setIsEditBrandModalOpen={setIsEditBrandModalOpen}
          />
          {isAddBrandModalOpen && (
            <AddBrandModal
              setIsAddBrandModalOpen={setIsAddBrandModalOpen}
              rule={rule}
            />
          )}
          {isEditBrandModalOpen && selectedBrand && (
            <EditBrandModal
              brand={selectedBrand}
              setIsEditBrandModalOpen={setIsEditBrandModalOpen}
              rule={rule}
            />
          )}
        </div>
  );

위의 컴포넌트는 데이터를 수정 및 추가하는 컴포넌트를 모달형식으로 관리하고 있습니다. 모달 컴포넌트들은 Firebase를 이용하여 데이터를 관리하고 해당 모듈을 초기에 불러오고 있습니다.

import React, { useState } from 'react';
import { BrandType } from '@/types';
import BrandList from './BrandList';
import dynamic from 'next/dynamic';

interface BrandContainerProps {
  brands: BrandType[];
}

const AddBrandModal = dynamic(() => import('./AddBrandModal'));
const EditBrandModal = dynamic(() => import('./EditBrandModal'));

const BrandContainer = ({ brands }: BrandContainerProps) => {
  const [selectedBrand, setSelectedBrand] = useState<BrandType | null>(null);
  const [isAddBrandModalOpen, setIsAddBrandModalOpen] = useState(false);
  const [isEditBrandModalOpen, setIsEditBrandModalOpen] = useState(false);
  return (
        <div className='mx-auto mt-20 min-h-[700px] w-[600px] border-2'>
          <div className='relative bg-black/80 py-2 text-center'>
            <span className='text-white'>브랜드</span>
            <button
              onClick={() => setIsAddBrandModalOpen(true)}
              className='absolute right-4 top-[50%] translate-y-[-50%] rounded-sm bg-emerald-600 px-2 py-[2px] text-sm text-white hover:bg-emerald-700'
            >
              추가하기
            </button>
          </div>
          <BrandList
            brands={brands}
            setSelectedBrand={setSelectedBrand}
            setIsEditBrandModalOpen={setIsEditBrandModalOpen}
          />
          {isAddBrandModalOpen && (
            <AddBrandModal
              setIsAddBrandModalOpen={setIsAddBrandModalOpen}
              rule={rule}
            />
          )}
          {isEditBrandModalOpen && selectedBrand && (
            <EditBrandModal
              brand={selectedBrand}
              setIsEditBrandModalOpen={setIsEditBrandModalOpen}
              rule={rule}
            />
          )}
        </div>

이를 next/dynamic을 사용하여 컴포넌트들을 동적으로 가져옵니다. 이는 모달이 열리는 상황이 되어야 모듈을 가져오므로 First Load JS의 용량을 줄일 수 있습니다.


결과

모듈 및 컴포넌트들을 동적 가져오기를 통해 코드 분할을 한 결과, 눈에 띄게 First Load JS의 용량을 줄일 수 있었습니다.


📝 후기

수치로 확인할 수 있는 성능 향상은 정말 즐거운 일입니다. 이러한 성능 개선은 사용자들에게 더 나은 경험을 제공하니 이후에도 꼭 신경쓰면서 코드를 작성해야겠습니다.
Next.js의 서버 컴포넌트와 클라이언트 컴포넌트를 이해하는데도 큰 도움이 되었습니다.


🧾 Reference

profile
기록하고 배우고 시도하고

0개의 댓글