---
title: '웹 서비스 성능 분석 (4)'
tags:
  - web-performance
  - frontend
published: true
date: 2025-07-19 19:20:44
description: '나도 해볼까.. 라는 생각이 든다면 바로 지금 연락주세요!!'
series: '웹 서비스 성능 분석'
seriesOrder: 4
---

## 실제 개발자 피드백

> 다음은 실제 개발자분이 보내주신 피드백입니다.

### 글이 많이 도움이 되었는지

읽는 내내 '와...' 소리가 나올 정도로 정말 많은 도움이 되었습니다.
상세하게 분석해주셔서 정말 감사드립니다.

이번에 애니메이션 라이브러리를 활용해 포트폴리오를 만들면서 처음으로 웹 성능에 대해 고민하게 되었습니다.
성능, 라이트 하우스 탭을 계속 살펴보며 자바스크립트 번들 줄이는 법 등 이런저런 문서들을 찾아 보았지만,
익숙하지 않은 내용들이라 스스로 해결하기가 어려웠습니다.

그러다가 저자님께 분석 서비스를 요청드렸는데, 정말 상세하고 전문적으로 분석에 감탄했습니다.
웹 성능 최적화를 위한 여러가지 전략 뿐만 아니라 SEO와 접근성을 고려한 원칙을 자세히 소개해주시고,
실제 적용한 예시까지도 보여주셔서 정말 많은 도움이 되었습니다.

개발하면서 제 스스로도 미심쩍고 의심이 들지만 그냥 넘길 때가 있었는데요,
특히 인트로 애니메이션을 매번 보여준 뒤, 주요 내용이 나오는 부분이 그랬습니다.
애니메이션을 강조하다가 성능과 접근성 모두를 낮추게 될 수도 있구나란 생각이 들었고,
앞으로 소개해주신 Progressive Enhancement 원칙에 부합하는 개발을 해야겠다는 생각이 들었습니다.

분석해주신 내용을 상세하게 학습하면서 하나 하나 포트폴리오에 적용해 개선해보도록 하겠습니다.
정말 최고입니다.. 진심으로 감사드립니다.

### 다른 개발자에게 이 성능 분석을 추천할 수 있을지

무조건 추천합니다. 이렇게 자세하게 성능에 대해 다루는 자료를 본 적이 없습니다.
출간되면 꼭 사서 읽어볼게요!

### 추가로 궁금하신 사항이나 보완이 필요한 부분

> 애니메이션을 구현할 때는 CSS의 트랜지션으로 구현하는 것이 라이브러리를 쓰는 것보다 더 성능에 좋은 지 궁금합니다.
> 직접 예시를 들어주신 classList.add() 메서드를 통한 추가 방식이 효과적이라고 생각하는데요,
> GSAP 같은 라이브러리도 내부적으로 최적화를 하고 있다고 알고 있는데 사실 성능 면에서 잘 모르니 자꾸 의심이 들었습니다.

단순한 트랜지션 효과의 경우에는 아무런 의존성이 없는 CSS 트랜지션이 당연히 더 유리할 것입니다. (아무런 자바스크립트의 부담이 없고, css 만 쓸 것이므로)

그러나 케이스가 복잡하거나, 사용자 인터랙션이 요구되는 상황에서는 GSAP도 마찬가지로 성능상의 손해가 느껴지지 않을 정도로 정도로 좋을 것으로 보입니다.

GSAP을 제가 많이 써본 것은 아닙니다만, (framer-motion을 주로 썼습니다) 아마도 내부적으로 GPU Path를 잘 타도록 제작되어 있을 것입니다.

> 애니메이션 성능에 대해 찾아보면서 GPU 가속화 이런 말을 많이 들었는데요, GPU가 어떻게 CSS를 처리하는지 궁금했습니다.
> 또한 애니메이션이 붙은 요소가 많아지더라도, 이런 원칙들을 잘 지키면 브라우저에서 60fps를 유지하면서 자연스럽게 실행될 수 있는 걸까요?

브라우저는 다음 조건을 만족하는 요소에 대해 자체적으로 레이어를 분리하여 GPU에서 합성 작업만 수행합니다.

https://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome/

- transform (translate, scale, rotate)
- opacity
- will-change
- position: fixed + z-index
- contain: paint

등등.. 은 이러한 속성들은 레이아웃 계산이나 페인트 단계를 건너뛰고, Composite 단계만 GPU에서 수행하게 되어 매우 빠르고 매끄럽게 처리됩니다.

따라서 애니메이션이 많이 적용해도 다음과 같은 조건이 맞다면 60fps를 달성할 수 있을 것입니다.

- GPU 친화 속성만 사용할 것 (transform, opacity)
- will-change를 남용하지 않을 것 (GPU 메모리 낭비)
- requestAnimationFrame 또는 내부 최적화 타이밍에 동기화시킬 것
- DOM 업데이트를 최소화할 것 (특히 scroll 연동일 경우, 스크롤의 속도를 DOM이 따라잡기 힘들수도 있음)

---

> 다음은 실제 개발자 분에게 전달 드린 글 입니다. 모두 공개 가능하다고 하셔서 별도 처리 없이 다 공개했습니다.

# portfolio-amber-mu-57.vercel.app 성능 분석

> **Disclaimer**

> 본 요약 내용은 제공된 portfolio-amber-mu-57.vercel.app 웹사이트 성능 분석 보고서(2025년 7월 19일 12시 기준)를 바탕으로 주요 사항을 간추린 것입니다. 분석 시점 이후 웹사이트의 업데이트나 환경 변화에 따라 실제 상태와는 차이가 있을 수 있습니다. 이번 분석은 브라우저에 배포되고 번들된 결과물을 토대로 유추하였기 때문에 실제 작성된 코드와 다소 차이가 있을 수도 있습니다.

