오늘은 React.memo함수에 대해 알아보는 시간을 가져보자.
React.memo는 컴포넌트의 불필요한 렌더링을 막음으로써 컴포넌트를 최적화시켜주는 함수이다. 아래의 코드를 살펴보자.
// App.js
const App = () => {
const [items, setItems] = useState(null);
const [query, setQuery] = useState('');
...
const getData = (query) => {
... // query로 영화 데이터 배열을 받아와 setItems(data)를 해주는 함수
};
const onInputChange = (e) => {
setQuery(e.target.value);
};
return (
<Wrapper>
<SearchMovieForm onSubmit={getData}>
<input onChange={onInputChange} value={query} />
<button type="submit">검색</button>
</SearchMovieForm>
{!loading && items ? (
<MovieList items={items} />
) : !items ? (
<MovieMessage>검색을 해 주십시오</MovieMessage>
) : (
<MovieMessage>로딩 중 . . .</MovieMessage>
)}
</Wrapper>
);
}
input에 키워드를 입력하고 submit하면 키워드와 관련된 영화의 데이터 객체를 배열형태로 받아와 items에 넣어주고, MovieList에 props로 넘겨준다.
이 코드의 문제점은 input값이 바뀔 때 마다 query state가 갱신되면서 App이 리렌더링되고, 하위 컴포넌트인 MovieList도 계속 리렌더링 된다는 것이다.
MoiveList는 items state를 props로 받아 그 내용을 화면에 표시해주는 컴포넌트이기 때문에 items가 바뀔 때에만 리렌더링되어야 하고, 그 이외의 상황에서 리렌더링될 필요가 전혀 없다.
이럴 때 사용하는 것이 바로 React.memo이다. 다음 코드를 보자.
// MovieList.js
const MovieList = ({ items }) => {
return (
<MovieListBlock>
{items.map(
({ title, image, userRating, pubDate, director, actor }, i) => (
<MovieListItem
key={i}
image={image}
title={title}
userRating={userRating}
pubDate={pubDate}
director={director}
actor={actor}
/>
)
)}
</MovieListBlock>
);
};
export default React.memo(MovieList); // MovieList를 React.memo로 감싸서 export한다.
단지 export하는 MovieList를 React.memo함수로 감쌌을 뿐이다. 그러나 이것으로 MovieList는 자신의 props인 items가 변할 때에만 리렌더링된다. 이것이 React.memo가 하는 일이다.
React.memo로 감싼 컴포넌트는 리렌더링이 되기 전에 props가 이전에 받은 props와 동일한지 확인하고, 그렇다면 이전에 렌더링 했던 결과를 재사용한다.
그래서 React.memo를 적용한 MovieList는 상위 컴포넌트가 리렌더링되어도 items가 변하지 않았다면 리렌더링되지 않는 것이다.
React.memo 함수는 첫 번째 인자로 메모이제이션할 컴포넌트를 받고, 두 번째 인자로 콜백 함수를 받을 수 있다. 두 번째 인자는 선택사항이다. 공식문서의 예제코드를 보자.
function MyComponent(props) {
/* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
/*
nextProp가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
*/
}
export default React.memo(MyComponent, areEqual);
areEqual 함수가 true를 반환했다면 메모이징된 결과를 렌더링하고, false를 반환했다면 리렌더링한다.
즉, 개발자가 직접 비교 로직을 만들어서 리렌더링 여부를 컨트롤할 수 있게 만들어주는 것이다.
useCallback과 React.memo는 굉장히 합이 잘 맞는 최적화 함수들이다. 아래의 코드를 보자.
// App.js
const App = () => {
const [number, setNumber] = useState(0);
const onRandom = () => {
setnumber(... //난수를 생성하는 로직);
};
return (
<>
<p>{number}</p>
<Button onIncrease={onRandom} />
</>
);
};
// Button.js
const Button = ({ onRandom }) => {
const getRandomcolor = () => {
... // 랜덤한 color hex를 반환
};
useEffect(() => {
console.log("Button 컴포넌트 리렌더링");
});
return (
<button onClick={onRandom} style={{ backgroundColor: getRandomcolor() }}>
증가
</button>
);
};
export default React.memo(Button);
버튼을 누르면 숫자가 1~100사이의 랜덤한 값으로 바뀌는 앱이다. Button은 onRandom함수를 props로 받고 React.memo로 감쌌다. 그리고 Button 컴포넌트가 리렌더링 될 때마다 button의 background-color이 랜덤하게 바뀌게 했다. 나는 button을 눌러 숫자가 바뀌어도 Button 컴포넌트가 리렌더링되지 않는 것을 기대하고 React.memo를 사용하였다. 하지만 결과는 그렇지 못 하다.
Button 컴포넌트에 React.memo를 사용했음에도 불구하고 count가 바뀔 때 마다 Button 컴포넌트는 리렌더링되고 있다. 이것은 무엇을 의미하는가? 바로 Button 컴포넌트가 props로 받고 있는 onRandom함수의 참조가 App이 리렌더링될 때마다 바뀌고 있다는 뜻이다.
여기서 등장하는 것이 바로 useCallback이다. 아래의 코드를 보자.
// App.js
const App = () => {
const [number, setNumber] = useState(0);
// 바뀐 부분
const onRandom = useCallback(() => {
setNumber(Math.floor(Math.random() * 100));
}, []);
return (
<>
<p>{number}</p>
<Button onIncrease={onRandom} />
</>
);
};
// Button.js
const Button = ({ onRandom }) => {
const getRandomcolor = () => {
... // 랜덤한 color hex를 반환
};
useEffect(() => {
console.log("Button 컴포넌트 리렌더링");
});
return (
<button onClick={onRandom} style={{ backgroundColor: getRandomcolor() }}>
증가
</button>
);
};
export default React.memo(Button);
이 코드에서 바뀐 부분은 onIncrease 함수를 useCallback으로 감쌌다는 것 뿐이지만 코드는 내 생각대로 작동하게 된다.
위 코드에서 Callback Hook은 App이 리렌더링될 때마다 첫 번째 인자로 넣은 콜백함수를 메모이징하여 재사용하며, 두 번째 인자로 받는 관계성 배열에 포함된 값이 바뀌면 새로운 함수를 반환한다. 관계성 배열이 비어 있다면 항상 메모이징된 함수를 반환한다.
그렇기 때문에 onRandom함수를 감싼 useCallback은 리렌더링되어도 이전에 쓰던 함수를 재사용하게 되며, Button 컴포넌트는 props로 받는 onRandom함수가 이전에 사용하던 함수이니까 리렌더링되지 않는 것이다.
지금은 고작 button DOM 하나를 최적화시켰을 뿐이지만, 이 원리를 이해했다면 아무리 React앱이더라도 최적화가 가능할 것이다.
오늘은 React.memo와 useCallback, 그리고 그 둘을 사용한 React 앱 최적화에 대해 알아보았다. 그리고 내가 작업하던 클론 프로젝트가 얼마나 개판인지도 알게 되는 시간이었다.
배우면 배울수록 내 모자란 부분이 채워지는 느낌이 듦과 동시에 그만큼 내가 모자랐음을 느끼게 되어 기분이 묘하다.