React 최적화를 해본 경험이 있나요?

sxxng_ju·2023년 3월 7일
0

Web

목록 보기
4/4

React에서 제공하는 기본 Hooks를 다루는 글에서 useMemo와 useCallback을 이미 언급한 적이 있습니다. React에서 렌더링을 최적화하기 위해 사용되는 Hook들입니다. 하지만 이러한 Hook들을 사용하는 본인 만의 기준이나 해야하는 이유를 조금 더 자세히 알아보려고 합니다.

React Virtual Dom (가상돔)

최적화에 앞서 먼저 React가 작동하는 방식을 알 필요가 있습니다. React는 상태 값이 업데이트되면 컴포넌트를 다시 실행합니다. 하지만 이때 React의 실제 DOM은 처음부터 컴포넌트를 다시 그리는 것이 아니라 변경된 부분 만을 그리게 됩니다.

React는 가상돔 객체를 두 개 가지고 있습니다.
렌더링 이전의 가상돔과 렌더링 이후의 가상돔입니다. 이 둘을 비교해 어떤 요소가 변했는지 평가하고 실제 DOM에 적용합니다.

React.memo

React.memo는 함수형 컴포넌트에만 사용이 가능합니다. Reat.memo의 안에 들어가는 인자 컴포넌트의 props가 변경되었을 경우만 컴포넌트를 재실행합니다. 즉 부모 컴포넌트가 변경되어 재실행될 경우 자식 컴포넌트도 다시 재실행되어야 하지만 React.memo를 사용하여 자식컴포넌트를 감쌌을 경우 재실행되지 않도록 막습니다.

그렇다면 어떠한 경우에 React.memo를 써야할까요? React.memo를 통해 최적화된 컴포넌트는 props 비교를 위해 별도의 메모리 공간을 차지하게 됩니다. 그렇기 때문에 오히려 재실행되는 것보다 더 비용이 큰 작업일 수 도 있습니다. 따라서 자식컴포넌트가 많아 컴포넌트 트리가 매우 크거나 상위 컴포넌트라면 하위 컴포넌트들의 렌더링을 피할 수 있기 때문에 유용합니다.

useCallback

위에서 알아본 React.memo를 통한다면 props로 같은 값을 받아오는 컴포넌트에 사용하면 좋을 것입니다.

const handleClick = () {...}

return 
	<Button name="확인" onClick={handleClick} />

예를들어 위의 버튼 컴포넌트를 사용할 때 매번 같은 name과 클릭이벤트를 전달하기 때문에 props의 변경이 없을 경우 최적화하고 싶다고 합시다. 따라서 React.memo(Button)으로 최적화를 하지만 부모컴포넌트가 재실행되면 props의 변경이 없었음에도 불구하고 Button 컴포넌트도 같이 재실행됩니다.

그 이유는 자바스크립트의 객체에 있습니다. 자바스크립트는 원시타입과 객체타입을 저장하는 방식이 다릅니다.

"test" === "test" // true
["test"] === ["test"] // false

위의 코드처럼 배열, 객체, 함수 등 자바스크립트에서는 모두 객체타입으로 참조 값을 저장하기 때문에 같은 결과라도 다른 메모리에 저장합니다.

그 말은 부모컴포넌트에서 재실행이 될 때 handleClick 함수가 다시 생성이되고 새로운 메모리에 객체가 할당될 것입니다. 그렇기 때문에 Button 컴포넌트 입장에서는 onClick prop이 변경한 것으로 받아들여 재실행하는 것입니다. handleClick 함수는 같은 값을 전달하므로 이를 해결하기 위해서는 handleClick 함수를 기억하는 것이 필요합니다. 그래야 새로운 메모리 주소에 담긴 함수를 넘기지않고 기존의 함수를 재사용할 것입니다. 이때 필요한 것이 useCallback 입니다.

const handleClick = useCallback(() => {
	...
}, [])

두번째 인자는 의존성 배열로 빈 배열을 넣게되면 handleClick 함수는 절대 다시 생성될 일 없다고 말하는 것과 같습니다. 따라서 경우에 따라서만 handleClick을 다시 생성하고 싶다면 의존성 배열에 추가하면 됩니다. 하지만 여기서 의문점이 생깁니다. 굳이 의존성 배열이 필요한가 입니다.

useCallback을 사용하여 함수를 저장하게 되면 만약 별도로 다른 상태값을 useCallback 함수 안에서 사용하게되면 그 상태값을 상수로 메모리에 저장하게됩니다. 그 말은 그 상태값이 변경되어 컴포넌트가 재실행되어도 함수는 기존의 함수를 그대로 사용하기 때문에 업데이트된 상태값을 사용할 수 없게 됩니다. 그럴때 이 상태를 의존성 배열에 추가하면 됩니다.

useMemo

위에서 자식 컴포넌트가 props의 변경이 있을 경우에만 재실행하도록 React.memo를 사용해 최적화 했습니다.

const 자식컴포넌트 = props => {
  	const dataList = props.items.sort(...).map(...) // 엄청나게 복잡한 작업
	return (
      	<>
    		<div>{props.name}</div>
	        {dataList.map(...)}
      	</>
    )
}

하지만 props.name만 변경되었을 경우마다 자식컴포넌트가 다시 실행되기 때문에 props.item을 가공하는 작업을 매번 실행하게 됩니다. 이러한 작업을 매번 실행하는 것이 아닌 필요한 props가 변경될 경우만 다시 실행하고 싶다면 값을 저장하는 useMemo를 사용하는 것입니다.

const 자식컴포넌트 = props => {
	const {items} = props;
  	const dataList = useMemo(() => {
	  return items.sort(...).map(...)
    }, [items]) // 엄청나게 복잡한 작업
	return (
      	<>
    		<div>{props.name}</div>
	        {dataList.map(...)}
      	</>
    )
}

여기서 props로 전달 받은 items 배열도 매번 같은 배열이어도 객체기 때문에 다른 items로 판단되기 때문에 props로 전달되는 items도 useMemo를 통해 최적화 해주어야 원하는대로 동작합니다.

결론

React.memo - 자식컴포넌트가 많아 컴포넌트 트리가 매우 큰 상위 컴포넌트에서 사용
useCallback - props로 전달하는 값이 객체타입일 경우
useMemo - props로 전달하는 값이 객체타입일 경우, 렌더링이 자주 일어나는데 복잡한 연산을 하여 값을 저장하는 경우(정렬)

0개의 댓글