개요
Sentry에서 INP(Interaction to Next Paint) 지연 경고가 간헐적으로 발생하고 있다. 로그를 분석해 보니 특정 페이지(eventDetail)에서 사용자의 클릭 반응 속도가 눈에 띄게 느려지는 현상이 포착되었다.
범인을 추적해 보니 피크(Peak)가 튀는 순간은 MUI(Material UI) 컴포넌트가 렌더링 될 때였다.
Deep Dive : 왜 MUI v5가 범인인가?
최적화를 시작하기 전에, 도대체 왜 MUI v5가 느린지 근본적인 원인을 파악해 보자. 범인은 바로 편리함의 상징인 sx 문법이다. v5의 sx가 문제라는 것이지 지금 버전의 sx도 문제라고 오해하지 않는 것이 좋다.
런타임 오버헤드 (Runtime Overhead)
우선 sx로 작성된 스타일이 실제 DOM에 적용되기까지는 꽤 복잡한 과정을 보자. 단순히 sx문법을 css로 변환하여 적용한다 라는 추상적인 내용보다는 조금 더 복잡한 일이 수행이 된다. ▼
- sx 문법 파싱
- Emotion이 이해할 수 있는 객체로 변환
- 직렬화(Serialization) 및 해싱
- DOM에 스타일 주입

MUI는, 정확히는 MUI v5는 스타일링 엔진으로 Emotion css을 사용한다. 그리고 Emotion은 런타임 css로 직렬화 및 해싱하는 작업을 런타임에 실행한다. 그리고 모든 문제는 이 런타임에서 발생한다. sx도 파싱하고 변환하는 작업을 런타임에 수행하기 때문이다.
리렌더링이 일어날 때 마다 Emotion css의 연산 작업에 sx 변환 작업까지 일어나게 되어 오버헤드가 발생하게 되는 것이다.
리렌더링을 일으키는 것들 (feat. Ripple 효과)
"그렇다면 리렌더링만 안일어나면 되는거 아닌가?"
하지만 MUI에는 리렌더링을 계속해서 일으키는 것이 있으니, Ripple 효과다. Material Design의 상징인 Ripple은 시각적으로는 훌륭하지만 성능에는 악영향을 준다. Ripple은 클릭 시 내부적으로 DOM 업데이트와 리렌더링을 유발하는데, 앞서 말한 sx 오버헤드와 맞물려 순간적인 프레임 드랍을 일으킨다. ▼

"그러면 Ripple만 제거하면 되겠네?"
일반적으로는 그렇다. Ripple 효과만 제거해도 성능이 꽤나 괜찮아진다. 하지만 컴포넌트 수가 많은 것도 문제다. 한 페이지에 MUI 컴포넌트가 많이 존재하게 되면, 각각의 sx를 해석하느라 기본적인 성능 자체가 나빠질 수 있다.
해당 부분은 MUI 공식에서도 언급한 바가 있다. 물론 일반적으로는 1000개의 Primitive 컴포넌트들을 넣는 일은 없겠지만, 이론적으로 그정도 들어갔을 때 렌더링 시간이 오래 걸린다고 나와있었다. ▼
Usage - MUI System
Learn the basics of working with MUI System and its utilities.
mui.com
그 외에도 Stack Overflow에서 MUI의 sx 문법에 대해 문제가 야기된 적이 있었고, 해당 글에서 실험한 결과 emotion_css가 가장 빨랐다고도 나왔다. ▼
Why is the `sx` prop so much slower?
As per MUI's own doco, and this answer - components using sx render significantly slower than components using other styling mechanisms. On the surface, it looks like sx is just an alternate conven...
stackoverflow.com

