최적화란
리액트에서 최적화란 불필요한 연산이나 렌더링을 방지하여, 애플리케이션의 성능을 개선하는 것을 말한다.
useMemo,React.memo,useCallback
리액트 18이상의 버전부터는 개선된 렌더링 전략이 사용되므로 위 훅의 필요성이 예전보다 많이 줄어들었다. 그래서 꼭 필요한 경우 아니면 사용할 필요가 없다.
useMemo
복잡한 계산 결과를 메모이제이션(캐싱)하여 불필요한 재계산을 방지하는 훅이다.
import React, { useState, useMemo } from "react";
function ExpensiveCalculation() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(0);
// 복잡한 계산 - count가 변경될 때만 재계산됨
const expensiveResult = useMemo(() => {
console.log("복잡한 계산 실행!");
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += count;
}
return result;
}, [count]); // count가 변경될 때만 재계산
return (
<div>
<p>Count: {count}</p>
<p>계산 결과: {expensiveResult}</p>
<button onClick={() => setCount(count + 1)}>Count 증가</button>
<button onClick={() => setOtherState(otherState + 1)}>
다른 상태 변경 (계산 안함)
</button>
</div>
);
}
언제 써야할까? 계산 비용이 정말 큰 연산이 있을 때만 사용해라. 대부분의 경우 그냥 일반 변수로 충분하다.
React.memo
부모의 리렌더링으로 인하여 자식까지 리렌더링되는 그 불필요함을 방지 컴포넌트를 메모이제이션하여 props가 변경되지 않으면 리렌더링을 방지한다.
import React, { useState, memo } from "react";
// 자식 컴포넌트 - React.memo로 감싸서 최적화
const ExpensiveComponent = memo(({ name }) => {
console.log("ExpensiveComponent 렌더링:", name);
// 무거운 렌더링을 시뮬레이션
let startTime = performance.now();
while (performance.now() - startTime < 100) {
// 100ms 동안 아무것도 하지 않음 (렌더링이 무거운 것처럼)
}
return <div>이름: {name}</div>;
});
// 부모 컴포넌트
function ParentComponent() {
const [name, setName] = useState("철수");
const [counter, setCounter] = useState(0);
return (
<div>
<button onClick={() => setName(name === "철수" ? "영희" : "철수")}>
이름 변경
</button>
<button onClick={() => setCounter(counter + 1)}>
카운터 증가 (자식은 리렌더링 안됨)
</button>
<p>카운터: {counter}</p>
<ExpensiveComponent name={name} />
</div>
);
}
언제 써야할까? 정말 렌더링 비용이 큰 컴포넌트가 불필요하게 자주 리렌더링될 때만 사용해라.
useCallback
React.memo로 최적화된 프롭스라고해도 함수를 프롭스로 넘기면 리렌더링 된다. 그떄 그 함수를 메모이제이션하여 불필요한 함수 재생성을 방지한다.
import React, { useState, useCallback } from "react";
function SearchComponent() {
const [query, setQuery] = useState("");
const [searchHistory, setSearchHistory] = useState([]);
// useCallback으로 함수 메모이제이션
// 의존성 배열이 변경되지 않는 한 같은 함수 인스턴스 유지
const handleSearch = useCallback(() => {
console.log("검색 실행:", query);
setSearchHistory((prev) => [...prev, query]);
// API 호출 등의 로직...
}, [query]); // query가 변경될 때만 함수 재생성
// 이벤트 핸들러도 메모이제이션 가능
const handleInputChange = useCallback((e) => {
setQuery(e.target.value);
}, []); // 의존성 없음 - 컴포넌트 생명주기 동안 같은 함수 유지
return (
<div>
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="검색어 입력"
/>
<button onClick={handleSearch}>검색</button>
<div>
<h3>검색 기록:</h3>
<ul>
{searchHistory.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
</div>
);
}
언제 써야할까? 주로 자식 컴포넌트에 함수를 props로 전달할 때, 특히 그 자식이 React.memo로 최적화되어 있을 때만 필요하다. 아니면 의존성 배열에 함수를 넣어야 할 때도 사용한다.
결론
리액트 18에서는 자동 배칭(Automatic Batching)과 같은 개선된 렌더링 전략 덕분에 이러한 최적화의 필요성이 줄었다. 성능 문제가 실제로 측정되기 전까지는 과도한 최적화를 피하는 것이 좋다. 코드가 복잡해지고 가독성이 떨어질 수 있기 때문이다. 정말 필요한 경우에만 사용하자!