FE 랜더링 최적화 기법
개요
내가 사용자라고 가정했을 때, 웹 사이트에 접근 시 어떤 경우에 가장 불편함을 느낄까?? 바로 과도한 로딩, 느린 렌더링 이라고 볼 수 있다.
예를 들어 맥도날드에서 햄버거를 주문했다고 가정 해보자
시나리오 1: 느린 렌더링 (느린 서비스)
햄버거를 주문했는데, 직원이 “잠시만요” 라고 하고 햄버거가 20분째 안나옴
사용자는 속으로 “여기 왜 이렇게 느려? 그냥 나갈까?”
시나리오 2: 초고속 렌더링 (빠른 서비스)
햄버거를 주문하자마자 바로 나온다면?
사용자는 속으로 “와! 여기 엄청 빠르네! 다음에도 와야지~~”
와 같은느낌을 받음
결국 빠른 렌더링 = 좋은 사용자 경험 이라고 볼 수 있다
실제로 웹사이트 로딩 속도가 1초 감소할 때 이탈률은 20% 이상 감소하고, 전환율(구매율, 가입률)은 10% 증가 한다는 연구 결과도 존재함
결국 렌더링 최적화는 UX의 핵심 이라고 볼 수 있다
그렇다면 어떻게 최적화를 시킬 수 있을까??
1. VanilaJS 에서의 렌더링 최적화
VanilaJS는 기본적인 JavaScript로 동작하기 때문에 최적화를 위해 직접 DOM 조작을 최소화 하고 효율적인 코드 작성이 요구 된다.
✅ 최적화 기법
1️⃣ DOM 조작 최소화
- DOM 업데이트는 비싸기 때문에 한 번에 여러 요소를 변경하는 것 보다 배치 업데이트(batch update) 를 사용해야 함
- 예제 (안 좋은 코드)
javascriptconst list = document.getElementById("list");
for (let i = 0; i < 1000; i++) {
const item = document.createElement("li");
item.textContent = `Item ${i}`;
}
- 예제 (좋은 코드)
javascriptconst list = document.getElementById("list");
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const item = document.createElement("li");
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
list.appendChild(fragment); // 한 번에 DOM에 추
2️⃣ Reflow & Repaint 최소화
- DOM 변경 시 브라우저가 다시 레이아웃을 계산하는 과정을 줄여야 함
- 예제 (안 좋은 코드 - 매번
offserWidth
접근
javascriptconst el = document.getElementById("box");
el.style.width = "100px";
console.log(el.offsetWidth); // 강제 Reflow 발생
el.style.height = "100px";
- 예제 (좋은 코드 - 클래스 변경 활용)
javascriptel.classList.add("resized"); // CSS에서 .resized 정의해두면 성능 향상
3️⃣ 이벤트 위임(Event Delegation) 활용
- 많은 이벤트 핸들러를 개별적으로 추가하는 대신, 부모 요소에서 이벤트를 관리하면 성능이 개선 됨
javascriptdocument.getElementById("list").addEventListener("click", (event) => {
if (event.target.tagName === "LI") {
console.log(`Clicked: ${event.target.textContent}`);
}
});
2. Vue에서 렌더링 최적화
Vue 의 경우 가상 DOM을 활용하지만, 효율적인 데이터 관리와 최적화 기법을 적용하면 성능을 더 향상 시킬 수 있음
✅ 최적화 기법
1️⃣ v-if vs v-show 적절한 사용
v-if
: 조건이 자주 변경되지 않을 때 사용 (DOM을 완전히 추가/제거)v-show
: 요소를 자주 숨기거나 보여줄 떄 사용 (CSSdisplay
속성 변경)
javascript<div v-if="isVisible">Visible Content</div> <!-- DOM 추가/제거 -->
<div v-show="isVisible">Visible Content</div> <!-- CSS 변경 -->
2️⃣ 컴포넌트 key 값 최적화
- 리스트 렌더링 시
:key
속성을 적절하게 설정하면 재사용이 가능해서 성능이 개선 됨
javascript<div v-for="item in list" :key="item.id">
{{ item.name }}
</div>
3️⃣ Vue computed 속성 활용
computed
는 캐싱되므로 동일한 연산이 여러 번 실행되지 않게 해줌
javascript<template>
<p>결과: {{ optimizedValue }}</p>
</template>
<script>
export default {
data() {
return {
num: 10
};
},
computed: {
optimizedValue() {
console.log("연산 실행됨");
return this.num * 2; // 값이 변경될 때만 실행됨
}
}
};
</script>
4️⃣ Lazy Loading & Code Splitting
defineAsyncComponent
를 사용하면 필요할 때만 컴포넌트를 로드할 수 있음
javascriptimport { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
);
3. React에서 렌더링 최적화
React의 경우 Virtual DOM을 사용하지만 불필요한 렌더링을 줄이는 것이 핵심임
✅ 최적화 기법
1️⃣ useMemo & useCallback 활용
- 불필요한 연산을 줄이고, 메모이제이션 을 활용하면 성능이 향상 됨
javascriptimport { useMemo } from "react";
function ExpensiveComponent({ value }) {
const computedValue = useMemo(() => {
console.log("연산 실행됨");
return value * 2;
}, [value]);
return <p>{computedValue}</p>;
}
useCallback
은 함수 재생성을 방지해줌
javascriptimport { useCallback } from "react";
function MyComponent({ onClick }) {
const handleClick = useCallback(() => {
console.log("버튼 클릭됨");
}, []);
return <button onClick={handleClick}>클릭</button>;
}
2️⃣ React.memo 활용
React.memo
를 사용하면 컴포넌트가 동일한 props를 받을 경우 리렌더링을 방지함
javascriptimport React from "react";
const MyComponent = React.memo(({ name }) => {
console.log("렌더링됨");
return <p>Hello, {name}!</p>;
});
3️⃣ Virtualization 기법 (React Window, React Virtualized)
- 리스트가 많을 경우, 화면에 보이는 항목만 렌더링하면 성능이 좋아짐
javascriptimport { FixedSizeList as List } from "react-window";
function MyList({ items }) {
return (
<List height={400} itemCount={items.length} itemSize={35} width={300}>
{({ index, style }) => <div style={style}>{items[index]}</div>}
</List>
);
}
4️⃣ Lazy Loading & Suspense 사용
- 코드 스플리팅을 적용해 필요한 시점에만 컴포넌트를 로드할 수 있음
javascriptimport React, { lazy, Suspense } from "react";
const LazyComponent = lazy(() => import("./HeavyComponent"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
결론
- VanilaJS : DOM 조작 최소허, Reflow/Repaint 방지, 이벤트 위임 활용
- Vue :
v-if vs v-show
최적 사용,computed
활용,Lazy Loading
적용 - React :
useMemo
,React.memo
,Vitualization
,Lazy Loading
적용
심화. React 좀 더 자세하기 다뤄보기
1. React의 렌더링 원리
✅ (1) Virtual DOM과 Reconciliation
React는 직접 DOM을 변경하지 않고, Virtual DOM(VDOM) 을 사용해서 변경 사항을 효율적으로 관리함
1️⃣ 초기 렌더링 : Virtual DOM 트리를 생성하고, 실제 DOM에 반영 (commit phase)
2️⃣ 업데이트 발생 : 새로운 Virtual DOM을 생성하고 기존 Virtual DOM과 비교 (diffing)
3️⃣ 변경 사항 최소화 : 필요한 부분만 업데이트하고, 변경이 없는 부분은 그대로 유지
✔ Reconciliation 과정
React는 변경된 요소만 업데이트 하기 위해 “재조정(Reconciliation)” 과정을 거침
이때 성능 최적화를 위해 React Fiber 라는 구조를 사용해 작업들 더 효율적으로 처리
2. React의 불필요한 렌더링을 방지하는 방법
React는 기본적으로 부모 컴포넌트가 렌더링 되면 자식도 같이 렌더링 됨
따라서 불필요한 렌더링을 최소화하는 것이 핵심임
✅ (1) React.memo() 사용
React.memo
는 컴포넌트가 동일한 props를 받으면 리렌더링을 방지하는 고차 컴포넌트(HOC)임
💡 HOC란?
고차 컴포넌트(HOC)는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수임고차 컴포넌트는 코드 재사용성을 높이는 데 매우 유용함
여러 컴포넌트에서 공통적으로 사용되는 로직이 있다면, 이를 HOC로 만들어 재사용할 수 있다.
예를 들어, 인증 체크나 권한 관리 등의 로직은 애플리케이션 전반에서 사용될 수 있다. 이러한 로직을 HOC 로 작성하면 중복 코드를 줄일 수 있다.
✔ 사용 예제
javascriptimport React from "react";
const MyComponent = React.memo(({ name }) => {
console.log("렌더링됨");
return <p>Hello, {name}!</p>;
});
export default function App() {
const [count, setCount] = React.useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>클릭 {count}</button>
<MyComponent name="John" />
</div>
);
}
🔥 결과: setCount
로 상태가 변경되더라도 MyComponent
는 name이 변하지 않으면 재렌더링되지 않음.
✔ 주의할 점
-
React.memo
는 얕은 비교(Shallow Compare) 를 하기 때문에, 객체나 배열을 props를 전달할 경우 의미가 없어질 수 있음→ useMemo 또는 useCallback과 함께 사용해야 함
✅ (2) useMemo() 사용
useMemo
는 특정 연산을 캐싱하여 불필요한 연산을 방지하는 Hook임
✔ 사용 예제
javascriptimport React, { useState, useMemo } from "react";
function ExpensiveCalculation(num) {
console.log("무거운 연산 실행됨");
return num * 2;
}
export default function App() {
const [count, setCount] = useState(0);
const [number, setNumber] = useState(5);
const computedValue = useMemo(() => ExpensiveCalculation(number), [number]);
return (
<div>
<button onClick={() => setCount(count + 1)}>카운트 {count}</button>
<button onClick={() => setNumber(number + 1)}>숫자 변경 {number}</button>
<p>결과: {computedValue}</p>
</div>
);
}
🔥 결과:
count
가 변경될 때는ExpensiveCalculation
이 실행되지 않음.number
가 변경될 때만 실행됨 → 렌더링 최적화 효과.
✅ (3) useCallback() 사용
useCallback
은 함수가 불필요하게 재생성되는 것을 방지 하는 Hook임
✔ 사용 예제
javascriptimport React, { useState, useCallback } from "react";
function Button({ onClick }) {
console.log("Button 렌더링됨");
return <button onClick={onClick}>클릭</button>;
}
export default function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
<div>
<p>카운트: {count}</p>
<Button onClick={handleClick} />
</div>
);
}
🔥 결과:
useCallback
을 사용하면handleClick
이 컴포넌트가 다시 렌더링될 때 새로 생성되지 않음.- 따라서
Button
컴포넌트가 불필요하게 다시 렌더링되지 않음.
✅ (4) 리스트 렌더링 최적화 (React Virtualization)
대량의 데이터를 렌더링할 때, 한 번에 모든 항목을 그리면 성능이 저하됨.
→ React Window 같은 라이브러리를 사용해서 가상화(Virtualization) 적용 가능.
✔ 사용 예제
javascriptimport { FixedSizeList as List } from "react-window";
const Row = ({ index, style }) => (
<div style={style}>Item {index}</div>
);
export default function App() {
return (
<List height={400} itemCount={10000} itemSize={35} width={300}>
{Row}
</List>
);
}
🔥 결과:
- 실제 DOM에 10,000개를 렌더링하는 것이 아니라, 보이는 부분만 렌더링하여 성능 최적화
✅ (5) Lazy Loading & Suspense 사용
코드 스플리팅을 통해 필요할 때만 컴포넌트를 로드하면 초기 로딩 속도가 향상됨
✔ 사용 예제
javascriptimport React, { lazy, Suspense } from "react";
const LazyComponent = lazy(() => import("./HeavyComponent"));
export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
🔥 결과:
HeavyComponent
는 사용자가 해당 페이지에 도달할 때만 로드됨 → 초기 렌더링 속도 증가
3. React Fiber를 이용한 최적화 원리
React는 Fiber 알고리즘을 이용해 렌더링을 최적화 함
✅ Fiber의 주요 특징
1️⃣ 작업을 여러 단계로 나눠서 처리 (Time-Slicing)
→ 렌더링 중 사용자 인터랙션이 발생하면 React가 우선순위를 조정 가능.
2️⃣ 렌더링 우선순위 조절 (Concurrent Mode)
→ 긴 작업을 중단하고 더 중요한 작업을 먼저 수행 가능.
3️⃣ Reconciliation 최적화
→ 변경된 부분만 업데이트하여 불필요한 렌더링 방지.
✔ Concurrent Mode 예제
javascriptimport { useState, useTransition } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [isPending, startTransition] = useTransition();
const handleClick = () => {
startTransition(() => {
setCount(count + 1);
});
};
return (
<div>
<button onClick={handleClick}>증가</button>
{isPending ? <p>로딩 중...</p> : <p>Count: {count}</p>}
</div>
);
}
🔥 결과:
startTransition
을 사용하면 React가 렌더링 우선순위를 조정해서 UI가 버벅이지 않음.
📌 결론
🔥 React 렌더링 최적화 핵심 정리
React.memo()
로 불필요한 렌더링 방지useMemo()
로 무거운 연산 캐싱useCallback()
으로 함수 재생성 방지React Window
를 이용한 리스트 최적화lazy()
와Suspense
로 코드 스플리팅- React Fiber의 Time-Slicing & Concurrent Mode 활용