이론적인 부분을 생각해보면 당연한 결과인 게, emotion 엔진이 이해하기 쉽게 변환하는 과정을 거치지 않기에 더 빨라질 수 밖에 없다.
MUI v6는 다른가?
앞에서부터 계속 MUI 'v5' 라고 구분지어 이야기하는데, 그러면 v6는 다른 건가? 의문이 들 수 있다. 그 대답은 그렇다이다.
MUI v6부터 도입된 Pigment css는 Zero-runtime이다. sx 문법을 빌드 타임에 미리 해석해서 정적 css 파일로 만들어버린다. 즉, 런타임에는 무거운 JS 연산 없이 브라우저가 css만 읽으면 되므로 성능 이슈가 해결된다. 그렇기에 빌드 시간이 더 오래 걸릴 지는 몰라도 최소한 런타임에서의 오버헤드는 없게 되는 것이다.
의사결정 1 : 마이그레이션 (기각)
가장 확실한 해결책은 Pigment CSS를 지원하는 v6나 v7으로 마이그레이션 하는 것이다. 공식 문서를 확인해 보았다. ▼
Upgrade to v6 - Material UI
This guide explains why and how to upgrade from Material UI v5 to v6.
mui.com
Upgrade to v7 - Material UI
This guide explains how to upgrade from Material UI v6 to v7.
mui.com
최소 버전 충족
마이그레이션을 할 때 TS와 React 버전이 문제가 되지 않을까 싶었는데, 다행히도 버전은 꾸준히 업데이트를 하여 문제는 되지 않았다.
TS의 경우 4.7버전 이상을 요구했고, React는 19 버전을 권장했다. 18 이하의 버전이라고 해도 `react-is`를 React와 같은 버전으로 설치를 하면 문제없이 사용이 가능하다.
문제점: Breaking Changes가 너무 많다.
Grid2, Typography, Accordion 등 프로젝트 전역에서 쓰이는 핵심 컴포넌트들의 스펙이 대거 변경되었다. 또한 버전을 올린다고 자동으로 Pigment CSS가 적용되는 게 아니라 별도의 설정이 필요하다.
결론
마이그레이션은 현재 상황에서 리스크가 너무 큰 대공사다. 사이드 이펙트를 감당할 수 없으므로 기각한다.
의사결정 2 : 현실적 최적화 (채택)
버전을 올릴 수 없다면, 현재 버전(v5) 안에서 런타임 비용을 줄이는 방법을 택해야 한다. 전략은 다음과 같다.
- SX 제거: 런타임 오버헤드가 심한 sx를 emotion css로 교체한다.
- Ripple 제거: 무거운 물결 효과를 끈다.
- 메모이제이션: 불필요한 리렌더링 자체를 막는다.
작업 1 : SX -> Emotion CSS 교체
공식 문서와 벤치마크 자료에 따르면, 다수의 컴포넌트 렌더링 시 sx보다 emotion의 성능이 월등히 좋다. 변환 작업이 사라져 오버헤드가 줄어들기 때문이다. 기존 sx 코드를 emotion의 css prop이나 스타일 객체로 변환했다. ▼
// Before: 런타임 오버헤드 발생
<Box sx={styles.statContainer}>
<Box sx={styles.statWrapper}>
// After: Emotion CSS 사용 (오버헤드 감소)
<Box css={styles.statContainer}>
<Box css={styles.statWrapper}>
style 인라인 속성으로 작성된 부분도 일관성을 위해 css prop으로 통일했다. ▼
// Before
<GtmBox style={{ cursor: "pointer" }}>
<ShareIcon style={styles.shareIcon(snackbarShow)} />
</GtmBox>
// After
<GtmBox css={styles.gtmBox}>
<ShareIcon css={shareIconStyle} />
</GtmBox>
작업 2 : Ripple 제거 및 메모이제이션
가장 큰 부하를 주는 Ripple을 끄고(disableRipple), 핸들러와 스타일 객체를 메모이제이션하여 리렌더링 방어선을 구축했다. ▼
// 스타일 메모이제이션
const detailItemStyle = useMemo(
() => styles.item(activeId === 0, true),
[activeId]
);
// 핸들러 메모이제이션
const handleDetailClick = useCallback(() => handleClick(0), [handleClick]);
// ...
<Button
disableRipple={true} // Ripple 제거
css={detailItemStyle} // sx 대신 css 사용
onClick={handleDetailClick} // 메모이제이션 된 핸들러
>
마치며
INP는 로컬 환경에서 정확히 테스트하기 어렵고, 사용자 기기 성능에 따라 편차가 크다. 비록 드라마틱한 마이그레이션은 하지 못했지만, sx 제거와 Ripple 차단이라는 현실적인 최적화를 통해 런타임 오버헤드를 최소화했다. 정확한 성과는 상용 배포 후 Sentry Performance 탭을 통해 검증해 볼 예정이다.
때로는 대단한 신기술 도입보다, 현재 시스템의 비효율을 걷어내는 것이 더 나은 엔지니어링일 때가 있다.
'Develop > Web' 카테고리의 다른 글
| [Web] JSDoc과 Storybook으로 컴포넌트 문서화 (0) | 2025.12.25 |
|---|---|
| [Web] 시맨틱 태그 적용 작업 (3) | 2025.12.12 |
| [JS] Named export와 Default export, 둘 중에 뭘 써야할까? (0) | 2025.12.09 |
| [React][Issue] 간단한 알고리즘과 함께 말줄임 디테일 챙기기 (0) | 2025.12.01 |
| [React] Spread Attributes를 조심해야 하는 이유 (0) | 2025.11.26 |