π κ³Όλν μ΄λ²€νΈ νΈλ€λ¬ νΈμΆ: λλ°μ΄μ€/μ°λ‘νλ§μ λΆμ¬
Summary
μ¦μ λΉλλ‘ λ°μνλ μ΄λ²€νΈμ μλ¬΄λ° μ μ½ μμ΄ νΈλ€λ¬λ₯Ό μ°κ²°νλ©΄ μ±λ₯ μ νμ μ¬μ©μ κ²½ν μ νλ₯Ό μ΄λν©λλ€. λλ°μ΄μ±(Debouncing)κ³Ό μ°λ‘νλ§(Throttling)μ μ¬μ©νμ¬ μ΄λ²€νΈ νΈλ€λ¬ μ€ν λΉλλ₯Ό μ΅μ νν¨μΌλ‘μ¨ μ±λ₯μ κ°μ νκ³ λΆλλ¬μ΄ μ¬μ©μ κ²½νμ μ 곡ν΄μΌ ν©λλ€.
Why Wrong?
μ¦μ λΉλλ‘ λ°μνλ μ΄λ²€νΈ(μ: scroll
, resize
, input
, mousemove
)μ μλ¬΄λ° μ μ½ μμ΄ νΈλ€λ¬λ₯Ό μ°κ²°νλ©΄, μ΄λ²€νΈ λ°μ μλ§λ€ ν΄λΉ νΈλ€λ¬κ° μ€νλμ΄ λ€μκ³Ό κ°μ λ¬Έμ λ€μ μΌκΈ°ν©λλ€:
- μ±λ₯ μ ν: μ΄λ²€νΈ νΈλ€λ¬ λ΄λΆμ DOM μ‘°μ, μν μ λ°μ΄νΈ, λ€νΈμν¬ μμ² λ± λΉμ©μ΄ λ§μ΄ λλ μμ λ€μ΄ μ§§μ μκ° μμ λ°λ³΅μ μΌλ‘ μ€νλμ΄ λ©μΈ μ€λ λλ₯Ό κ³ΌλΆνμν€κ³ μ ν리μΌμ΄μ μ μλ΅μ±μ λ¨μ΄λ¨λ¦½λλ€. νΉν μ€ν¬λ‘€ μ΄λ²€νΈμ κ²½μ° μ΄λΉ μμμμ μλ°± λ² λ°μν μ μμ΅λλ€.
- Janky UI (λκΈ°λ μ¬μ©μ κ²½ν): κ³Όλν μ°μ°μΌλ‘ μΈν΄ λΈλΌμ°μ μ λ λλ§ νμ΄νλΌμΈμ΄ λΈλ‘λκ±°λ μ§μ°λμ΄ UIκ° λκΈ°κ±°λ λ²λ² 거리λ νμμ΄ λ°μν©λλ€. μ¬μ©μλ μΉ νμ΄μ§κ° λ리거λ λΆμμ νλ€κ³ λλΌκ² λ©λλ€.
- λΆνμν 리μμ€ μλΉ: μλ²μ λΆνμν μμ²μ 보λ΄κ±°λ, λΆνμν DOM μ λ°μ΄νΈ/νμΈν μ μ λ°νμ¬ λ€νΈμν¬ λμνκ³Ό ν΄λΌμ΄μΈνΈμ CPU, λ©λͺ¨λ¦¬ 리μμ€λ₯Ό λλΉν©λλ€.
- μμΈ‘ λΆκ°λ₯ν λμ: νΉμ μν μ λ°μ΄νΈκ° μ¬λ¬ λ² νΈλ¦¬κ±°λκ±°λ, λΉλκΈ° μμ μ΄ μ€λ³΅μΌλ‘ μμλμ΄ λ°μ΄ν° λΆμΌμΉλ μμμΉ λͺ»ν λ²κ·Έλ₯Ό λ°μμν¬ μ μμ΅λλ€.
How to Fix?
μ΄λ²€νΈ νΈλ€λ¬μ μ€ν λΉλλ₯Ό μ΅μ ννκΈ° μν΄ **λλ°μ΄μ±(Debouncing)**κ³Ό μ°λ‘νλ§(Throttling) κΈ°λ²μ μ¬μ©ν΄μΌ ν©λλ€.
-
λλ°μ΄μ± (Debouncing): νΉμ μ΄λ²€νΈκ° μΌμ μκ° λμ λ€μ λ°μνμ§ μμ λκΉμ§ ν¨μ μ€νμ μ§μ°μν΅λλ€. λ§μ§λ§ μ΄λ²€νΈ λ°μ μμ μΌλ‘λΆν° μ§μ λ
delay
μκ° λμ μΆκ° μ΄λ²€νΈκ° μμΌλ©΄ ν¨μκ° μ€νλ©λλ€. μ£Όλ‘ μ λ ₯ νλ(κ²μ μλ μμ±, μ ν¨μ± κ²μ¬), μλμ° λ¦¬μ¬μ΄μ¦, μ€ν¬λ‘€ μ΄λ²€νΈ μ’ λ£ μμ μ μ μ©ν©λλ€. -
μ°λ‘νλ§ (Throttling): νΉμ μκ°(interval) λμ ν¨μκ° μ΅λ ν λ²λ§ μ€νλλλ‘ μ νν©λλ€. μ΄λ²€νΈκ° μ무리 λΉ λ₯΄κ² λ°μν΄λ μ§μ λ μκ° κ°κ²© λ΄μμλ ν¨μκ° ν λ²λ§ μ€νλ©λλ€. μ£Όλ‘ μ€ν¬λ‘€ μ΄λ²€νΈ(무ν μ€ν¬λ‘€), λ§μ°μ€ μ΄λ, κ²μμ 곡격 λ²νΌ μ°ν λ± μ§μμ μΈ μ΄λ²€νΈμ μ μ©ν©λλ€.
ꡬν λ°©λ²: μΌλ° JavaScript ν¨μλ‘ μ§μ ꡬννκ±°λ, Lodashμ κ°μ λΌμ΄λΈλ¬λ¦¬μμ μ 곡νλ μ νΈλ¦¬ν° ν¨μ(_.debounce
, _.throttle
)λ₯Ό μ¬μ©νλ κ²μ΄ μΌλ°μ μ
λλ€. React νκ²½μμλ useCallback
ν
κ³Ό useRef
λ₯Ό ν¨κ» μ¬μ©νμ¬ λλ°μ΄μ€/μ°λ‘ν ν¨μκ° μ»΄ν¬λνΈ λΌμ΄νμ¬μ΄ν΄ λμ μΌκ΄λκ² μ μ§λλλ‘ κ΄λ¦¬ν΄μΌ ν©λλ€.
Before Code (Bad)
import React, { useState, useEffect } from 'react';
function SearchInput() {
const [searchText, setSearchText] = useState('');
const [results, setResults] = useState([]);
// λ¬Έμ μ : input κ°μ΄ λ³κ²½λ λλ§λ€(κΈμ νλνλ μ
λ ₯ μ) API νΈμΆ
// μ¬μ©μκ° 'hello'λ₯Ό μ
λ ₯νλ©΄ 'h', 'he', 'hel', 'hell', 'hello' μ΄ 5λ² νΈμΆ λ°μ
const fetchSearchResults = async (query) => {
if (query.length < 2) {
setResults([]);
return;
}
console.log(`API νΈμΆ: ${query}`);
// μ€μ API νΈμΆ λ‘μ§ (μμ)
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
setResults(data);
};
const handleChange = (e) => {
const value = e.target.value;
setSearchText(value);
fetchSearchResults(value); // λ¬Έμ μ : λ§€ μ
λ ₯λ§λ€ νΈμΆ
};
return (
<div>
<input
type="text"
value={searchText}
onChange={handleChange}
placeholder="κ²μμ΄λ₯Ό μ
λ ₯νμΈμ"
/>
<ul>
{results.map((item, index) => (
<li key={index}>{item.name}</li>
))}
</ul>
</div>
);
}
export default SearchInput;
```
```jsx
import React, { useEffect, useState } from 'react';
function ScrollTracker() {
const [scrollPos, setScrollPos] = useState(0);
// λ¬Έμ μ : μ€ν¬λ‘€ μλ§λ€ μ΄λ²€νΈκ° λ°μνκ³ , μ΄μ λ°λΌ μν μ
λ°μ΄νΈ λ° λ¦¬λ λλ§μ΄ κ³Όλνκ² λ°μ
const handleScroll = () => {
setScrollPos(window.scrollY);
// μ€ν¬λ‘€ μμΉμ λ°λΌ μΆκ° λ‘μ§ (μ: νΉμ μ€ν¬λ‘€ μμΉμμ UI λ³κ²½ λ±)
console.log('Scroll position:', window.scrollY);
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<div style={{ height: '200vh', paddingTop: '50px' }}>
<h1>μ€ν¬λ‘€ ν
μ€νΈ</h1>
<p>νμ¬ μ€ν¬λ‘€ μμΉ: {scrollPos}</p>
<div style={{ height: '100vh', background: 'lightgray' }}>
μ€ν¬λ‘€νμ¬ μ΄ μμμ μ§λκ°μΈμ.
</div>
</div>
);
}
export default ScrollTracker;
After Code (Good)
import React, { useState, useEffect, useCallback, useRef } from 'react';
import debounce from 'lodash.debounce'; // λλ μ§μ ꡬνν debounce ν¨μ
function SearchInputOptimized() {
const [searchText, setSearchText] = useState('');
const [results, setResults] = useState([]);
const fetchSearchResults = async (query) => {
if (query.length < 2) {
setResults([]);
return;
}
console.log(`API νΈμΆ: ${query}`);
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
setResults(data);
};
// useRefλ₯Ό μ¬μ©νμ¬ debounce ν¨μλ₯Ό μ»΄ν¬λνΈ μμ μ£ΌκΈ° λμ μ μ§
// useEffect λ°μμ μ§μ debounceλ₯Ό νΈμΆνλ©΄ λ λλ§ μλ§λ€ μλ‘μ΄ ν¨μκ° μμ±λμ΄ debounceκ° μ λλ‘ λμνμ§ μμ μ μμ
const debouncedFetchResults = useRef(
debounce((query) => fetchSearchResults(query), 500)
).current;
// μ»΄ν¬λνΈ μΈλ§μ΄νΈ μ debounce νμ΄λ¨Έ ν΄λ¦¬μ΄
useEffect(() => {
return () => {
debouncedFetchResults.cancel(); // Lodash debounceμ cancel λ©μλ
};
}, [debouncedFetchResults]);
const handleChange = (e) => {
const value = e.target.value;
setSearchText(value);
debouncedFetchResults(value); // λλ°μ΄μ€λ ν¨μ νΈμΆ
};
return (
<div>
<input
type="text"
value={searchText}
onChange={handleChange}
placeholder="κ²μμ΄λ₯Ό μ
λ ₯νμΈμ (500ms μ΄ν κ²μ)"
/>
<ul>
{results.map((item, index) => (
<li key={item.id || index}>{item.name}</li>
))}
</ul>
</div>
);
}
export default SearchInputOptimized;
```
```jsx
import React, { useEffect, useState, useRef } from 'react';
import throttle from 'lodash.throttle'; // λλ μ§μ ꡬνν throttle ν¨μ
function ScrollTrackerOptimized() {
const [scrollPos, setScrollPos] = useState(0);
const handleScroll = () => {
setScrollPos(window.scrollY);
console.log('Scroll position:', window.scrollY);
};
// useRefλ₯Ό μ¬μ©νμ¬ throttle ν¨μλ₯Ό μ»΄ν¬λνΈ μμ μ£ΌκΈ° λμ μ μ§
const throttledHandleScroll = useRef(
throttle(() => handleScroll(), 200)
).current; // 200msλ§λ€ μ΅λ 1λ² μ€ν
useEffect(() => {
window.addEventListener('scroll', throttledHandleScroll);
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
throttledHandleScroll.cancel(); // Lodash throttleμ cancel λ©μλ
};
}, [throttledHandleScroll]);
return (
<div style={{ height: '200vh', paddingTop: '50px' }}>
<h1>μ€ν¬λ‘€ ν
μ€νΈ (μ΅μ νλ¨)</h1>
<p>νμ¬ μ€ν¬λ‘€ μμΉ: {scrollPos}</p>
<div style={{ height: '100vh', background: 'lightgray' }}>
μ€ν¬λ‘€νμ¬ μ΄ μμμ μ§λκ°μΈμ.
</div>
</div>
);
}
export default ScrollTrackerOptimized;