훅은 개발 경험을 극적으로 향상시켰지만, 동시에 새로운 종류의 성능 문제를 수면 위로 떠오르게 했다. 그 문제는 너무나 미묘해서, 많은 개발자들이 알아차리지 못한 채 지나치곤 했다. 문제의 이름은 ‘불필요한 리렌더링’이었다.
페이스북의 한 개발팀이 부모-자식 관계의 컴포넌트를 만들고 있었다.
부모 컴포넌트 <Dashboard>는 사용자의 활동 로그를 보여주고, 자식 컴포넌트 <LogItem>은 개별 로그 항목을 표시했다.
function Dashboard() {
const [logs, setLogs] = useState([]);
const [filter, setFilter] = useState('all');
// 로그를 삭제하는 함수
const handleDeleteLog = (logId) => {
setLogs(currentLogs => currentLogs.filter(log => log.id !== logId));
};
const filteredLogs = logs.filter(log => filter === 'all' || log.type === filter);
return (
<div>
<FilterButtons onSelect={setFilter} />
{filteredLogs.map(log => (
<LogItem key={log.id} log={log} onDelete={handleDeleteLog} />
))}
</div>
);
}
// LogItem 컴포넌트는 React.memo로 감싸져 있다.
const LogItem = React.memo(function LogItem({ log, onDelete }) {
console.log(`Rendering LogItem: ${log.text}`);
return (
<div>
<span>{log.text}</span>
<button onClick={() => onDelete(log.id)}>Delete</button>
</div>
);
});
개발자는 <LogItem>의 불필요한 리렌더링을 막기 위해, React.memo라는 최적화 도구를 사용했다. React.memo는 컴포넌트를 감싸, props가 변경되지 않는 한 리렌더링을 건너뛰게 만드는 영리한 기능이었다.
모든 것이 완벽해 보였다.
하지만 개발자는 리액트 개발자 도구의 ‘렌더링 하이라이트’ 기능을 켜고 이상한 점을 발견했다.
‘삭제’ 버튼을 눌러 특정 로그를 삭제하는 것은 문제가 없었다. 하지만 상단의 필터 버튼을 눌러 filter 상태를 바꿀 때, 화면에 남아있는 모든 <LogItem> 컴포넌트들이 번쩍이며 리렌더링되고 있었다.
“이해할 수가 없어요.” 개발자가 말했다. “필터를 바꿔도 각각의 <LogItem>이 받는 log prop과 onDelete prop은 변하지 않잖아요. React.memo가 왜 작동하지 않는 거죠?”
그는 console.log를 통해 <LogItem>이 매번 렌더링되고 있음을 확인했다.
log 객체 자체는 변하지 않았지만, React.memo는 리렌더링을 막아주지 못했다.
이 미스터리를 해결하기 위해, 소피 알퍼트가 코드 리뷰에 참여했다. 그녀는 성능 문제에 특히 예민한 감각을 가지고 있었다. 그녀는 문제의 원인을 즉시 파악했다.
“문제는 onDelete prop입니다.”
그녀가 말했다.
“자바스크립트에서 함수는 객체와 같습니다. Dashboard 컴포넌트가 리렌더링될 때마다, 이 handleDeleteLog 함수는 매번 ‘새롭게’ 생성됩니다.”
Dashboard의 filter 상태가 바뀔 때마다, Dashboard 함수 전체가 다시 실행된다. 그 과정에서 handleDeleteLog라는 이름의 함수가 메모리상에 새로 만들어지는 것이다.
이전 렌더링의 handleDeleteLog와 현재 렌더링의 handleDeleteLog는 코드 내용은 똑같지만, 서로 다른 메모리 주소를 가진 별개의 함수였다.
React.memo는 props를 비교할 때, onDelete prop으로 전달된 두 함수가 서로 ‘다르다’고 판단했다. 결국, 모든 <LogItem>은 리렌더링될 수밖에 없었다.
이것은 훅 패러다임이 가진 교묘한 부작용이었다.
함수형 컴포넌트는 그 자체로 렌더링 함수이므로, 렌더링될 때마다 내부의 모든 것이 새로 만들어진다는 사실. 이 사실을 인지하지 못하면, 개발자들은 자신도 모르는 사이에 애플리케이션의 성능을 갉아먹는 코드를 작성하게 될 터였다.
이 보이지 않는 적, 불필요한 리렌더링을 막기 위해서는 새로운 종류의 최적화 도구가 필요했다.
리액트 팀은 이 문제를 해결하기 위해, 두 개의 새로운 훅을 준비하고 있었다. 하나는 함수를 기억하기 위한 훅, 다른 하나는 값을 기억하기 위한 훅이었다.


