전편에서는 리액트는 비교를 통해서 성능 최적화를 하기 때문에, 자바스크립트에서는 어떤 비교가 있고, 비교의 개념과 그 코드는 어떻게 구성되는지 알아보았습니다.
그렇다면 이제는 실제로 어떻게 적용되어 성능 최적화에 기여하는지 알아보도록 하겠습니다.
혹시나 잊으셨을까봐 전편글 링크도 함께 첨부합니다.
[전편 글][https://velog.io/@carloskim/깊은-비교와-얕은-비교-맛보기-上](https://velog.io/@carloskim/%EA%B9%8A%EC%9D%80-%EB%B9%84%EA%B5%90%EC%99%80-%EC%96%95%EC%9D%80-%EB%B9%84%EA%B5%90-%EB%A7%9B%EB%B3%B4%EA%B8%B0-%E4%B8%8A)
[리액트에서 얕은 비교 코드]
https://github.com/facebook/react/blob/main/packages/shared/shallowEqual.js
//hasOwnProperty.js
//객체가 특정 프로퍼티를 자신의 직접적인 프로퍼티로 가지고 있는지 확인하는 메서드입니다.
const hasOwnProperty = Object.prototype.hasOwnProperty;
export default hasOwnProperty;
//objectIs.js
//동등 비교에서 발생하는 특수한 경우들을 올바르게 처리하기 위한 함수입니다.
// +0 === -0 (false)의 처리 등..
// NaN === NaN (false) 에 대한 처리
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const objectIs: (x: any, y: any) => boolean =
// $FlowFixMe[method-unbinding]
typeof Object.is === 'function' ? Object.is : is;
export default objectIs;
//shallowEqual.js
import is from './objectIs'; // 정확한 값 비교를 위한 유틸리티
import hasOwnProperty from './hasOwnProperty'; // 안전한 프로퍼티 존재 확인
/**
* 두 객체 간의 얕은 비교를 수행하는 함수
* @param {mixed} objA 비교할 첫 번째 객체
* @param {mixed} objB 비교할 두 번째 객체
* @returns {boolean} 두 객체가 얕은 수준에서 동일한지 여부
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
// 1. 완전히 동일한 객체인지 먼저 확인
// Object.is를 사용하여 +0/-0, NaN 등의 엣지케이스 처리
if (is(objA, objB)) {
return true;
}
// 2. null 체크 및 타입 체크
// 둘 중 하나라도 객체가 아니거나 null이면 false 반환
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
// 3. 객체의 키 배열 생성
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// 4. 키의 개수가 다르면 두 객체는 다름
if (keysA.length !== keysB.length) {
return false;
}
// 5. objA의 모든 키에 대해 검사
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
// 두 가지 조건 체크:
if (
// 5-1. objB에 해당 키가 존재하는지 확인
// hasOwnProperty.call을 사용하여 프로토타입 체인 오염 방지
!hasOwnProperty.call(objB, currentKey) ||
// 5-2. 해당 키의 값이 양쪽 객체에서 동일한지 확인
// Object.is를 사용하여 정확한 값 비교
// $FlowFixMe[incompatible-use] - Flow 타입 체커 경고 무시
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
// 모든 검사를 통과하면 두 객체는 얕은 수준에서 동일
return true;
}
export default shallowEqual;
하지만 찾아보다 보니 shallowCompare add-on은 제거되고, react.memo와 PureComponent가 표준이 되었습니다. 얕은 비교하는 방식 자체가 사라진 것은 아니지만 다른 방식을 채택하고 있습니다.
또한 PureComponent도 마찬가지로 class형 컴포넌트에서만 사용되기 때문에 제외하고 React에서 얕은 비교가 사용되는 사례들을 한 번 알아보도록 하겠습니다.
// 부모 컴포넌트
function ExpensiveListContainer() {
const [searchTerm, setSearchTerm] = useState("");
const [selectedFilter, setSelectedFilter] = useState("all");
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<ExpensiveItemList // 이 컴포넌트를 메모이제이션
filter={selectedFilter}
items={someExpensiveItems}
onItemClick={handleItemClick}
/>
<SimpleFooter /> // 이 컴포넌트는 메모이제이션 불필요
</div>
);
}
// 최적화가 필요한 복잡한 컴포넌트
const ExpensiveItemList = React.memo(function ExpensiveItemList({
filter,
items,
onItemClick
}) {
// 복잡한 필터링 로직
const filteredItems = useMemo(() => {
return items.filter(item => /* 복잡한 필터링 */);
}, [items, filter]);
return (
<div>
{filteredItems.map(item => (
<div key={item.id}>
{/* 복잡한 렌더링 로직 */}
</div>
))}
</div>
);
});
React.memo로 컴포넌트를 래핑하면 해당 컴포넌트는 렌더링하고, 결과를 메모이징합니다. 그리고 다음 렌더링이 일어날 때 props가 같다면 메모이징된 내용을 재사용합니다!
언제 사용할까?
만약 위의 조건들에 부합하지 않다면, 사용하지 않는게 좋습니다! 만약 Props가 자주 달라지는 컴포넌트에
memo를 래핑했다고 했을 때, 리액트는 두 가지 작업을 수행하는데
비교 함수의 결과는 대부분 false 를 반환하여 props비교는 불필요하게 되고, 결국에는 불필요한 memoization이 되겠죠?
function ProductList({ products, category, searchTerm }) {
// 1. 기본적인 사용
const filteredProducts = useMemo(() => {
return products.filter(product =>
product.category === category &&
product.name.includes(searchTerm)
);
}, [products, category, searchTerm]); // 이 값들이 변경될 때만 필터링 실행
// 2. 객체 메모이제이션
const productStats = useMemo(() => ({
total: products.length,
inStock: products.filter(p => p.stock > 0).length,
averagePrice: products.reduce((acc, p) => acc + p.price, 0) / products.length
}), [products]); // products가 변경될 때만 통계 재계산
return (
<div>
<div>총 상품 수: {productStats.total}</div>
<div>재고 있는 상품: {productStats.inStock}</div>
<ProductGrid products={filteredProducts} />
<CategoryStats data={sortedAndGrouped} />
</div>
);
}
useMemo hook은 리렌더링 사이에 계산 결과를 캐싱할 수 있게 해주는 React Hook인데요,
첫 번째 인자는 메모이제이션 하고싶은 값을 계산하는 순수함수.
두 번째 인자는 콜백함수가 사용하는 의존성들의 배열입니다. 이 값들이 변경될 때 콜백함수가 실행됩니다.
useMemo는 리렌더링이 발생하면 → 리액트가 의존성 배열의 각 값을 이전 렌더링 값과 얕은 비교를 하고 →
모든 의존성이 같다면 → 이전의 메모이제이션된 값을 재사용!
하나라도 다르다면 → 콜백 함수를 실행하여 새 값을 계산하고 메모이제이션합니다.
function Cart({ items, user }) {
// 🚫 잘못된 예시: 객체를 직접 의존성으로 사용
useEffect(() => {
const total = calculateTotal(items);
saveToDatabase({ user, total });
}, [{ user }]); // 매 렌더링마다 새로운 객체가 생성되어 항상 실행됨
// ✅ 올바른 예시: 필요한 속성만 의존성으로 사용
useEffect(() => {
const total = calculateTotal(items);
saveToDatabase({ user, total });
}, [user.id, items]); // 실제로 변경된 경우에만 실행
// 🚫 잘못된 예시: 불필요한 의존성
useEffect(() => {
console.log('User logged in');
}, [user]); // user 객체의 모든 변경에 반응
// ✅ 올바른 예시: 필요한 값만 의존성으로 사용
useEffect(() => {
console.log('User logged in');
}, [user.isLoggedIn]);
}
useEffect는 너무 유명하기 때문에, 어떤 기능을 하는지는 따로 말씀 안드리겠습니다 ㅎㅎㅎ..
혹시나 모르신다면.. 여기 를 클릭해주세요
useEffect가 얕은 비교를 사용하는 동작순서는 이러합니다.
컴포넌트 리렌더링 발생 → 리액트가 의존성 배열의 각 값과 이전 렌더링의 값을 얕은 비교(===)
비교 결과에 따라
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 1. 기본적인 useCallback 사용
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // 의존성이 없으므로 항상 같은 함수 참조 유지
// 2. 의존성이 있는 useCallback
const handleSearch = useCallback((searchTerm) => {
console.log(`Searching for ${searchTerm} in ${text}`);
}, [text]); // text가 변경될 때만 새로운 함수 생성
// 3. 자식 컴포넌트에 전달되는 콜백
const handleItemSelect = useCallback((itemId) => {
console.log(`Selected item: ${itemId}`);
setCount(c => c + 1);
}, []); // count는 setState 함수 형태로 사용되므로 의존성 불필요
return (
<div>
<ExpensiveChild onItemSelect={handleItemSelect} />
<SearchComponent onSearch={handleSearch} />
<button onClick={handleClick}>Count: {count}</button>
</div>
);
}
컴포넌트 리렌더링 발생 → React는 의존성 배열의 각 값을 이전 렌더링의 값과 얕은 비교(===)
React의 성능 최적화 전략의 핵심에는 얕은 비교(shallow comparison)가 자리 잡고 있습니다. 이를 통해 React는 효율적으로 컴포넌트의 리렌더링 여부를 결정합니다. 우리는 이번 탐구를 통해:
얕은 비교와 깊은 비교의 차이점을 이해함으로써, 우리는 React의 내부 작동 방식에 대한 더 깊은 통찰을 얻었습니다. 이는 우리가 더 효율적이고 성능이 뛰어난 React 애플리케이션을 개발하는 데 도움이 될 것 같아요! .추가적으로 React의 Reconciliation 과정에 대해 더 자세히 탐구하여, 얕은 비교가 이 과정에서 어떻게 활용되는지 더욱 깊이 이해하고자 합니다. 이를 통해 React의 성능 최적화 전략을 더욱 효과적으로 활용할 수 있을 것입니다.이번 학습을 통해 React의 성능 최적화 전략을 더 깊이 이해할 수 있었던 것 같습니다!
[React.memo] https://ui.toast.com/weekly-pick/ko_20190731