클라이언트 사이트에서 Framer Motion을 제거하고 번들을 27% 줄였습니다
Source: Dev.to
번역을 진행하려면 번역하고자 하는 전체 텍스트(본문)를 제공해 주시겠어요?
본문을 알려주시면, 원본 형식과 마크다운을 그대로 유지하면서 한국어로 번역해 드리겠습니다.
문제
클라이언트 사이트의 성능 감사를 진행하던 중, 모바일 Lighthouse 점수가 70대 초반에 머물렀고, 데스크톱은 정상(95+)이었습니다.
원인은 **Total Blocking Time (TBT)**였으며, 이는 단일 무거운 의존성인 Framer Motion에 의해 발생했습니다.
우리는 이를 스크롤 시 페이드‑인, 모바일‑네비 슬라이드, 몇 가지 호버 효과 등 총 여섯 개의 간단한 애니메이션에만 사용했지만, 방문자마다 **40 KB+**의 JavaScript가 추가로 다운로드되었습니다.
간단한 UI에 프레이머 모션이 과도한 이유
프레이머 모션은 훌륭한 라이브러리입니다—API가 아름답고 스프링 물리 엔진이 최고 수준입니다. 하지만 일반적인 비즈니스 웹사이트 요구사항(페이드‑인, 슬라이드 전환, 호버 상태)에는 마무리 못을 두드리는 데에 망치를 사용하는 것과 같습니다. 실제 문제는 번들 크기만이 아니라 실행 비용입니다. 프레이머 모션은 매 애니메이션 프레임마다 JavaScript를 실행하는데, 중급 Android 기기에서는 TBT가 증가하고 모바일 Lighthouse 점수가 낮아집니다.
더 간단한 솔루션: CSS 전환 + IntersectionObserver
스크롤 시 페이드‑인 (Framer Motion 버전)
import { motion } from 'framer-motion';
function FadeInSection({ children }) {
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.1 }}
>
{children}
</motion.div>
);
}스크롤 시 페이드‑인 (CSS + IntersectionObserver)
styles.css
.fade-in {
opacity: 0;
transform: translateY(16px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}Component
import { useEffect, useRef } from 'react';
function FadeInSection({ children }) {
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
},
{ threshold: 0.1 }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<div ref={ref} className="fade-in">
{children}
</div>
);
}두 접근 방식 모두 동일한 시각적 결과를 제공하지만, CSS 버전은 애니메이션 중 자바스크립트 실행이 전혀 없습니다—브라우저의 컴포지터가 GPU에서 전환을 처리하여 메인 스레드 차단 및 TBT 영향을 없앱니다.
모바일 네비게이션 슬라이드 (Framer Motion 버전)
{/* Example placeholder – original Framer Motion code omitted */}모바일 네비게이션 슬라이드 (CSS만 사용)
/* nav.css */
.mobile-nav {
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.mobile-nav.open {
transform: translateX(0);
}스프링 물리 효과는 사라지지만, 열리기 전에 40 KB의 자바스크립트를 전송하지 않는 네비게이션을 얻을 수 있습니다.
호버 효과 (CSS)
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}React 재렌더링도 없고, 자바스크립트도 없습니다—그저 CSS가 본래의 역할을 수행합니다.
Results After Removing Framer Motion
| Metric | Before | After |
|---|---|---|
| Bundle size | – | ‑27 % |
| Mobile Lighthouse Performance | 72 | 89 |
| Total Blocking Time | – | ‑≈180 ms |
| Desktop Lighthouse | 100/100/100/100 | unchanged |
| Time to Interactive (mobile) | – | ‑0.4 s |
사이트는 동일하게 보입니다: 애니메이션이 실행되고, 모바일 네비게이션이 슬라이드하며, 카드가 호버 시 들어올립니다. 차이점은 브라우저가 이제 애니메이션을 컴포지터 스레드에서 처리하고, 40 KB JavaScript 페이로드가 사라졌다는 점입니다.
프레이머 모션이 여전히 의미가 있을 때
- 레이아웃 애니메이션 (
layoutprop) – CSS는 DOM 위치 변화 사이를 애니메이션할 수 없습니다. - AnimatePresence를 사용한 종료 애니메이션 – CSS에는 “언마운트 시 애니메이션”이라는 개념이 없습니다.
- 복잡한 조정 시퀀스는 조화된 스프링 물리를 필요로 합니다.
- 드래그 및 제스처 인터랙션 – 여기서 프레이머 모션이 진정으로 빛을 발합니다.
창의적인 포트폴리오, 복잡한 UI 전환을 갖춘 SaaS 제품, 혹은 드래그‑앤‑드롭에 의존하는 모든 인터페이스를 구축한다면, 프레이머 모션의 번들 비용은 정당화됩니다.
요약
일반적인 비즈니스 웹사이트—랜딩 페이지, 서비스 페이지, 마케팅 사이트—에 간단한 페이드‑인과 호버 효과만을 위해 Framer Motion을 가져오는 것은 자전거에 레이스카 엔진을 장착하는 것과 같습니다. npm 의존성 하나하나가 방문자가 페이지를 로드할 때마다 내는 세금이 됩니다. 제가 지금 따르는 규칙은 다음과 같습니다:
- CSS first 로 전환 효과와 호버 상태를 구현합니다.
- 스크롤에 따라 트리거되는 애니메이션은 IntersectionObserver 를 사용합니다.
- CSS 로는 구현할 수 없는 경우에만 JavaScript animation library 를 사용합니다.
그 결과: 번들 크기 27 % 감소, 모바일 Lighthouse 점수 17점 상승, 그리고 동일한 시각적 경험—추가 라이브러리가 전혀 필요하지 않았습니다.