진행중인 사이드 프로젝트의 기능을 추가하는 과정에서 Firebase
를 사용하여 인증 기능을 추가했습니다. 기능을 추가하고 배포 전에 빌드를 테스트하는 과정에서 First Load JS
의 용량이 커서 웹의 초기 로딩 시간이 늘어나는 문제가 생겨 First Load JS
의 용량을 줄여 이를 해결하고자 하였습니다.
First Load JS
는 웹 애플리케이션을 처음으로 로드할 때 필요한 JavaScript
파일을 의미합니다. 이는 사용자가 웹 사이트를 처음 방문할 때 필요한 JavaScript
리소스를 가리키며, 전체 애플리케이션의 초기 렌더링 및 실행에 필요한 로직이 포함됩니다.
First Load JS
가 커지는 경우는 다양합니다. 단순히 코드가 축적 되면서 커질 수 도 있고
이미지 및 기타 리소스가 추가할 때에도 커질 수 있습니다. 그리고 의존성 추가, 즉 라이브러리를 사용하면 커질 수 있습니다.
next/bundle-analyzer
를 사용하면 웹을 빌드 후 각 모듈의 번들링된 사이즈를 확인할 수 있습니다. 이를 이용하여 현재 어떤 모듈들이 First Load JS
에 큰 비중을 차지하는지 알 수 있습니다.
진행중인 사이드 프로젝트에 next/bundle-analyzer
를 사용하여 모듈의 번들링된 사이즈를 확인해 보니 웹의 인증 기능 및 데이터베이스를 관리하는 Firebase
가 큰 용량을 차지하고 있었습니다.
Next.js
의App Router
를 사용하면 서버에서Data Fetching
이 가능하여Firebase
를 사용하여 데이터를 가져오는 것은First Load JS
의 용량과 상관이 없었습니다.
하지만 클라이언트에서 사용되는Firebase
와 같은 라이브러리들은First Load JS
의 용량에 영향을 미치므로 잘 확인해야합니다.
First Load JS
의 용량 줄이기 위해서는 모듈에 Dynamic Import
를 적용하거나, 모듈이 사용되는 컴포넌트에 Next.js
의 Lazy Loading
을 적용하여 용량을 줄일 수 있습니다.
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
의 용량을 줄일 수 있습니다.
Next.js
의 Lazy Loading
을 구현하는 방법 중 하나인 next/dynamic
은 React.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의 서버 컴포넌트와 클라이언트 컴포넌트를 이해하는데도 큰 도움이 되었습니다.