웹 사이트 최적화와 성능 개선, 꼭 해야하나요?
당연한 소리를 한 번 해봤습니다. 사용자에게 최적의 경험을 제공하기 위해선 낮은 성능의 웹사이트는 문제가 됩니다. 성능 개선은 필수죠.
Mapbox를 이용한 웹페이지를 구현했습니다.
충남 미술사의 정보를 보여주는 프로젝트를 진행했습니다. 해당 프로젝트가 어떤 웹 페이지인지 어떤 기능이 있는지에 대해서는 추후에 회고 글로 가져오도록 하겠습니다.
페이지의 기능을 구현하고 나니 인터랙션이 많은 페이지이다보니 데이터가 많아졌을때 버벅이는 현상이 나타나진 않을까? 라는 걱정이 생기기 시작했습니다. 성능 측정을 위해서 Lighthouse를 이용했습니다.


성능이 조금 떨어지는 페이지를 구현했지만, 저에게 이젠 올라갈 일만 남았다고 생각하고 성능 개선을 위해 하나씩 살펴보자구요!


*DCL: 스크립팅을 구현할 준비가 완료된 시점*
performance를 확인해보면 첫번째 컨텐츠가 나올때까지의 시간(FCP), 가장 큰 컨텐츠가 나오는데까지 걸리는 시간(LCP)를 확인할 수 있습니다.
지금 확인을 해봤을때 HTML을 로드하고 CSS 파일을 불러온 후 페이지가 렌더링되는데까지의 시간이 꽤 오래걸리는 것을 볼 수 있습니다.
폰트를 가져오는 시간이 너무 길다.

Network 를 먼저 확인해보면 폰트를 불러오는데 183.72ms가 소요되는 것을 볼 수 있습니다.
Duration: 183.72ms
Queuing and connecting: 6.62ms
Request sent and waiting: 5.34ms
Content downloading: 159.11ms
Wating on main thread: 12.65ms지금 프로젝트에서는 localFont를 사용하고 있습니다. 그런데 왜 외부에서 불러오는 폰트도 아니고 로컬에서 불러오는데 오래걸리는 걸까요? 그 이유는 variable 을 사용하고 있었기 때문이었습니다.
variable은 모든 weight를 포함하고 있기 때문에 파일 자체의 크기가 6.4MB로 크키가 매우 큰 상태였습니다.
그래서 수정한 부분은 두 가지입니다.
- 압축률이 높은 woff2 타입의 폰트를 사용하는 것.
- 사용하는 weight만 가져오는 것.


이제 모든 폰트를 가지고 와도 50ms 정도의 시간밖에 걸리지 않는 것을 확인할 수 있습니다.
Render Blocking Resources 시간 줄이기
브라우저가 페이지를 화면에 그리기 전에 반드시 다운로드하고 처리해야하는 리소스들에서 시간이 오래 걸린다면 어떻게 될까요? 화면이 빈 화면으로 유지되기 때문에 사용자는 빈 화면에서 기다려야하는 상황이 발생합니다.
사용자 경험:
┌─────────────────────────────────────┐
│ 0ms - 210ms: 흰 화면 (blocking) │
│ 210ms 이후: 콘텐츠가 보이기 시작 │
└─────────────────────────────────────┘Lighthouse가 측정했을 때 '너무 오래걸리는데?' 라고 판단한 파일은 2개였습니다.
- CSS Chunk (210ms)
- MapBox GL CSS (90ms)
사용하고 있는 CSS 파일을 확인했을때 Shadcn/ui를 사용하면서 자동으로 추가된 변수들이 있는데, 사용하지 않는 변수들을 모두 제거해주었습니다.

또한 CSS 최적화를 위해 optimizeCss를 true로 설정해주었습니다. true로 설정하게 되면 다음과 같은 방식으로 변경합니다.
// next.config.js
module.exports = {
experimental: {
optimizeCss: true, // CSS 최적화 활성화
},
};

FCP 시간이 줄어들면서 성능 점수도 올랐습니다!
이제는 대충봐도 LCP에 문제가 있어보입니다.
LCP의 시간이 길다.


- 불필요한 폰트파일 삭제
- 개발 환경에서 console.log를 많이 찍고 있는 상황이었기 때문에 production인 경우엔 콘솔이 출력되지 않도록 조건부 처리
- LCP 이미지 priority 추가


JS 번들사이즈가 줄어든 것을 확인할 수 있습니다.
초기 페이지 로드 시 사용되지 않는 JavaScript가 너무 많다.

이렇게 큰 용량의 청크는 높은 확률로 Mapbox GL JS일거라고 생각했습니다.
그래서 Mapbox를 사용하는 컴포넌트를 동적으로 import 해오도록 수정했습니다.

하지만 같은 문제가 동일하게 나타났습니다.
dynamic import를 쓰고 있는데도 초기 번들에 406KiB가 잡힌다니? 👉🏼 그 말은 즉, 다른 곳에서 정적 import를 하고 있다!
그래서 import mapboxgl from 'mapbox-gl' 을 사용하는 곳이 있는지 확인해봤습니다.

