React에서 컴포넌트는 다음과 같은 상황에 리렌더링(re-rendering)된다.
state
가 변경될 때props
가 변경될 때불필요한 렌더링을 방지하는 방법은 다음과 같다.
리액트는 특정 state가 변경되면 그 state가 선언된 컴포넌트와 그 하위 컴포넌트들을 모두 리렌더링 시킨다. 따라서 state가 선언되는 위치를 잘 설계하는 것은 리렌더링 횟수에 엄청난 영향을 끼친다. 기본적으로 state의 선언 위치는 해당 state를 사용하는 컴포넌트들을 잘 구분해 놓은 뒤 가장 최상위 컴포넌트에 선언한다. 만약 그 state를 사용하는 최상위 컴포넌트보다 더 상위 컴포넌트에 state를 선언하면 state를 사용하지 않는 더 많은 컴포넌트가 state 변경에 의해 불필요한 렌더링을 겪게 된다.
Index
ㄴGroup
ㄴUserList
ㄴUserItem
위와 같은 컴포넌트 구조가 있다고 볼 때 UserList와 UserItem에서만 사용되는 users state가 있다. 이 users state는 UserItem에서 보여줘야 할 데이터들을 가지고 있다. 이 데이터는 두 컴포넌트에서만 사용하기 때문에 그 중 가장 상위 컴포넌트인 UserList에 선언해야 한다.
그런데 만약 이 users state를 UserList보다 상위 컴포넌트인 Index에 선언하게 되면 users state가 변경되면 users 데이터를 사용하지 않는 Index 컴포넌트와 Group 컴포넌트까지 리렌더링이 발생하게 된다.
컴포넌트가 React.memo()
로 래핑될 때, React는 컴포넌트를 렌더링하고 결과를 메모이징(Memoizing)한다. 그리고 다음 렌더링이 일어날 때 props
가 같다면 React는 메모이징(Memoizing)된 내용을 재사용한다.
const Child = (...) => {
}
export default React.memo(CHild);
또는
const Child = React.memo((...) => {
....
})
export default Child;
React.memo는 컴포넌트가 같은 props로 자주 렌더링되거나, 무겁고 비용이 큰 연산이 있는 경우에 사용한다. 그 외에는 사용하지 않는것이 좋다.
React에서는 컴포넌트들을 매핑할 때, 고유의 key값을 부여할 것을 강제하고 있다. 이 때, key값에 index를 사용하는것을 지양해야 한다.
배열 중간에 어떤 요소가 삽입되면 그 중간보다 이후에 위치한 요소들은 전부 인덱스가 변경된다. 이로 인해 key값이 변경되고 리렌더링이 된다. 또한 데이터가 key와 매치가 안되어 서로 꼬이는 부작용도 발생한다.
가급적이면 데이터의 ID 등 고유값을 key에 넣기를 권장한다. 다음과 같은 경우에는 index를 써도 무방하다.
컴포넌트 내 어떤 함수가 값을 리턴하는데 많은 시간이 소요된다면, 이 컴포넌트가 리렌더링 될 때마다 함수호출에 많은 시간이 소요될 것이다. 또 그 함수의 리턴값을 자식 컴포넌트가 참조한다면, 해당값이 변경될 때마다 리렌더링이 발생될 것이다.
useMemo()
는 이런 경우 사용되는 Hook으로, CPU 소모가 심한 함수들을 캐싱하기 위해 사용된다.
const memoizedValue = useMemo(() => func, [depsList]);
첫 번째 인자는 캐싱하는 함수이며, 두 번째 인자는 의존 배열 객체로 여기에 포함된 값이 바뀌어야 해당 함수를 재호출한다.
useCallback()
역시 useMemo()와 같은 매커니즘으로 렌더링 최적화에 활용된다.
useMemo()가 특정 리턴값을 메모이징했다면, useCallback()은 props로 넘겨주는 함수 자체를 메모이징한다.
useCallback()
은 React.memo()
와 함께 사용하여 자식 컴포넌트의 불필요한 렌더링을 줄일 수 있다.
다음은 침실, 주방, 욕실의 불을 켜고 끄는 예제다.
import React from "react";
function Light({room, on, toggle}) {
console.log({room, on});
return (
<button onClick={toggle}>
{room} {on ? "💡" : "⬛"}
</button>
);
};
그리고 React.memo()
로 이 컴포넌트를 감싸준다.
Light = React.memo(Light);
다음으로 3개의 방의 스위치를 중앙 제어해주는 SmartHome
컴포넌트다.
import React, {useState, useCallback } from "react";
function SmartHome() {
const [masterOn, setMasterOn] = useState(false);
const [kitchenOn, setKitchenOn] = useState(false);
const [bathOn, setBathOn] = useState(false);
const toggleMaster = () => setMasterOn(!masterOn);
const toggleKitchen = () => setKitchenOn(!kitchenOn);
const toggleBath = () => setBathOn(!bathOn);
return (
<>
<Light room"침실" on={masterOn} toggle={toggleMaster} />
<Light room"주방" on={kitchenOn} toggle={toggleKitchen} />
<Light room"욕실" on={bathOn} toggle={toggleBath} />
</>
);
};
이 컴포넌트를 이용해서 침실의 조명을 켜보면 침실 뿐만 아니라 다른 모든 방에 대한 Light
컴포넌트가 호출되는것을 콘솔로그로 확인할 수 있다.
{room: "침실", on: true}
{room: "주방", on: false}
{room: "욕실", on: false}
조명을 키거나 끄는 방에 대한 Light
컴포넌트만 호출되게 하고 싶어서 React.memo()
를 사용한 것인데 해결되지 않았다. 이유는 조명을 제어할 때 쓰이는 toggleMaster()
, toggleKitchen()
, toggleBath()
함수의 참조값이 SmartHome
컴포넌트가 렌더링 될 때마다 모두 바뀌어버리기 때문이다.
이 문제를 해결하려면 모든 조명 제어 함수를 useCallback()
으로 감싸고 두 번째 인자로 각 함수가 의존하고 있는 상태를 배열로 넘겨야 한다.
import React, {useState, useCallback } from "react";
function SmartHome() {
const [masterOn, setMasterOn] = useState(false);
const [kitchenOn, setKitchenOn] = useState(false);
const [bathOn, setBathOn] = useState(false);
const toggleMaster = () => useCallback(() => setMasterOn(!masterOn), [masterOn]);
const toggleKitchen = () => useCallback(() => setKitchenOn(!kitchenOn), [kitchenOn]);
const toggleBath = () => useCallback(() => setBathOn(!bathOn), [bathOn]);;
return (
<>
<Light room"침실" on={masterOn} toggle={toggleMaster} />
<Light room"주방" on={kitchenOn} toggle={toggleKitchen} />
<Light room"욕실" on={bathOn} toggle={toggleBath} />
</>
);
};
이제는 침실에 조명을 켜 보면 침실에 대한 Light
컴포넌트만 호출되는 것을 확인할 수 있다.