이 글의 목적은 리렌더링 최적화이다.
사실 최적화의 문제라기 보단 당연한 것인데,
react의 원리를 잘 파악하지 못하거나 신경쓰지 않았다면 놓치기 쉬운 부분이다.
예제 코드는 간단하게 여러장의 카드 중에서 한장을 선택하면 배경색이 바뀌는 로직이다.
// App.js
export default function App() {
const [selected, setSelected] = useState(null);
return (
<div className="grid">
{CARDS.map((c) => (
<Card
key={c.id}
id={c.id}
isSelected={c.id === selected}
onSelect={() => setSelected(c.id)}
/>
))}
</div>
);
}
const CARDS = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4 },
{ id: 5 },
{ id: 6 }
];
// Card.js
const Card = ({ id, isSelected, onSelect }) => {
return (
<div
className="card"
onClick={onSelect}
style={{ backgroundColor: isSelected ? "rgba(0, 0, 0, 0.1)" : "white" }}
/>
);
};
export default Card;
카드 한장을 선택 할때 마다 모든 카드가 리렌더링 되고 있다.
예제에선 카드가 6장 밖에 없어 성능상 큰 문제가 없는데
개발자로 일하면서 마주치게 될 실제 상황 예로는 3000개의 Row를 가진 Table, 3000개의 Item을 가진 List등 에서 각각의 인터랙션이 필요한 상황이 있을 수 있다.
한장 선택 했는데 3000장의 카드가 모두 리렌더링 되는 것은 초과비용이 너무 크다.
컴포넌트의 props 가 바뀌지 않았다면, 리렌더링을 방지한다.
React.memo를 사용하기 위해 Card 컴포넌트의 Props를 확인 해보자.
<Card id={c.id} isSelected={c.id === selected} onSelect={() => setSelected(c.id)} />
id, isSelected, onSelected
3가지 Props중에 카드를 선택 할때 마다isSelected
가 변할 것이다.
카드들의 선택 isSelected
를 배열에 담았다고 가정하면,
1번 카드 선택 - [true,false,false,false,false,false]
2번 카드 선택 - [false,true,false,false,false,false]
위와 같이 1번 카드를 선택한 상태에서 2번 카드를 선택하면 1,2번의 isSelected
만 바뀌는 것이다.
그래서 3,4,5,6 번 카드는 props의 변화가 없으니 React.memo()
를 적용하면 해결 될 것이다.
//
const Card = ({ id, isSelected, onSelect }) => {
{...}
};
export default React.memo(Card);
하지만 변화가 없다. 카드를 선택 할때 마다 모든 카드가 렌더링 된다.
<Card id={c.id} isSelected={c.id === selected} onSelect={() => setSelected(c.id)} />
문제는 onSelect={() => setSelected(c.id)}
여기서 발생한다.
() => setSelected(c.id)
이 함수가 새롭게 생성되는 것이다. (이 문제의 핵심)그래서 카드 한장을 선택 할때 마다 selected
가 변경되어 리렌더링 되면서
onSelect
Props로 전달한 () => setSelected(c.id)
함수가 새로 생성된 함수로 바뀌어
React.memo()
가 의도한 대로 동작하지 않은 것이다.
메모이제이션(memoization)하기 위해서 사용되는 hook 함수.
첫번째 인자로 넘어온 함수를, 두번째 인자로 넘어온 배열 내의 값이 변경될 때까지 저장해놓고 재사용할 수 있다.
const memoizedCallback = useCallback(func, []);
// App.js
export default function App() {
const [selected, setSelected] = useState(null);
// useCallback을 이용한 memoization : 렌더링 되어도 새로 생성하지 않는다.
const onSelect = useCallback((id) => {
setSelected(id);
}, []);
return (
<div className="grid">
{CARDS.map((c) => (
<Card
key={c.id}
id={c.id}
isSelected={c.id === selected}
onSelect={onSelect}
// onSelect={() => onSelect(c.id)}
// props로 화살표 함수를 전달 해도 렌더링 시 새로 생성되기에 onSelect의 인자는 Card(자식)에서 전달하도록 한다.
/>
))}
</div>
);
}
// Card.js
const Card = ({ id, isSelected, onSelect }) => {
// onSelect의 인자는 Card(자식) 컴포넌트에서 전달 한다.
const handleSelect = () => {
onSelect && onSelect(id);
};
return (
<div
className="card"
onClick={handleSelect}
style={{ backgroundColor: isSelected ? "rgba(0, 0, 0, 0.1)" : "white" }}
/>
);
};
export default React.memo(Card);
원하는 대로 이전 선택된 카드, 새로 선택된 카드만 리렌더링 되는 것을 확인 할수 있다.