사용자 제보로 상품 목록 화면의 성능 개선 작업을 진행하고 구성원들에게 아래 내용을 공유했다.
| 구분 | 개발자 PC (나) | 사용자 PC (이슈 리포트) | 비고 |
|---|---|---|---|
| CPU | Apple M3 Pro | Intel Core i5-8400 | 싱글 코어 성능 약 2~3배 차이 |
| RAM | 18GB | 16GB | 멀티태스킹 시 가용 메모리 부족 |
➡️ CPU 6x slowdown 으로 사용자 환경과 유사한 환경 설정 가능
하단의 Lighthouse 측정 결과를 통해 실제 성능 차이를 확인할 수 있음(INP는 약 5배 차이, CLS는 3배 차이). 상단의 이미지가 CPU no-throttle, 하단의 이미지가 CPU 6x slowdown한 결과다.


➡️ 반응성은 5배, 레이아웃 안정성은 3배, 전체 실행 시간 대비 JS 실행 시간 비중이 40% 증가
무한 스크롤이 아닌, 페이지 당 최대 100개 항목을 표시하는 페이지네이션이 적용된 화면이었기에 더 측정 전까지 더 안일하게 생각하던 것도 있었다.
📚 LCP(Largest Contentful Paint)란? *출처
- 페이지에서 가장 큰 콘텐츠(예: 이미지, 텍스트 블록)가 화면에 렌더링되는 데 걸리는 시간
- 이미지 최적화, 서버 응답 속도 개선, 폰트 로딩 최적화로 개선 가능
e.g.) 사용자가 사이트에 들어왔을 때, 화면 대부분을 차지하는 배너 이미지가 2.3초 만에 나타나면 → LCP = 2.3s
📚 INP(Interaction to Next Paint)란? *출처
- 사용자가 버튼을 누르거나 입력을 했을 때, 화면에 반응이 렌더링되기까지 걸린 시간
- JS 최적화, 이벤트 핸들러 경량화, 비동기 처리로 개선 가능
e.g.) 버튼 클릭 후 40ms 만에 새 UI가 뜨면 → INP = 40ms
📚 CLS (Cumulative Layout Shift)란? *출처
- 페이지의 콘텐츠가 예고 없이 움직인 정도, 즉 ‘레이아웃 흔들림’을 측정
- 이미지 크기 고정, 폰트 로딩 최적화, 광고 자리 예약으로 개선 가능
e.g.) 글 읽다가 갑자기 이미지가 로딩되며 아래 텍스트가 내려가면 → CLS ↑ (점수 나빠짐)
아래는 성능 최적화 전 PRD 페이지의 성능을 Lighthouse를 통해 측정한 결과이다. 위에서부터 차례대로 1) 수집상품 목록-카드뷰 2) 등록상품목록 - 카드뷰 3) 수집상품목록 - 리스트뷰 4) 등록상품목록 - 리스트뷰




Google의 RAIL(Response, Animation, Idle, Load) 모델과 Core Web Vitals 가이드를 기준으로 목표 설정

| 지표 | 목표 수치 | 설정 근거 (RAIL 모델 & Web Vitals) |
|---|---|---|
| LCP | 2.5초 이하 | 페이지 로딩 시작 후 주요 콘텐츠가 2.5초 이내에 떠야 사용자가 이탈하지 않고 집중력을 유지 |
| INP | 200ms 이하 | 사용자가 클릭 등 인터랙션을 했을 때, 100ms~200ms 이내에 반응해야 "즉각적이다"라고 느낌. 300ms가 넘어가면 렉이 걸린다고 인지 |
| CLS | 0.1 이하 | 0.1 이하는 시각적으로 안정적인 수준 |
📚 가상화란?
실제로 화면에 보이는 요소만 렌더링 하고 보이지 않는 요소는 렌더링하지 않는 기법



