React 컴포넌트가 렌더링을 수행하는 시점은 다음과 같다.
forceUpdate()
를 실행하였을 때위 과정에서 컴포넌트 렌더링 결과에 영향을 미치지 않는 즉, 보여지는 부분에 영향을 미치지 않는 변경사항 때문에 리렌더링이 발생한다면, 불필요한 렌더링으로 인해 성능 손실이 발생할 수 있다.
이를 방지하기 위해 개발자는 컴포넌트가 최소한으로 렌더링 되기위한 최적화작업을 하는 것이 중요하다. 이를 위해 여러 hook을 사용할 수 있다. useCallback
, useMemo
그리고 useEffect
는 클래스 컴포넌트의 lifecycle메소드처럼 리렌더링할 때, 성능의 최적화를 도와준다.
useEffect
는 모든 컴포넌트가 렌더링된 후 상태변화, 구독, 타이머, 로깅 및 기타 side effect를 처리하는데 도움이 되는 Hook이다. useEffct
는 함수와 종속성 배열을 매개변수로 전달받으며 종속성 배열의 요소가 바뀔 때 전달된 함수를 실행한다.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// componentDidMount, componentDidUpdate와 같은 방식으로
useEffect(() => {
// 브라우저 API를 이용하여 문서 타이틀을 업데이트합니다.
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useCallback
은 특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용한다. 이 Hook은 함수와 종속성 배열(dependency array)을 매개변수로 전달받으면 메모이제이션된 콜백을 반환하므로 부모 컴포넌트가 자식 컴포넌트의 렌더링을 방지하기 위해 자식 컴포넌트에 콜백을 전달할 때 매우 유용하다.
쉽게 말해 컴포넌트가 리렌더될 때 마다 컴포넌트 내의 정의된 함수들은 이전에 생성된 함수와는 다른 주소값을 가진 함수를 매번 생성하는데 useCallback
을 이용해 종속성 배열이 변경되지 않았다면 동일한 주소값을 가진 함수를 재사용할 수 있다.
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useMemo
는 useCallback
과 유사하게 함수와 종속성 배열을 전달받는다. 하지만 메모이제이션된 콜백이 아닌 전달받은 함수의 메모이제션된 값을 반환한다. 이것은 종속성 배열의 요소가 바뀔 때에만 다시 계산을 한다. 그래서 비싼 계산을 해야하는 잘 바뀌지 않는 값의 리렌더링을 방지할 때 유용한다.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
React.memo
는 HOC(High-Order-Components)로서 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환하는 구조의 함수로써 useMemo
, useCallback
과 같이 성능 최적화를 위해 불필요한 렌더링 또는 연산을 제어하는 용도로 사용된다.
하지만 앞서 말했듯 React.memo
는 HOC이고, useMemo
와 useCallback
은 Hook이라는 차이점이 있다. 따라서 React.memo
는 클래스형 컴포넌트, 함수형 컴포넌트 모두 사용 가능하지만, useMemo
는 hook이기 때문에 함수형 컴포넌트 안에서만 사용 가능하다.
또한 React.memo는 props이 같을 시 컴포넌트 자체를 기억하여 새로 그려내지 않지만 useMemo, useCallback과 같은 Hook들은 컴포넌트 자체를 기억하는 것이 아닌 렌더링시에 계산된 함수또는 값을 기억하며 컴포넌트의 내부 로직에서 실행된다.
const MyComponent = React.memo(function MyComponent(props) {
/* props를 사용하여 렌더링 */
});
useMemo
,useCallback
,React.memo
를 이용하여 불필요한 렌더링을 줄일 수 있습니다.
예를 들어React.memo()
를 대표적으로 예시를 들어보겠습니다. 화면에 1,2,3이 출력되어있고 click버튼을 누르면 숫자가 +1 되어지는 것을 counter를 개발한다고 가정을 해봅시다. 이 때 click 버튼을 누르면 이벤트로 인해 새로운 요소가 추가되면서 재렌더링이 일어나게 됩니다. 하지만 기존에 출력되어있는 숫자들은 다시 렌더링을 할 필요가 없어지게 될 것입니다. 이 때React.memo()
를 사용하여 재렌더링을 방지합니다. memo는 재렌더링을 방지하고자하는 컴포넌트를 감싸주기만 하면 되고, 전달받은 props에 변화가 없다면 상위컴포넌트에 재렌더링이 일어나도 해당 컴포넌트를 재렌더링하지 않습니다. 그러므로 memo에 의해 재사용이 가능하게 되어 불필요한 렌더링을 줄일 수 있습니다.
자세한 예시는 링크를 통해 확인할 수 있습니다.
다시 복습!
useMemo
, useCallback
, React.memo
은 각각 무엇일까?
React.memo는 Higher-Order Components(HOC)이다.
👀 Higher-Order Components(HOC)란 컴포넌트를 인자로 받아 새로운 컴포넌트롤 다시 return해주는 함수이다.
const NewComponent = higherOrderComponent(WrappedComponent);
일반 컴포넌트는 인자로 받은 props를 UI에 활용하는 반면에, higher-order component는 인자로 받은 컴포넌트를 새로운 별도의 컴포넌트로 만든다. HOC는 리액트의 API가 아니라 리액트가 컴포넌트를 구성하는데 있어서의 일종의 패턴이라고 보면된다.
React.memo의 사용법은 다음과 같다.
const MyComponent = React.memo((props) => {
return (/*컴포넌트 렌더링 코드*/)}
);
만약 컴포넌트가 같은 props를 받을 때 같은 결과를 렌더링한다면 React.memo를 사용하여 불필요한 컴포넌트 렌더링을 방지할 수 있다.
즉, 컴포넌트에 같은 props가 들어온다면 리액트는 컴포넌트 렌더링 과정을 스킵하고 마지막에 렌더링된 결과를 재사용한다.
React.memo는 오직 props가 변경됐는지 아닌지만 체크한다. 만약 React.memo에 감싸진 함수형 컴포넌트가 함수 내부에서 useState나 useContext같은 훅을 사용하고 있다면, state나 context가 변경될 때마다 리렌더링된다.
기본적으로 props로 들어온 object는 shallow compare로 비교한다. 즉, props로 들어온 number, string과 같은 scarlar 값은 실제 값이 동일한가를 비교하지만, object의 경우 scarlar 값과 달리 같은 값을 'reference(참조)'하고 있는지를 비교 한다.
만약 비교방식을 커스텀하고 싶다면 아래 코드처럼 비교함수를 React.memo의 두번째 인자로 넣어주면 된다.
function MyComponent(props) {
/* 컴포넌트 렌더링 코드 */
}
function areEqual(prevProps, nextProps) {
/*
만약 전달되는 nextProps가 prevProps와 같다면 true를 반환, 같지 않다면 false를 반환
*/
}
export default React.memo(MyComponent, areEqual);
useMemo는 메모이즈된 값을 return하는 hook이다.
인자로 함수와 의존 값(dependencies)을 받는다. useMemo는 두번째 인자로 준 의존 인자 중에 하나라도 변경되면 값을 재계산한다. 이를 통해 매 렌더시마다 소요되는 불필요한 계산을 피할 수 있다. 만약 dependencies 인자로 아무것도 전달되지 않는다면, 렌더시마다 항상 값을 새롭게 계산하여 return한다.
아래의 코드는 a, b값이 변할 때만 첫번째 인자로 들어온 함수가 실행되어 재계산이 되고, 그렇지 않은 경우에는 메모이즈된 값을 return한다.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
React.memo와 useMemo 모두 props가 변하지 않으면(이전 props와 동일하면) 인자로 넘긴 함수는 재실행되지 않고, 이전의 메모이즈된 결과를 반환한다는 점에서 공통점이있다.
아래 React.memo와 useMemo를 사용한 코드를 보면 두가지 코드는 props.name의 값이 변하지 않는다면 리렌더링 되지 않고 이전의 값을 반환한다는 점에서 동일하게 동작한다.
/*별도로 두번째 인자를 넘기지 않을 경우 props가 변하지 않는다면 재렌더링 되지 않음*/
const NameTag = React.memo(
(props) => <div>{props.name}</div>
);
/*만약 두번째 인자로 특정 props.name값이 같지 않을때만 재렌더링 하도록 커스텀 비교 함수를 넣어주고 싶을 때*/
const NameTag = React.memo(
(props) => <div>{props.name}</div>
,
(prevProps, nextProps) => prevProps.name === nextProps.name
)
function NameTag(props) {
return useMemo(
() => <div>{props.name}</div>
,
[props.name]
)
}
useCallback은 메모리제이션된 함수를 return하는 hook이다.
const handleChange = useCallback(
(e) => {setNum(e.target.value)
}, []
) ;
useCallback을 통해 memoized된 함수는 eventhandler로 사용되며
두번째 인자인 [] <- 배열인 요소가 변경될때마다 새로운 함수가 생성된다.
> 최적화를 위한 연산이 불필요한 경우에는 비용만 발생시키는 단점이 있다.