avatar
Published on

웹 서비스 성능 분석 (4)

Author
  • avatar
    Name
    yceffort

실제 개발자 피드백

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

글이 많이 도움이 되었는지

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

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

그러다가 저자님께 분석 서비스를 요청드렸는데, 정말 상세하고 전문적으로 분석에 감탄했습니다. 웹 성능 최적화를 위한 여러가지 전략 뿐만 아니라 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/ 웹사이트가 더욱 빠르고 안정적인 사용자 경험을 제공할 수 있도록, 현재 배포 중인 서비스에 대한 성능 분석 결과를 핵심 위주로 요약해드렸습니다.

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. 웹사이트 분석

./images/portfolio/1.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은 (이하 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나 DevTools 내 Lighthouse 탭에서는 꽤 좋은 점수가 나오고 있었습니다.

./images/portfolio/3.png

./images/portfolio/4.png

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

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

./images/portfolio/5.png

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

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

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

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

UI.panels.timeline._millisecondsToRecordAfterLoadEvent = 5000

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

./images/portfolio/13.gif

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

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

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

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

./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 메서드를 살펴봐야 합니다. 이 함수는 페이지 로딩 완료 시점을 측정하는 메서드로, 다음 세가지 조건을 만족해야 합니다.

_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로 기록되어있음을 볼 수 있습니다.

./images/portfolio/9.png

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

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

// 블로그 글로 추정컨데 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: hiddenvisible로 변경하며 시야에 노출
<!--다음 예시 코드는 실제 포트폴리오와 상관없이 만든 예제 코드입니다. -->
<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. 첫 방문과 재방문을 구분하는 전략을 도입합니다.

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

// 첫 방문자에게만 전체 인트로 애니메이션을 실행
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 미디어 쿼리입니다. 이를 활용하면 애니메이션이나 전환 효과를 최소화하거나 생략함으로써 모션 민감 사용자를 배려해줄 수 있습니다.
    @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 설정을 통해 쉽게 적용할 수 있습니다:

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

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

./images/portfolio/12.png

./images/portfolio/11.png

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

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

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

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

6. 결론

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

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

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

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

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

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