잡았다 이놈!
interface UseDataMarkersProps {
mapRef: RefObject<mapboxgl.Map | null>; //⚠️ 타입정의를 위해 mapboxgl을 import 하고 있음!
isMapLoaded: boolean;
//...
}하지만 모든 훅은 Map 컴포넌트에서 사용하고 있는데?
page.tsx
└─ dynamic(() => import('Map')) ← 분리된 청크
└─ Map.tsx
└─ useMapInit()
└─ import mapboxgl ← Map 청크에 포함됨 ✓이 구조면 mapbox-gl은 Map 청크가 로드될 때 같이 로드되기 때문에 초기 번들에는 들어가지 않습니다. 그럼 대체 뭐가 문제일까요..
번들 사이즈 분석을 해보자.
이 시점에서 의문점이 하나 생기는데요,
1. Map 컴포넌트에서 mapboxgl을 import 하고 있다.
2. Map 컴포넌트는 lazy loading을 적용했다.
---> 그런데 왜 초기 번들에 mapboxgl이 포함되는걸까?무슨 문제인지 코드로만은 확인이 어려워 번들 사이즈 분석 라이브러리 @next/bundle-analyzer를 이용해 분석해봤습니다.
import type { NextConfig } from 'next';
import bundleAnalyzer from '@next/bundle-analyzer';
// ⚠️ 추가
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
const nextConfig: NextConfig = {
output: 'export',
images: { unoptimized: true },
experimental: {
optimizeCss: true,
},
// 프로덕션 빌드 시 console.log 자동 제거
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
};
export default withBundleAnalyzer(nextConfig); // ⚠️ 추가주범은 따로 있었다.


문제라고 생각했던 mapbox-gl이 별도 청크에 잘 분리되어 있었습니다. 그리고 이건 dynamic import 덕분에 초기 로드에는 안 들어갑니다. 진짜 문제는 data.json이었습니다. page-fa0e3041b2e6bd29.js 안에 거대하게 들어가 있었습니다.
현재 구조는 data.json을 모든 컴포넌트에서 import해서 사용하고 있어서 번들에 무조건 포함되는 문제가 있었습니다. 추후에 data.json의 크기는 더 커지기 때문에 런타임에 한번만 로드하는 로직으로 수정했습니다.
- store에서 데이터 관리
- 앱 초기화 시 한 번만 로드
- 각 훅과 컴포넌트, 유틸에서는 store에서 가져다 쓰기
이렇게 되면 data.json은 번들에서는 제외되고 런타임에 한번만 fetch하게 됩니다. 빌드를 해보면?

304KB에서 203KB로 줄어들었습니다.
CLS (Cumulative Layout Shift)
CLS는 페이지 로딩 중 예기치않게 요소가 움직이는 정도를 측정하는 Core Web Vitals 지표입니다.

| 점수 | 등급 | 상태 |
|---|---|---|
| 0 ~ 0.1 | Good | 양호 |
| 0.1 ~ 0.25 | Needs Improvement | 개선 필요 |
| 0.25+ | Poor | 나쁨 |
0.172점이라면 개선이 필요한 점수입니다. 우선 왜 CLS가 발생하는가? 를 먼저 확인해봐야할 것 같습니다.
- 지도 초기화 Mapbox가 로드되기 전에 캔버스 크기가 0이거나 다르다.
- 지도가 로드되면서 캔버스 크기가 변경된다.
페이지 로드 시작
↓
컨테이너: 빈 공간
↓
Mapbox 로드 완료
↓
캔버스가 갑자기 전체 크기로 확장 ← 💥 Layout Shift 발생!즉, mapbox가 그려질 컨테이너가 고정적이지 않다는 얘기인데요, 제가 작성한 코드를 확인해보겠습니다.

flex-1은 다른 요소들이 렌더링된 후 남은 공간을 계산합니다. 이 과정에서 Mapbox 캔버스 크기가 변경됩니다.
하지만 고정적인 컨테이너 높이를 주기엔 디바이스마다 캔버스를 그릴 수 있는 크기가 유동적이기 때문에 flex-1을 수정하는건 어렵습니다. 이때 CSS 속성 중 contain을 사용하면 CLS 문제를 해결할 수 있습니다.
contain: strict가 무슨 역할을 하냐면요
[Mapbox 캔버스 크기 변경]
↓
[contain: strict] ← 🛡️ "이 안에서 일어나는 일은 바깥에 영향 없음!"
↓
[부모 레이아웃 변경 없음] ← ✅ CLS 0!
CLS가 등급이 올라간 것을 확인할 수 있습니다.
50점대에서 73점까지

최종적으로 Lighthouse 검사를 했을때 점수는 73점이었습니다. Performance 측면에선 50점대에서 73점까지 향상되었고 Best Practices는 100점을 달성했습니다. 성능을 향상시키기 위해 코드를 리팩토링하면서 다른 점수들도 같이 올라갔습니다.

성능측면에선 LCP 문제를 해결해야하는 부분들이 남아있는데요. 여러 방법을 시도했지만 아직 완벽하지 않습니다. Mapbox를 어떻게 최적화해야할지에 대해서도 더 고민을 해야할 것 같습니다. 성능 개선을 하면서 성능분석 툴에 대해서도 찾아보고 브라우저가 어떤 방식으로 렌더링되는지에 대해서도 다시 한번 더 공부하는 시간이 되었습니다. 이 기나긴 과정을 같이 봐주셔서 감사합니다.