The Ultimate React Course 2024: React, Redux & More 의 필기 위주로 작성되었습니다. 해당 강의는 강의 내용 기반으로 블로그 글 작성이 허용된 강의입니다.
강의에 따르면, 크게 3가지 방면에서 성능 개선이 가능하다.
복습겸 보면, 리액트에서 재렌더링이 일어나는 경우는 상태 변화, context 변화와 부모 컴포넌트 재렌더링 3가지 경우다. 이중 prop이 바뀌면 리렌더링이 일어난다는 얘기는 거짓인데, 왜냐면 prop이 바뀌면 parent 재렌더링이 트리거링 되기 때문이다.
Wasted render는 DOM에 아무런 변화를 만들어내지 않은 렌더를 말한다. 보통 리액트는 빠르기에 큰 문제는 아니다만, 너무 자주 일어나거나, 컴포넌트가 느릴 경우 문제가 된다.
// 매우 느린 컴포넌트, wasted render가 일어남
function SlowComponent() {
const words = Array.from({ length: 100_000 }, () => "WORD");
return (
<ul>
{words.map((word, i) => (
<li key={i}>
{i}: {word}
</li>
))}
</ul>
);
}
...
function Counter({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<h1>Slow counter?!?</h1>
<button onClick={() => setCount((c) => c + 1)}>Increase: {count}</button>
{children}
</div>
);
}
...
<Counter>
<SlowComponent />
</Counter>
결과부터 말하자면, SlowComponent를 다른 컴포넌트의 자식으로 두기만 해도 wasted render가 생기지 않는다.
이유를 보자. SlowComponent는 Counter의 children prop으로 들어갔고, 이는 Counter 컴포넌트가 재렌더링 되기 전에 생겼다는 뜻이다. 그래서 SlowComponent는 Counter의 state change에 영향을 받지 않고, 리액트는 결론적으로 SlowComponent를 재렌더링하지 않는다. SlowComponent 내부의 내용은 변화되었을 리가 없기 때문이다!
Context API를 사용할 때도 Provider에서 children prop으로 밑의 컴포넌트들을 받으면, consume하지 않는 한 자식 컴포넌트들은 쓸데없이 재렌더링되지 않는다.
Memoization은 pure function을 한번 실행하여 해당 결과를 메모리에 저장하고, 같은 input을 가진다면 저장된 결과를 캐싱하여 반환하는 기법이다. DP에서 쓰던 단어라 익숙하다. 리액트에선 컴포넌트, 객체, 함수를 memoize 할 수 있다. 이를 사용하면 wasted redner를 방지하고, 앱 속도와 반응 속도도 올릴 수 있다.
부모가 재렌더링 하더라도 prop이 같다면 재렌더링 되지 않을 컴포넌트를 만들때 사용되는 함수다. prop에만 효과가 있고, 다른 컴포넌트들처럼 자기 자신의 state가 바뀌거나 subscribe된 context가 바뀐다면 재렌더링된다. 그렇다고 모든 컴포넌트에 memo를 때리라는건 아니고, 렌더링이 느린 무거운 컴포넌트들에 사용하면 된다.
import { memo } from "react";
const Archive = memo(function Archive() ...)
위처럼 컴포넌트 전체에 memo()
함수를 씌우고 변수에 할당하면 된다.
하지만 memo에는 문제가 존재한다. 자바스크립트에서 두 객체나 함수는 내용이 똑같더라도 다른 객체/함수로 간주되기에 컴포넌트에 객체/함수가 prop으로 전달되면 매번 다른 prop으로 간주되서 memo가 무용지물이 된다. 해결책은 있다. 객체/함수들도 memoize해놔서 재렌더링간에도 stable하도록 해주면 된다. 어떻게?
useMemo는 렌더들 간 보존해두고 싶은 값들을 memoize하고, useCallback은 함수들을 memoize하는데 쓰이는 hooks다.
이 hooks들에 pass된 값들은 캐싱되고, dependency가 똑같다면 재 렌더시에 캐시된 값이 반환된다.
이들은 크게 3가지 상황에 쓰인다:
const archiveOptions = useMemo(() => {
return {
show: false,
title: `Post archive in addition to ${posts.length} main posts`,
};
}, [posts.length]);
const handleAddPost = useCallback(function handleAddPost(post) {
setPosts((posts) => [post, ...posts]);
}, []);
<Archive archiveOptions={archiveOptions}/> // memo 된 컴포넌트
<Main onAddPost={handleAddPost} /> // memo 된 컴포넌트
사용 예시다. 이 hook들은 메모리를 사용하기에, 리렌더링시 확실한 효과가 있는게 아니면 막써서 좋을게 없다.