React Profiler를 통해 DOM 업데이트 시간이 오래 걸린 부분의 병목 컴포넌트를 분석한 결과,
➡️ 한꺼번에 렌더링하는 요소를 줄일 수 있는 가상화 우선 적용
이 이유로 가상화 라이브러리는 @tanstack/react-virtual을 선택해서 적용했다.
안정적인 스크롤 경험을 위해 overscan 값을 보수적으로 카드뷰 3행, 리스트뷰 10행으로 설정
| LCP | 🟢 1.59s → 🟢 1.74s | 10% 증가 |
|---|---|---|
| INP | 🔴 899ms → 🟢 83ms | 90.8% 개선 |
| CLS | 🟡 0.16 → 🟢 0.00 | 100% 개선 |
| 메모리 | 약 96.6 MB → 약 68.2 MB | 29.4% 감소 |
| LCP | 🟢 2.11s → 🟢 2.49s | 18% 증가 |
|---|---|---|
| INP | 🔴 2,967ms → 🟡 568ms | 80.9% 개선 |
| CLS | 🟡 0.16 → 🟢 0.02 | 87.5% 개선 |
| 메모리 | 약 97.1 MB → 약 67.5 MB | 30.6% 감소 |
| LCP | 🟡 3.00s → 🟢 1.41s | 47% 개선 |
|---|---|---|
| INP | 🔴 699ms → 🟢 62ms | 91.1% 개선 |
| CLS | 🟡 0.16 → 🟢 0.02 | 87.5% 개선 |
| 메모리 | 약 88.9 MB → 약 58.1 MB | 34.6% 감소 |
| LCP | 🟡 3.26s → 🟢 2.49s | 23.6% 개선 |
|---|---|---|
| INP | 🔴 764ms → 🟢 72ms | 90.6% 개선 |
| CLS | 🟡 0.16 → 🟢 0.02 | 87.5% 개선 |
| 메모리 | 약 86.1 MB → 약 55.7 MB | 35.3% 감소 |
📚 메모이제이션이란?
계산 결과를 메모리에 저장해 두고 동일한 계산이 요청될 때 다시 계산하지 않고 저장된 값을 반환하는 기법
컴포넌트는 내부에 선언된 상태 혹은 부모로부터 내려받은 parameter가 변경되면 리렌더링됨
function IAmYourMother(){
const [money, setMoney] = useState(3000)
return (
<IAmYourDaughter budget={money}/>
// 새로운 3000으로 덮어씌워짐(변경으로 인식)
<button onClick={() => {setMoney(3000)}}>오직 3000원 뿐</button>
)
}
// budget이나 feeling이 변하면 리렌더링
function IAmYourDaughter(budget){
const [feeling, setFeeling] = useState('happy')
const 예산 = budget * 0.3
return (
<div>오늘 제 점심값은 {예산}입니다.</div>
<div>오늘 제 기분은 {feeling}입니다.</div>
)
}
const expectedMargin = useMemo(
() =>
getEstimatedProfit({
price: product.newSalePrice,
purchaseCost: purchaseCost,
}),
[product.newSalePrice, purchaseCost],
// 의존성으로 정의된 부분이 변경될 때만 갱신
)const toggle = useCallback(() => setToggleState((prev) => !prev), [])
const on = useCallback(() => setToggleState(true), [])
const off = useCallback(() => setToggleState(false), [])
const set = useCallback((value: boolean) => setToggleState(value), [])export default React.memo(ProductItemCard)
💡
export const 컴포넌트명 = …&import { 컴포넌트명 } from …⇒ 메모이제이션 되지 않은 원본 사용
export default React.memo(컴포넌트명)&import 컴포넌트명 from …⇒ 메모이제이션 된 버전 사용
🔥 가상화로 인해 스크롤할 때마다 컴포넌트가 리렌더링 → 구독 정보 요청 API가 호출되는 현상 발생
➡️ 파라미터로 구독 정보 전달하도록 구조 수정




📽️ 수집 상품 목록 - 뷰타입 전환 속도 체감 영상
(Before) https://drive.google.com/file/d/1Fyl-XdpmVE9ctkKUxTqhwGOlpZIZhFAO/view?usp=drive_link
(After) https://drive.google.com/file/d/1W1hIV0aZ7CIOiimfywoUgR2wywCE_3PI/view?usp=sharing
📽️ 등록 상품 목록 - 뷰타입 전환 속도 체감 영상
(Before) https://drive.google.com/file/d/1-Bz1Y1npMuHv9rVlMNHgTnQIUxMZNms7/view?usp=drive_link
(After) https://drive.google.com/file/d/1sW_wKwi6owXiqK7tvVdHSZldhSbzddsw/view?usp=sharing
| LCP | 🟢 1.45s → 🟢 1.32s | 9% 개선 |
|---|---|---|
| INP | 🔴 1248ms → 🟢 176ms | 85.9% 개선 |
| CLS | 🟡 0.22 → 🟢 0.01 | 95.5% 개선 |
| 메모리 | 약 103 MB → 약 69 MB | 29.4% 감소 |
| LCP | 🟢 1.67s → 🟢 1.42s | 15% 개선 |
|---|---|---|
| INP | 🔴 10064ms → 🟡 480ms | 95% 개선 |
| CLS | 🟡 0.22 → 🔴 1.60 | |
| 메모리 | 약 124 MB → 약74.6 MB | 39.8% 감소 |
| LCP | 🟡 3.13s → 🟢 1.41s | 55% 개선 |
|---|---|---|
| INP | 🔴 1047ms → 🟢 24ms | 97.7% 개선 |
| CLS | 🟡 0.22 → 🟢 0.01 | 95.5% 개선 |
| 메모리 | 약 71 MB → 약 63.2 MB | 11% 감소 |
| LCP | 🟢 1.45s → 🟢 2.49s | 11.7% 개선 |
|---|---|---|
| INP | 🔴 1256ms → 🟢 48ms | 96.2% 개선 |
| CLS | 🟢 0.01 → 🟡 0.23 | |
| 메모리 | 약 75.8 MB → 약 61.8 MB | 18.4% 감소 |
평균적으로 LCP 22%, INP 93.7%, 메모리는 20% 개선,
CLS는 카드뷰에서는 95.5% 개선되었으나 리스트뷰에서는 성능이 저하된 부분 존재.