> 제시된 성능 병목 지점 및 개선 방안은 일반적인 권장 사항이며, 실제 적용 시 효과는 웹사이트의 구체적인 구현 방식, 서버 환경, 트래픽 패턴 등 다양한 요인에 따라 달라질 수 있습니다. 본 요약은 정보 제공을 목적으로 하며, 제안된 내용을 적용함에 따른 최종적인 결정과 그 결과에 대한 책임은 웹사이트 관리 주체에게 있습니다.

> 보다 상세한 분석 내용, 방법론, 그리고 전체 컨텍스트는 원본 분석 보고서를 참고해주시기 바랍니다.

## 1. 요약

안녕하세요! [https://portfolio-amber-mu-57.vercel.app/](https://portfolio-amber-mu-57.vercel.app/) 웹사이트가 더욱 빠르고 안정적인 사용자 경험을 제공할 수 있도록, 현재 배포 중인 서비스에 대한 성능 분석 결과를 핵심 위주로 요약해드렸습니다.

`portfolio-amber-mu-57.vercel.app` 웹사이트는 **GSAP 기반의 인트로 애니메이션**과 **CSR 중심의 리액트 기술 스택**을 바탕으로 구성된 단일 페이지 포트폴리오입니다. 현재는 **개발 초기 단계로 보이며**, 애니메이션 연출 측면에서는 인상적인 사용자 경험을 제공합니다. 구조상 일부 설계 방식이 Lighthouse 등의 성능 측정 도구에서 불리하게 작용하고 있어 개선 여지가 확인되었습니다.

주요 분석 내용은 다음과 같습니다.

- **인트로 애니메이션 이후 주요 콘텐츠가 자바스크립트를 통해 동적으로 렌더링 및 마운트됨**  
   → LCP 측정 지연 및 콘텐츠 노출 타이밍 왜곡 발생
- **애플리케이션 전체 로직이 단일 `index.js` 번들에 포함됨**  
   → 자바스크립트 파싱 및 실행 지연, 캐시 재활용률 저하
- **GSAP 기반 전환 애니메이션이 콘텐츠 제거 후 재등장하는 방식으로 구성됨**  
   → 실제 콘텐츠가 브라우저 및 크롤러에 늦게 인식됨
- **`react`, `react-dom`, `emotion`, `gsap` 등 외부 라이브러리가 하나의 번들에 포함됨**  
   → 초기 청크 크기 증가 및 로딩 지연 발생
- **핵심 콘텐츠가 초기 HTML에 포함되지 않음**  
   → SEO 및 접근성 측면에서 불리
- **Lighthouse와 Performance 탭 간 LCP 측정 결과 불일치**  
   → 측정 종료 시점 기준의 구조적 차이에 기인

이에 따라 다음과 같은 개선 방안을 우선적으로 제안드립니다.

- **콘텐츠 우선 렌더링 및 Progressive Enhancement 적용**:
  - 주요 콘텐츠는 초기에 HTML에 포함하고, 시각적 전환만 애니메이션으로 처리합니다.
  - 자바스크립트가 비활성화된 환경에서도 최소한의 정보가 전달되도록 구조를 개선합니다.
- **GSAP 애니메이션 구조 재설계**:
  - 콘텐츠를 제거한 뒤 다시 등장시키는 방식 대신, `opacity`, `transform` 등 GPU 친화적인 속성으로 시각적 전환만 구현합니다.
  - 첫 한줄 소개 이후에야 실질 콘텐츠가 렌더링되는 구조는 LCP 측면에서 불리하므로 제거를 권장합니다.
- **코드 스플리팅 및 청크 캐싱 전략 도입**:
  - `react`, `react-dom` 등 정적 라이브러리는 `manualChunks` 옵션으로 별도 분리합니다.
  - 작은 변경에도 전체 번들이 재생성되지 않도록 하여 사용자 캐싱을 최대한 활용할 수 있는 구조로 전환합니다.
- **첫 방문자 / 재방문자 분기 처리**:
  - 첫 방문 시 전체 인트로 애니메이션, 이후에는 축약된 전환 또는 즉시 콘텐츠를 노출 시키는 전략을 추천해드립니다.
  - 이렇게 함으로써 첫번째 방문자에겐 깊이 있는 애니메이션을, 반복적으로 접근하는 사용자에게는 빠르게 해당 사용자가 원하는 컨텐츠를 제공할 수 있습니다.
  - `localStorage` 등을 활용한 방문자 상태 추적

이러한 개선 사항들을 적용하시면 초기 렌더링 시점의 사용자 경험은 물론, LCP/FCP 등의 주요 성능 지표, 검색 엔진 최적화, 모바일 대응력까지 다방면에서 개선 효과를 기대할 수 있습니다. 자세한 기술적 맥락과 근거는 본문 보고서를 참고해주시기 바랍니다.

## 2. 분석 개요

2025년 7월 19일 기준 배포된 웹사이트를 분석했습니다.

## 3. 웹사이트 분석

![1.png](./images/portfolio/1.png)

![2.png](./images/portfolio/2.png)

분석에 사용한 도구는 다음과 같습니다.

- chrome dev tool
- webpagetest

### 3-1. 주요 프레임워크 및 라이브러리, 빌드 환경

이 웹사이트는 단 하나의 자바스크립트 리소스만 가지고 있기 때문에, 이 자바스크립트 리소스를 토대로 어떤 프레임워크와 라이브러리를 사용하는지 분석해보았습니다.

- react: react@19.1.0
- vite: ESModule 을 사용한 빌드 방식을 확인할 수 있었습니다. 아마도 create-vite 기반의 리액트 보일러 플레이트를 사용하여 개발된 프로젝트 일 것으로 보입니다.
- emotion: `data-emotion` `__EMOTION_TYPE_PLEASE_DO_NOT_USE__` 와 같은 emotion 특유의 예약어를 볼 수 있었습니다.
- GSAP: [GreenSock Animation Platform](https://gsap.com/)은 (이하 GSAP) 웹에서 사용되는 강력한 자바스크립트 애니메이션 라이브러리 입니다. 이 라이브러리에서만 사용되는 고유한 네이밍 컨벤션, 텍스트 스플릿 애니메이션, 타임라인과 시퀀스 구조 등이 눈에 띄었습니다. 이 웹사이트의 대부분을 이루는 애니메이션은 GSAP을 기반으로 제작되었을 것으로 보입니다.
- jotai: `useAtom`, `useSetAtom` 과 같은 jotai 특유의 상태 관리를 위한 함수 사용 패턴을 발견헀습니다. jotai를 쓰고 있거나, 혹은 이와 비슷한 라이브러리를 직접 구현하였거나 차용한 것으로 보입니다.
- react-router: 리액트 라우터 특유의 에러메시지 또는 문자열을 확인할 수 있었습니다.

### 3-2. 배포 환경

주소에서 명확히 드러나듯, https://portfolio-amber-mu-57.vercel.app/ 는 현재 **Vercel 플랫폼을 통해 배포 및 서비스되고 있는 웹사이트**입니다.

## 4. 주요 질문에 대한 답변

개발자님께서 가지고 계신 고민인 낮은 라이트 하우스 성능 점수를의 근본적인 원인을 파악하고, 우선적으로 개선해야할 지점을 구체적으로 분석해보았습니다.

### 4-0. 들어가기전에: 왜 performance tab 과 ligththouse 의 점수가 다를까?

웹 성능 분석에서 가장 당혹스러운 순간 중 하나는 같은 페이지를 측정했는데 Performance 탭과 Lighthouse에서 LCP 값이 전혀 다르게 나오는 상황입니다. 실제로 개발자님께서 Lighthouse 점수가 낮다고 고민하셨던 반면, [WebPageTest](https://webpagetest.org/)나 DevTools 내 Lighthouse 탭에서는 꽤 좋은 점수가 나오고 있었습니다.

![3.png](./images/portfolio/3.png)

![4.png](./images/portfolio/4.png)

위 스크린샷은 각각 webpagetest 와 크롬 개발자 도구에서 측정한 점수로, 라이트 하우스 점수가 모두 뛰어나게 나오는 것을 볼 수 있습니다.

하지만 performance 탭에서는 조금 이야기가 다릅니다.

![5.png](./images/portfolio/5.png)

앞서 라이트하우스가 뛰어난 점수를 보여준 것과는 다르게, performance 탭에서는 LCP가 4초 대로 떨어진 것을 볼 수 있습니다. 이 원인을 알기 위해서는, performance 탭과 라이트하우스의 측정 방식의 차이점을 알아볼 필요가 있습니다.

도구마다 결과가 다른 이유는 단순한 측정 오차가 아니라, 측정 종료 시점을 결정하는 로직과 철학의 차이 때문입니다.

#### 4-0-1. Performance 탭의 종료 시점: 사용자의 실제 경험에 가까운 기록

Performance 탭은 DevTools가 브라우저의 실제 실행 경로를 그대로 추적합니다. 로드 이벤트가 끝난 이후에도 추가적인 사용자 인터페이스 변화(예: 자바스크립트 애니메이션, 렌더링 등)를 관찰하기 위해, 자동으로 5초를 더 기록합니다. 관련 소스는 [크로미움 코드베이스](https://source.chromium.org/search?q=millisecondsToRecordAfterLoadEvent)에서도 확인할 수 있습니다.

```js
UI.panels.timeline._millisecondsToRecordAfterLoadEvent = 5000
```

이는 사용자가 느끼는 실제 체감 성능을 반영하려는 목적입니다. 코드를 보면, 실제 `millisecondsToRecordAfterLoadEvent`라는 변수명을 토대로 5000ms 정도 대기하고 있는 것을 볼 수 있습니다. 이는 실제로 performance 탭에서 성능을 기록해보면 대략적으로 유추해볼 수도 있습니다.

![13.gif](./images/portfolio/13.gif)

정확하지는 않지만, 약 5초 정도 대기하고 있는 것을 볼 수 있습니다.

그렇다면 이 초 단위를 수정해보고, 그리고 실제로 다시 측정해서 점수가 라이트하우스가 낮게 나온다면 이러한 가정이 정말 맞는지 확인해 볼 수 있지 않을까요? 이 대기시간을 수정하는 방법은 다음과 같습니다. 조금 과한 확인일 수도 있지만 재미있을 것 같으니 한번 살펴보겠습니다. 😄

1. 먼저 크롬 개발자 모드를 활성화 시킵니다.
2. 우측 상단의 삼점 메뉴를 누른다음, Dock side 를 맨왼쪽 아이콘을 클릭합니다. 이렇게하면 크롬 개발자 도구 화면이 별도 창으로 뜹니다.
   ![6.png](./images/portfolio/6.png)
3. 이 개발자 도구가 떠있는 화면에서, 크롬 개발자 도구를 여는 단축키 Ctrl+Shift+i or ⌘+⌥+|i 를 또 누릅니다. 그러면 개발자도구를 위한 개발자 도구가 뜹니다. 이러한 방식을 devtools on devtools 라고 부릅니다.
   ![7.png](./images/portfolio/7.png)
   이 창에서는 웹사이트에서 사용중인 개발자도구를 개발자도구로 열어서 우리가 원하는 조작을 수행할 수 있습니다.
4. 콘솔 창으로 이동한다음, `UI.panels.timeline.millisecondsToRecordAfterLoadEvent = 3000 ` 을 입력합니다. 이렇게 하면, 이제 5초를 대기하던 performance 가 3초만 대기하게 됩니다.

그리고 다시금 성능을 측정해보면, 라이트하우스와 비슷하게 LCP 가 굉장히 비슷하게 좋은 점수로 나오는 것을 확인할 수 있습니다.

![8.png](./images/portfolio/8.png)

#### 4-0-2. Lighthouse의 종료 시점: "완전히 로드되었는가?"를 판단하는 세 가지 조건

그렇다면 라이트하우스는 어떤 시점을 토대로 점수를 측정할까요? 라이트 하우스의 측정 조건을 알기 위해서는 라이트 하우스의 코어 로직을 살펴볼 필요가 있습니다.

https://paulirish.github.io/lighthouse/docs/api/lighthouse/2.5.1/lighthouse-core_gather_driver.js.html

측정이 종료되는 시점을 살펴보기 위해서는 `_waitForFullyLoaded` 메서드를 살펴봐야 합니다. 이 함수는 페이지 로딩 완료 시점을 측정하는 메서드로, 다음 세가지 조건을 만족해야 합니다.

```js
_waitForFullyLoaded(pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs,
                   maxWaitForLoadedMs) {
  let maxTimeoutHandle;  // 타임아웃 핸들 저장용

  // 1. Load 이벤트 대기 (+ 추가 대기 시간)
  // pauseAfterLoadMs: Load 이벤트 후 추가로 기다릴 시간 (기본값: 0ms)
  const waitForLoadEvent = this._waitForLoadEvent(pauseAfterLoadMs);

  // 2. Network Quiet 대기
  // networkQuietThresholdMs: 네트워크가 조용해야 하는 시간 (기본값: 5000ms)
  // 조용함의 기준은? 동시 진행 중인 요청이 2개 이하
  const waitForNetworkIdle = this._waitForNetworkIdle(networkQuietThresholdMs);

  // 3. CPU Quiet 대기 (나중에 초기화)
  let waitForCPUIdle = null;

  // 4. Load와 Network가 모두 완료되면 실행되는 Promise
  const loadPromise = Promise.all([
    waitForLoadEvent.promise,      // Load 이벤트 대기
    waitForNetworkIdle.promise,    // Network Quiet 대기
  ]).then(() => {
    // Network가 조용해진 후에만 CPU 체크 시작!
    // cpuQuietThresholdMs: CPU가 조용해야 하는 시간 (기본값: 0 = 체크 안함)
    waitForCPUIdle = this._waitForCPUIdle(cpuQuietThresholdMs);
    return waitForCPUIdle.promise;
  }).then(() => {
    // 모든 조건 충족 시 cleanup 함수 반환
    return function() {
      log.verbose('Driver', 'loadEventFired and network considered idle');
      clearTimeout(maxTimeoutHandle);  // 타임아웃 취소
    };
  });

  // 5. 최대 대기 시간 타임아웃 (안전장치)
  // maxWaitForLoadedMs: 최대 대기 시간 (기본값: 30초)
  const maxTimeoutPromise = new Promise((resolve, reject) => {
    maxTimeoutHandle = setTimeout(resolve, maxWaitForLoadedMs);
  }).then(_ => {
    // 타임아웃 시 cleanup 함수 반환
    return function() {
      log.warn('Driver', 'Timed out waiting for page load. Moving on...');
      waitForLoadEvent.cancel();       // 모든 대기 취소
      waitForNetworkIdle.cancel();
      waitForCPUIdle && waitForCPUIdle.cancel();
    };
  });

  // 6. 경쟁: 정상 완료 vs 타임아웃 중 먼저 끝나는 것
  return Promise.race([
    loadPromise,        // 정상적인 측정 완료
    maxTimeoutPromise,  // 30초 타임아웃
  ]).then(cleanup => cleanup());  // cleanup 함수 실행
}
```

해당 함수는 아래 세 가지 조건을 순차적으로 확인하며, 그 중 하나라도 만족하지 못하면 `maxWaitForLoadedMs` (기본값 30초) 이후 강제 종료됩니다.

1. Load 이벤트 발생 + 지정된 시간만큼 대기 (`pauseAfterLoadMs`)
   - 기본값은 `0ms`이며, Load 이벤트 이후 바로 다음 조건으로 넘어감
2. Network Quiet 상태 유지 (`networkQuietThresholdMs`)
   - 진행 중인 네트워크 요청이 2개 이하인 상태가 `5000ms` 이상 유지되어야 함
   - Lighthouse 내부에서는 `"network-2-quiet"` 상태로 판단
3. CPU Quiet 상태 유지 (`cpuQuietThresholdMs`)
   - 50ms 이상의 Long Task가 없어야 하며,
   - 마지막 Long Task가 종료된 시점부터 `cpuQuietThresholdMs` (예: 5000ms) 이상 지나야 함
   - 단, `cpuQuietThresholdMs`가 0일 경우 이 체크는 생략됨
4. 종료 조건 경쟁
   - 정상적인 조건이 충족되면 종료
   - 그 외에는 30초 타임아웃 이후 강제 종료

#### 4-0-3. 결론: LCP가 다르게 나오는 건 '의도된 차이'다

지금까지 이야기한 내용을 표로 정리하면 다음과 같습니다.

| 항목          | Performance 탭                   | Lighthouse                                                     |
| ------------- | -------------------------------- | -------------------------------------------------------------- |
| 목적          | 실시간 동작 기록                 | 특정 조건에 따른 통제된 환경에서의 품질 측정                   |
| 종료 기준     | `load` 이벤트 + 5초 후 자동 종료 | Load, Network, CPU Idle 조건 만족 시 종료 (또는 30초 타임아웃) |
| LCP 수집 시점 | 마지막 콘텐츠 등장까지 감지 가능 | 조기 종료되면 나중에 등장한 요소는 반영 안 됨                  |
| 측정값        | 실제 사용자 경험에 가까움        | 실험적 기준 기반 (변수 통제 가능)                              |

DevTools의 Performance 탭에서 LCP가 더 느리게 측정되는 이유는 그것이 실제로 유저가 콘텐츠를 보게 되는 순간까지를 감지하려는 목적이기 때문입니다. 반면, Lighthouse는 페이지가 충분히 안정되었다고 판단되면 일찍 측정을 종료하며, 그 이후 나타난 큰 콘텐츠는 LCP 후보로 포함되지 않습니다.

따라서 Lighthouse에서 LCP가 낮게 나오고, Performance 탭에서 높게 나오는 것은 단순한 오차가 아니라 도구의 설계 목표 차이로 이해해야 합니다. 이 차이를 이해하면 어떤 도구에서의 결과가 실제 문제인지를 더 명확히 판단할 수 있습니다.

물론 가장 정확한 성능 측정은 실제 사용자 데이터를 기반으로 한 RUM(Real User Monitoring)입니다. 이를 측정하기 위한 도구인 Lighthouse와 DevTools는 그 보조 수단일 뿐, 진짜 정답은 브라우저가 아니라 사용자의 눈이 가지고 있다는 점을 잊지 마셔야 합니다.

### 4-1. Performance 탭에서 LCP가 더 느리게 측정되는 구조적 이유

앞서 Lighthouse와 Performance 탭 간의 LCP 측정 차이는 **측정 종료 시점을 결정하는 기준의 차이**에서 발생한다는 점을 설명드렸습니다. 이번 항목에서는 해당 웹사이트의 렌더링 흐름을 바탕으로, 왜 Performance 탭에서 LCP가 상대적으로 더 늦게 측정되는지를 구체적으로 분석해보겠습니다.

#### 4-1-1. 초기 렌더링: 비어 있는 루트 요소

페이지 초기 진입 시점에서 `div#root` 요소는 콘텐츠 없이 빈 상태로 존재합니다. 초기 렌더링 시 뷰포트 내에 표시되는 의미 있는 요소가 없기 때문에, LCP 후보 또한 존재하지 않습니다. 이는 브라우저가 최초로 시각적 콘텐츠를 감지할 수 없는 상태로 해석됩니다.

#### 4-1-2. 자기소개 텍스트의 등장: 전환용 애니메이션 콘텐츠

이후 `.introTitle`, `.introTitleFill` 등의 클래스명을 가진 요소가 등장하며, 짧은 자기소개 문구가 화면에 나타납니다. 해당 동작은 GSAP 기반의 커스텀 애니메이션 구현체를 통해 시퀀스 단위로 재생됩니다. 이 시점에서 해당 텍스트 블록이 LCP 후보로 기록되었을 것입니다. 실제, webpagetest 를 통해 살펴보면 해당 영역이 LCP로 기록되어있음을 볼 수 있습니다.

![9.png](./images/portfolio/9.png)

#### 4-1-3. 첫 번째 콘텐츠 제거 및 주요 콘텐츠 진입

자기소개 문구가 등장한 이후, `.introTitleSection` 전체를 대상으로 다시 페이드 아웃 애니메이션이 적용됩니다. 해당 영역은 투명도와 Y축 이동을 통해 시각적으로 제거되며, 이어서 실제 웹사이트 본문의 주요 콘텐츠가 렌더링됩니다. 이 단계에서 사용되는 핵심 코드는 다음과 같습니다.

```ts
// 블로그 글로 추정컨데 Rally 라는 라이브러리? 함수? 를 만드시지 않았을까 추측해봅니다.
Rally({
  target: '.introTitleSection',
  motions: [
    {
      delay: 0.4,
      duration: 0.6,
      ease: 'back.in',
      opacity: {
        to: 0,
      },
      translateY: {
        to: -30,
      },
    },
  ],
})
```

해당 애니메이션 시퀀스의 종료 시점 이후에 실제 컨텐츠를 렌더링하는 콜백이 실행되며, 이 시점부터 실제 웹사이트의 핵심 콘텐츠가 마운트되고 뷰포트 내에 표시됩니다.

#### 4-1-4. LCP 측정 지점의 차이: 측정 종료 시점에 따른 후보 누락 여부

여기에서 4-0. 파트에서 이야기 했던 내용을 다시금 상기해볼 필요가 있습니다.

- **Lighthouse**는 내부 로직상 네트워크 및 CPU가 idle 상태에 도달하면 LCP 측정을 종료합니다. 위에서 설명한 `updateStep` 콜백이 실행되기 이전에 측정이 종료되기 때문에, **실제 콘텐츠가 아닌 자기소개 텍스트가 최종 LCP 후보로 기록됩니다.**
- **Performance 탭**은 Load 이벤트 이후에도 5초간 추가로 관찰을 수행합니다. 이로 인해 `updateStep` 콜백 이후 등장하는 **실제 콘텐츠 영역이 LCP 후보로 감지되며**, 최종적으로 **더 늦은 시점의 콘텐츠가 LCP로 기록**됩니다.

따라서, 웹사이트의 측정 목표에 따라 LCP나 다른 지표의 점수를 어떻게 판단할지 고려해볼 필요가 있습니다.

#### 4-1-5. 점수 차이에 따른 결과

해당 웹사이트는 초기 콘텐츠가 임시적으로 등장하고 이후 제거되는 **전환형 구조**를 가지고 있으며, 실제 주요 콘텐츠는 늦은 시점에 등장합니다. 이 구조에서는 Lighthouse가 실질적인 콘텐츠가 등장하기 이전에 측정을 종료해버리기 때문에 LCP가 짧게 측정되며, 반면 Performance 탭은 **사용자가 실질적으로 보는 시점까지 포함하여 측정**하기 때문에 LCP가 더 길게 기록됩니다.

이는 단순한 측정 오차가 아니라, 두 도구의 설계 목적에 따른 **의도된 측정 차이**입니다. 따라서 실제 사용자 경험과 가까운 성능 지표를 확보하고자 할 경우, Performance 탭의 결과를 기준으로 판단하거나 RUM 기반 측정 도입을 고려하는 것이 바람직합니다.

### 4-2. 현재 문제를 해결하기 위한 제안

현재 웹사이트는 초기 콘텐츠가 지연되어 등장하는 구조로 인해, 사용자 경험과 성능 측정 지표, 검색 최적화 측면에서 몇 가지 한계를 드러내고 있습니다. 이는 의도적으로 연출된 시각 효과 측면에서는 장점이 될 수 있으나, 실제 운영되는 포트폴리오 웹사이트로서의 목적에는 다소 부합하지 않는 면이 있습니다.

본 절에서는 **Progressive Enhancement** 원칙을 기반으로, 현재 구조의 단점을 보완하면서도 애니메이션 효과는 유지할 수 있는 개선 방안을 제안합니다.

> **Progressive Enhancement란?** https://en.wikipedia.org/wiki/Progressive_enhancement
>
> Progressive Enhancement(점진적 향상)는 웹 접근성과 안정성을 보장하기 위한 설계 철학으로, **핵심 콘텐츠와 기능을 가장 기본적인 형태로 먼저 제공한 뒤**, 브라우저나 기기의 성능, 사용자의 환경에 따라 **점진적으로 시각적·상호작용적 기능을 추가하는 방식**을 말합니다.  
> 즉, **콘텐츠와 의미 구조가 항상 최우선으로 제공되어야 하며**, 자바스크립트나 고급 스타일링은 **기본 기능이 보장된 이후에 선택적으로 동작**해야 합니다.  
> 이 원칙은 다양한 사용자 환경을 고려한 웹 개발의 기본 전략으로 널리 채택되고 있으며, 성능 최적화, 접근성, SEO 개선 등에도 직접적인 영향을 미칩니다.

#### 4-2-1. 콘텐츠 우선 렌더링 구조로 전환합니다.

가장 우선적으로 고려해야 할 개선 사항은 **주요 콘텐츠를 초기 HTML에 포함시키는 것**입니다. 현재 구조에서는 인트로 애니메이션이 완료된 이후에야 실질적인 콘텐츠가 자바스크립트를 통해 마운트되기 때문에, 브라우저 및 성능 측정 도구는 콘텐츠가 존재하지 않는 페이지로 인식하게 됩니다.

이러한 콘텐츠 지연 노출 구조는 LCP와 같은 성능 지표를 악화시킬 뿐만 아니라, 검색 엔진이 콘텐츠를 적절히 인덱싱하지 못해 **SEO 측면에서도 불리한 결과**를 초래할 수 있습니다.

여기서 말하는 콘텐츠 우선 렌더링이란, 서버 사이드 렌더링(SSR)이나 프레임워크 전환을 의미하는 것이 아닙니다. 물론 SSR 을 도입한다면 훨씬 더 빠르게 성능을 향상시킬 수도 있습니다. 그러나 현재와 같은 CSR 기반 환경에서도, 의미 있는 콘텐츠를 HTML 상에 포함시키고, 이후 애니메이션을 통해 시각적으로 노출하는 방식으로 충분히 구현할 수 있습니다.

예를 들어, 주요 콘텐츠를 DOM에 미리 포함한 뒤 `opacity`, `transform`, `visibility` 등 CSS 속성을 활용하여 시각적 전환을 구현하면, 사용자는 페이지 진입 직후부터 정보를 인지할 수 있으며, 성능 측정 도구도 이를 정확히 반영할 수 있습니다. 결과적으로 LCP와 같은 메트릭은 실질적인 콘텐츠 기준으로 측정되며, SEO 친화적인 구조로 개선됩니다.

이와 같은 방식은 **현재의 CSR 구조를 유지하면서도 Progressive Enhancement 원칙에 부합하는 가장 현실적인 개선 전략**입니다.

#### 4-2-2. 애니메이션은 시각적 계층에서 처리합니다.

애니메이션을 적용할 때는 콘텐츠 자체의 **렌더링 시점을 지연시키는 방식보다는**, 이미 렌더링된 콘텐츠에 **시각적 스타일을 통해 점진적으로 등장하는 효과를 주는 방식이 바람직**합니다.

예를 들어, 현재 구조처럼 자바스크립트 실행 후 특정 DOM 노드를 생성하고 마운트하는 방식은 **브라우저와 성능 측정 도구가 해당 콘텐츠를 늦게 인식하도록 만들며**, LCP, FCP 등의 지표가 불리하게 측정될 수 있습니다. 또한 콘텐츠가 늦게 삽입되면 검색 엔진 크롤러가 콘텐츠를 발견하지 못할 가능성도 있습니다.

이를 해결하기 위해 다음과 같은 속성을 활용한 **시각적 애니메이션 처리 방식**을 제안합니다:

- `opacity`: 투명도를 점진적으로 증가시켜 자연스럽게 등장시키기
- `transform`: `translateY`, `scale` 등을 이용한 위치 이동이나 확대/축소 효과
- `clip-path`: 요소가 일정 형태로 점점 잘려 나가거나 드러나게 하는 효과
- `visibility`: `visibility: hidden` → `visible`로 변경하며 시야에 노출

```html
<!--다음 예시 코드는 실제 포트폴리오와 상관없이 만든 예제 코드입니다. -->
<section class="intro">
  <h1 class="intro-title">김성현입니다</h1>
  <p class="intro-description">프론트엔드 개발자 포트폴리오</p>
</section>

<style>
  .intro {
    opacity: 0;
    transform: translateY(30px);
    transition:
      opacity 0.6s ease-out,
      transform 0.6s ease-out;
  }

  .intro.visible {
    opacity: 1;
    transform: translateY(0);
  }
</style>

<script>
  window.addEventListener('DOMContentLoaded', () => {
    requestAnimationFrame(() => {
      document.querySelector('.intro')?.classList.add('visible')
    })
  })
</script>
```

위 예시는 HTML에 콘텐츠가 **처음부터 존재하며**, 자바스크립트는 그저 클래스 하나를 추가하여 시각적 애니메이션만 부여하는 구조입니다. 이렇게 구성하면 **검색 엔진, Lighthouse, 브라우저 렌더링 엔진이 모두 콘텐츠를 정확히 인식할 수 있으며**, 동시에 사용자는 부드러운 전환 효과를 경험하게 됩니다.

이 방식은 또한 GPU 레벨에서 최적화되는 애니메이션 속성(opacity, transform)을 중심으로 구성되기 때문에, **모바일 기기에서도 성능 저하 없이 안정적인 동작**이 가능합니다.

#### 4-2-3. 첫 방문과 재방문을 구분하는 전략을 도입합니다.

포트폴리오의 성격상 첫 방문자에게는 시각적으로 인상적인 연출을 제공하는 것이 중요할 수 있습니다. 그러나 반복 방문 사용자에게는 콘텐츠 접근성이 더 중요합니다. 이에 따라 **방문 이력을 기반으로 애니메이션 실행 여부를 분기하는 전략**이 유효할 수도 있습니다.

```javascript
// 첫 방문자에게만 전체 인트로 애니메이션을 실행
const showFullAnimation = !localStorage.getItem('returning-visitor')

if (showFullAnimation) {
  runIntroAnimation()
  localStorage.setItem('returning-visitor', 'true')
} else {
  skipToContentImmediately()
}
```

이러한 방식은 사용자의 피로도를 줄이면서도, 첫 노출에서 시각적 임팩트를 유지하는 절충안이 될 수 있습니다.

#### 4-2-4. 실무 적용 시 점검 항목

앞서 제안한 개선 사항들을 실제 프로젝트에 반영할 때는, 다음의 항목들을 체크리스트 형태로 활용하여 코드 품질, 성능, 접근성 측면에서 구조적으로 문제가 없는지 사전에 검토해보시는 것은 어떨까요?

- **모든 텍스트 콘텐츠가 초기 HTML에 포함되었는가?**  
   → 성능 측정 도구와 검색 엔진이 콘텐츠를 정확히 인식할 수 있도록 합니다.
- **자바스크립트 비활성화 상태에서도 콘텐츠가 보이는가?**  
   → Progressive Enhancement 원칙을 따르고 있는지 확인합니다.
- **애니메이션이 `will-change` 속성으로 최적화되었는가?**  
   → `will-change`는 브라우저에 "이 속성이 곧 변경될 예정"이라는 힌트를 주는 CSS 속성입니다. 예를 들어 `will-change: transform`을 설정하면 브라우저는 해당 요소에 대해 레이어 분리나 GPU 가속 처리를 미리 준비하여 **레이아웃 계산 및 리페인트 비용을 줄일 수 있습니다**. 다만 과도한 사용은 오히려 성능을 저하시킬 수 있으므로 변화가 집중되는 핵심 요소에만 국한해서 사용하는 것이 좋습니다.
- **`prefers-reduced-motion` 미디어 쿼리를 고려했는가?**  
   → `prefers-reduced-motion`은 **사용자가 운영체제 또는 브라우저에서 애니메이션 최소화를 선호한다고 명시했을 때** 이를 감지할 수 있는 CSS 미디어 쿼리입니다. 이를 활용하면 애니메이션이나 전환 효과를 최소화하거나 생략함으로써 모션 민감 사용자를 배려해줄 수 있습니다.
  ```css
  @media (prefers-reduced-motion: reduce) {
    .animated {
      animation: none;
      transition: none;
    }
  }
  ```
- **모바일 기기에서 애니메이션 성능이 충분한가?**  
   → 저성능 디바이스에서도 렌더링 병목이나 프레임 드롭 없이 동작하는지 확인합니다. 특히 `opacity`, `transform` 등 GPU 최적화 가능한 속성만을 사용하는 것을 권장해드리며, layout/paint/reflow를 유발하는 속성(`top`, `left`, `height` 등)은 지양하는 것이 좋습니다.

이러한 체크리스트는 단순히 애니메이션이 잘 보이느냐를 넘어서, 웹 접근성, 성능 안정성, 유지보수 가능성 전반을 평가하는 기준으로 작용합니다. 특히 포트폴리오 사이트처럼 콘텐츠 전달력과 시각적 완성도를 동시에 요구하는 웹사이트에서는, 이러한 기준이 곧 품질의 기준이 됩니다.

따라서 사용자에게 인상적인 경험을 제공하면서도 검색 가능성과 성능 최적화까지 만족시키려면, 콘텐츠 우선 렌더링과 점진적 향상 전략(Progressive Enhancement)을 함께 고려한 설계가 핵심이 되어야한다고 생각합니다.

## 5. 기타 제안

앞서 언급한 핵심 병목 요소들 외에도, 전반적인 성능 향상에 기여할 수 있는 세부 최적화 지점들을 추가로 분석해보았습니다.

### 5-1. 코드 스플리팅 코드 스플리팅을 통한 번들 최적화 제안

현재 번들 파일인 `index-CF5N6APp.js`의 크기는 약 138KB입니다. 이는 최근 프론트엔드 트렌드 상 과도하게 큰 크기는 아니지만, **복잡한 로직이 없는 정적 사이트**라는 점을 고려하면 다소 무거운 편입니다. 특히 이 웹사이트는 **싱글 페이지 애플리케이션(SPA)** 구조로 되어 있어, 이러한 번들 구조가 성능에 직접적인 영향을 미칩니다.

#### 5-1-1. 번들 크기가 큰 원인

현재 자바스크립트 파일에는 개발자님이 작성하신 서비스 로직뿐만 아니라, `react`, `react-dom`, `emotion`, `gsap` 등 프레임워크 및 외부 라이브러리 코드가 모두 포함되어 있습니다. 또한 코드 스플리팅이 적용되어 있지 않아, 페이지를 구성하는 모든 로직이 하나의 번들에 의존하는 구조입니다. 이로 인해 브라우저는 전체 리소스를 한 번에 다운로드하고 파싱해야 하며, **병렬 로딩의 이점을 전혀 활용하지 못하고 있습니다.**

#### 5-1-2. SPA 구조에서 JS 파싱 지연의 영향

100KB 이상의 자바스크립트 파일은 일반적인 CSR 기반의 서비스에서는 치명적이지 않을 수 있습니다. 하지만 본 웹사이트는 SSR이 적용되어 있지 않기 때문에, 브라우저가 초기 화면을 구성하는 전 과정을 자바스크립트에 의존하고 있습니다.

이러한 구조에서는 **JS 파싱 및 실행 시간이 길어질수록 콘텐츠의 렌더링 지연이 직접적으로 발생**하며, 결과적으로 LCP, TTI와 같은 주요 성능 지표에도 부정적인 영향을 줍니다. 특히 인트로 애니메이션 후 실제 콘텐츠가 등장하는 현재 구성에서는 이러한 지연이 더욱 두드러질 수 있습니다.

#### 5-1-3. 초기 렌더링 및 캐시 효율을 고려한 코드 분리 전략

현재 구조에서는 모든 코드가 하나의 번들에 포함되어 있으며, `react`, `react-dom`, `emotion`, `gsap` 등 주요 라이브러리도 `index.js`에 함께 포함되어 있습니다. 이러한 구조는 다음과 같은 문제를 초래합니다:

- **초기 렌더링 시 과도한 JS 파싱 및 실행 비용 발생**
- **작은 코드 변경에도 전체 번들이 다시 빌드되어 캐시가 무효화됨**

이 문제를 해결하기 위해서는 **초기 렌더링에 꼭 필요한 코드만 최소 청크로 분리하고, 공통 라이브러리는 별도 청크로 분리하는 전략**이 필요합니다. 이를 통해 다음과 같은 효과를 기대할 수 있습니다:

- 브라우저는 **초기 화면 구성에 필요한 최소 리소스만 우선 로딩**하게 되어 **LCP 개선**
- `react`, `react-dom` 등은 변경 가능성이 낮기 때문에 **캐시 활용 극대화**
- 전체 번들 재생성 없이 변경된 청크만 교체되므로 **배포 효율 향상**

Vite에서는 `rollupOptions.output.manualChunks` 설정을 통해 쉽게 적용할 수 있습니다:

```ts
// vite.config.ts 예시
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          react: ['react', 'react-dom'],
        },
      },
    },
  },
}
```

Next.js 역시 동일한 전략을 채택하고 있으며, `framework.js`라는 파일에 공통 라이브러리를 분리하여 **변경이 없는 리소스를 장기간 캐시하도록 구성**되어 있습니다.

![12.png](./images/portfolio/12.png)

![1.png](./images/portfolio/11.png)

이처럼 단순한 청크 분리만으로도 **초기 렌더링 성능, 네트워크 부하, 배포 효율을 모두 개선할 수 있으며**, SPA 구조인 현재 프로젝트에서는 특히 효과적인 전략이 될 수 있습니다.

#### 5-1-5. 현재 구조에서는 과하지 않은 분리로도 충분한 이점을 기대할 수 있습니다

물론, 과도한 코드 스플리팅은 라우팅이 많은 대규모 웹 애플리케이션에서는 오히려 초기 로딩 속도를 저하시킬 수 있습니다. 하지만 본 사이트는 명확한 페이지 구획 없이 하나의 뷰에서 구성되는 SPA이므로, **라이브러리 수준의 단순한 청크 분리만으로도 성능 이점을 얻을 수 있습니다.**

**결론적으로**, 현재 구조에서는 초기 렌더링 시점에 불필요하게 많은 자바스크립트가 한꺼번에 로딩되고 있으며, 이를 개선하기 위해서는 **청크 분리를 통한 번들 최적화가 필수적인 과제**입니다. Vite 기반의 빌드 환경에서는 이를 비교적 간단하게 구현할 수 있으므로, 실제 배포 환경에서도 성능과 사용자 경험 모두를 향상시킬 수 있을 것입니다.

## 6. 결론

[https://portfolio-amber-mu-57.vercel.app/](https://portfolio-amber-mu-57.vercel.app/)의 구조를 바탕으로, 사용자 경험과 성능 측면에서 어떤 개선이 가능한지를 구체적으로 살펴보았습니다.

현 구조는 개발 초기 단계임에도 불구하고, **GSAP을 활용한 강도 높은 애니메이션 연출**, **Emotion 기반 스타일링**, 그리고 리액트와 vite 기반의 현대적인 프론트엔드 구성 요소를 바탕으로 **기술적인 완성도를 갖춘 인상적인 포트폴리오**입니다. 특히 시각적 임팩트를 중심에 두고 구성된 UI/UX 흐름은 콘텐츠보다는 **연출과 표현력 자체를 중심으로 보여주려는 의도**가 분명히 느껴졌습니다.

다만, 인트로 이후 콘텐츠가 마운트되는 구조나, 모든 리소스를 하나의 번들로 처리하는 현재의 방식은 Lighthouse 등 성능 측정 도구에서는 **실제 사용자 경험과 괴리가 있는 평가 결과를 초래할 수 있으며**, 검색 최적화나 접근성 측면에서도 개선 여지가 있는 것으로 판단됩니다.

지금은 포트폴리오 초기 단계이므로, 복잡한 구조 변경 없이도 **렌더링 순서, 코드 분리 전략, Progressive Enhancement 원칙 적용 등 가벼운 구조 개선만으로도 충분한 성능 개선 효과**를 기대할 수 있습니다. 또한 향후 콘텐츠가 추가되거나 페이지 수가 늘어나는 과정에서도, 이번 분석에서 제시한 방향을 기반으로 **구조적 확장성과 기술적 지속 가능성을 확보**하실 수 있으리라 생각합니다.

앞으로 이 포트폴리오가 더 발전하고, 그 안에 담긴 콘텐츠와 메시지가 더 널리 전달될 수 있기를, 그리고 프론트엔드 개발자로서의 성공적인 첫발을 내딛으 실 수 있기를 진심으로 기원합니다.

궁금한 점이나 추가로 논의하고 싶은 부분이 있다면 언제든 편하게 말씀해주세요.
읽어주셔서 감사합니다!
