유저들은 반응이 빠른 UI를 선호한다. 100ms 미만의 UI 응답 지연은 유저들이 즉시 느낄 수 있고, 100ms에서 300ms가 지연되면 이미 유저들은 상당한 지연으로 느낀다.
UI 성능을 증가시키기 위해, React는 고차 컴퍼넌트(Higher Order Component, HOC) React.memo()
를 제공한다. 렌더링 결과를 메모이징(Memoizing)함으로써, 불필요한 리렌더링을 건너뛴다.
이 글은 언제 React.memo()
가 성능을 향상하는데 도움이 되는지, 언제 불필요한지 구분하는 데 도움이 될 것이다.
추가로 당신이 알아두면 좋을 쓸만한 메모이제이션(Memoization) 팁 또한 설명할 것이다.
React는 먼저 컴퍼넌트를 렌더링(rendering) 한 뒤, 이전 렌더된 결과와 비교하여 DOM 업데이트를 결정한다. 만약 렌더 결과가 이전과 다르다면, React는 DOM을 업데이트한다.
다음 렌더링 결과와 이전 결과의 비교는 빠르다. 하지만 어떤 상황에서는 이 과정의 속도를 좀 더 높일 수 있다.
컴퍼넌트가 React.memo()
로 래핑 될 때, React는 컴퍼넌트를 렌더링하고 결과를 메모이징(Memoizing)한다. 그리고 다음 렌더링이 일어날 때 props
가 같다면, React는 메모이징(Memoizing)된 내용을 재사용한다.
예시를 살펴보자. 함수형 컴퍼넌트 Movie
가 React.memo()
로 래핑 되어 있다.
export function Movie({ title, releaseDate }) {
return (
<div>
<div>Movie title: {title}</div>
<div>Release date: {releaseDate}</div>
</div>
);
}
export const MemoizedMovie = React.memo(Movie);
React.memo(Movie)
는 새로 메모이징된 컴퍼넌트인 MemoizedMovie
를 반환한다. 한 가지 차이점을 제외하고 원래의 Movie
컴퍼넌트와 같은 결과를 나타낼 것이다.
MemoizedMovie
의 렌더링 결과는 메모이징 되어있다. 만약 title
이나 releaseData
같은 props
가 변경 되지 않는다면 다음 렌더링 때 메모이징 된 내용을 그대로 사용하게 된다.
// 첫 렌더이다. React는 MemoizedMovie 함수를 호출한다.
<MemoizedMovie
movieTitle="Heat"
releaseDate="December 15, 1995"
/>
// 다시 렌더링 할 때 React는 MemoizedMovie 함수를 호출하지 않는다.
// 리렌더링을 막는다.
<MemoizedMovie
movieTitle="Heat"
releaseDate="December 15, 1995"
/>
메모이징 한 결과를 재사용 함으로써, React에서 리렌더링을 할 때 가상 DOM에서 달라진 부분을 확인하지 않아 성능상의 이점을 누릴 수 있다.
클래스 컴퍼넌트 또한 PureComponent로 동일한 내용이 구현되어 있다.
React.memo()
는 props 혹은 props의 객체를 비교할 때 얕은(shallow) 비교를 한다.
비교 방식을 수정하고 싶다면 React.memo()
두 번째 매개변수로 비교함수를 만들어 넘겨주면 된다.
React.memo(Component, [areEqual(prevProps, nextProps)]);
areEqual(prevProps, nextProps)
함수는 prevProps
와 nextProps
가 같다면 true
를 반환할 것이다.
예를들어 Movie
의 props
가 동일한지 수동으로 비교해보자.
function moviePropsAreEqual(prevMovie, nextMovie) {
return (
prevMovie.title === nextMovie.title &&
prevMovie.releaseDate === nextMovie.releaseDate
);
}
const MemoizedMovie2 = React.memo(Movie, moviePropsAreEqual);
moviePropsAreEqual()
함수는 이전 props
와 현재 props
가 같다면 true
를 반환할 것이다.
React.memo()
는 함수형 컴퍼넌트에 적용되어 같은 props에 같은 렌더링 결과를 제공한다.
React.memo()
를 사용하기 가장 좋은 케이스는 함수형 컴퍼넌트가 같은 props
로 자주 렌더링 될거라 예상될 때이다.
일반적으로 부모 컴퍼넌트에 의해 하위 컴퍼넌트가 같은 props로 리렌더링 될 때가 있다.
위에서 정의한 Movie
를 다시 사용해서 예시를 들어보자. 여기 Movie
의 부모 컴퍼넌트인 실시간으로 업데이트되는 영화 조회수를 나타내는 MovieViewsRealtime
컴퍼넌트가 있다.
function MovieViewsRealtime({ title, releaseDate, views }) {
return (
<div>
<Movie title={title} releaseDate={releaseDate} />
Movie views: {views}
</div>
);
}
이 어플리케이션은 주기적(매초)으로 서버에서 데이터를 폴링(Polling)해서 MovieViewsRealtime
컴퍼넌트의 views
를 업데이트한다.
// Initial render
<MovieViewsRealtime
views={0}
title="Forrest Gump"
releaseDate="June 23, 1994"
/>
// After 1 second, views is 10
<MovieViewsRealtime
views={10}
title="Forrest Gump"
releaseDate="June 23, 1994"
/>
// After 2 seconds, views is 25
<MovieViewsRealtime
views={25}
title="Forrest Gump"
releaseDate="June 23, 1994"
/>
// etc
views
가 새로운 숫자가 업데이트 될 때 마다 MoviewViewsRealtime
컴퍼넌트 또한 리렌더링 된다. 이때 Movie
컴퍼넌트 또한 title
이나 releaseData
가 같음에도 불구하고 리렌더링 된다.
이때가 Movie
컴퍼넌트에 메모이제이션을 적용할 적절한 케이스다.
MovieViewsRealtime
에 메모이징된 컴퍼넌트인 MemoizedMovie
를 대신 사용해 성능을 향상해보자.
function MovieViewsRealtime({ title, releaseDate, views }) {
return (
<div>
<MemoizedMovie title={title} releaseDate={releaseDate} />
Movie views: {views}
</div>
);
}
title
혹은 releaseDate
props가 같다면, React는 MemoizedMovie
를 리렌더링 하지 않을 것이다. 이렇게 MovieViewsRealtime
컴퍼넌트의 성능을 향상할 수 있다.
컴퍼넌트가 같은 props로 자주 렌더링되거나, 무겁고 비용이 큰 연산이 있는 경우,
React.memo()
로 컴퍼넌트를 래핑할 필요가 있다.
profiling을 통해 React.memo()
의 이점을 측정해봐라
추가적으로 적절한 상황이 있다면 이 글의 댓글로 남겨주기 바란다!
만약 위에서 언급한 상황에 일치하지 않는다면 React.memo()
를 사용할 필요가 없을 가능성이 높다.
경험적으로, 성능적인 이점을 얻지 못한다면 메모이제이션을 사용하지 않는것이 좋다.
성능 관련 변경이 잘못 적용 된다면 성능이 오히려 악화될 수 있다.
React.memo()
를 현명하게 사용하라.
또한, 기술적으로는 가능하지만 클래스 기반의 컴퍼넌트를 React.memo()
로 래핑하는것은 적절하지 않다. 클래스 기반의 컴퍼넌트에서 메모이제이션이 필요하다면 PureComponent
를 확장하여 사용하거나, shouldComponentUpdate()
메서드를 구현하는 것이 적절하다.
렌더링될 때 props
가 다른 경우가 대부분인 컴포넌트를 생각해보면, 메모이제이션 기법의 이점을 얻기 힘들다.
props
가 자주 변하는 컴퍼넌트를 React.memo()
로 래핑할지라도, React는 두 가지 작업을 리렌더링 할 때마다 수행할 것이다.
props
와 다음 props
의 동등 비교를 위해 비교 함수를 수행한다.false
를 반환할 것이기 때문에, React는 이전 렌더링 내용과 다음 렌더링 내용을 비교할 것이다.비교 함수의 결과는 대부분 false
를 반환하기에 props
비교는 불필요하게 된다.
함수 객체는 "일반" 객체와 동일한 비교 원칙을 따른다. 함수 객체는 오직 자신에게만 동일하다.
몇가지 함수를 비교해보자.
function sumFactory() {
return (a, b) => a + b;
}
const sum1 = sumFactory();
const sum2 = sumFactory();
console.log(sum1 === sum2); // => false
console.log(sum1 === sum1); // => true
console.log(sum2 === sum2); // => true
sumFactory()
는 팩토리 함수이다. 이 함수는 2가지 숫자를 더해주는 화살표 함수를 반환한다.
함수 sum1
과 sum2
는 팩토리에 의해 생성된 함수다. 두 함수 모두 두 숫자를 더해주는 함수이다. 그러나 sum1
과 sum2
는 다른 함수 객체이다.
부모 컴퍼넌트가 자식 컴퍼넌트의 콜백 함수를 정의한다면, 새 함수가 암시적으로 생성될 수 있다. 이것이 어떻게 메모이제이션을 막는지 보고, 수정해보자.
Logout
컴퍼넌트는 콜백 prop
인 onLogout
을 갖는다.
function Logout({ username, onLogout }) {
return <div onClick={onLogout}>Logout {username}</div>;
}
const MemoizedLogout = React.memo(Logout);
함수의 동등성이란 함정 때문에, 메모이제이션을 적용할 때는 콜백을 받는 컴퍼넌트 관리에 주의해야한다. 리렌더를 할 때 마다 부모 함수가 다른 콜백 함수의 인스턴스를 넘길 가능성이 있다.
function MyApp({ store, cookies }) {
return (
<div className="main">
<header>
<MemoizedLogout
username={store.username}
onLogout={() => cookies.clear()}
/>
</header>
{store.content}
</div>
);
}
동일한 username
값이 전달되더라고, MemoizedLogout
은 새로운 onLogout
콜백 때문에 리렌더링을 하게 된다.
메모이제이션이 중단되게 되는 것이다.
이 문제를 해결하려면 onLogout
prop의 값을 매번 동일한 콜백 인스턴스로 설정해야만 한다.useCallback()을 이용해서 콜백 인스턴스를 보존시켜보자.
const MemoizedLogout = React.memo(Logout);
function MyApp({ store, cookies }) {
const onLogout = useCallback(() => {
cookies.clear();
}, []);
return (
<div className="main">
<header>
<MemoizedLogout username={store.username} onLogout={onLogout} />
</header>
{store.content}
</div>
);
}
useCallback(() => { cookies.clear() }, [])
는 항상 같은 함수 인스턴스를 반환한다. MemoizedLogout
의 메모이제이션이 정상적으로 동작하도록 수정되었다.
엄밀히 말하면, React에서는 성능 개선을 위한 하나의 도구로 메모이제이션을 사용한다.
대부분의 상황에서 React는 메모이징 된 컴퍼넌트의 리렌더링을 피할 수 있지만, 렌더링을 막기 위해 메모이제이션에 의존하면 안된다.
React.memo()
는 함수형 컴퍼넌트에서도 메모이제이션의 장점을 얻게 해 주는 훌륭한 도구다. 올바르게 적용 된다면 변경되지 않은 동일한 prop에 대해 리렌더링을 하는 것을 막을 수 있다.
다만, 콜백 함수를 prop으로 사용하는 컴퍼넌트에서 메모이징을 할 때 주의하라. 그리고 같은 렌더링을 할 때 이전과 동일한 콜백 함수 인스턴스를 넘기는지 확실히 하라.
그리고 메모이제이션의 성능상 이점을 측정하기 위해 profiling을 사용하는 것을 잊지 말아라.
React.memo()
사용 케이스에 대해 더 궁금한가? 그렇다면 이 글에 댓글을 남겨라!