React 렌더링 최적화 (React.memo)

민순기·2022년 4월 6일
2

얼마 전에 회사 선배님이 한가지 퀴즈를 내주셨다. 퀴즈의 내용은 다음과 같다.

  1. recoil/atom/userList User[] 방식으로 사용자 정보를 다루고 있다.
  2. isAlone 이라는 상태 변수로 <혼자컴포넌트/><다수컴포넌트/>를 분기처리하여 렌더링하고 있다.
  3. 3인 이상일때, 특정 사용자가 진입하거나 나갔을때, <다수컴포넌트/>의 리렌더링을 막을수 있는 방법은?

이 문제를 보고 가장 먼저 들었던 방법은 useEffect의 의존성을 이용하는 것이었다. 그런데 useEffect만 사용해서 다수컴포넌트의 리렌더링을 막을수 있나? 하는 생각이 자꾸 들었다. 우선 컴포넌트의 리렌더링 조건부터 알아보도록 하자.

컴포넌트의 리렌더링 조건

컴포넌트가 리렌더링이 되는 조건은 다음과 같다.

  1. 부모 컴포넌트가 리렌더링이 되면 자식 컴포넌트도 리렌더링 된다.
  2. state가 변화하면 컴포넌트는 리렌더링 된다.
  3. 전달받은 props가 바뀌어도 리렌더링 된다.

컴포넌트가 리렌더링 되는 조건을 알았으니 리렌더링을 막기 위해서는 결국 저 조건을 모두 충족하지 않으면 된다.

여기까지 생각하고 퀴즈의 예시를 만들어 보았다.

const App = () => {
  	const [isAlone, setIsAlone] = useState(true);
  	const userList = useRecoilValue(userListAtom);
  	  
	return (
    	<div>{isAlone ? <혼자컴포넌트 {...userList}/> : <다수컴포넌트 {...userList}/>}</div>
    )
}

처음에는 이런 예제를 만들어서 퀴즈에 대해 고민했다. 하지만 이것은 예제가 잘못되었다.
userList의 길이가 바뀌면 isAlone이라는 상태값이 바뀌지 않더라도 <혼자컴포넌트> <다수컴포넌트> 모두 리렌더링된다. 때문에 리렌더링을 막는건 이대로는 불가능하다.

그럼 어떤 예제를 사용해야 할까?

const App = () => {
  	const [isAlone, setIsAlone] = useState(true);
  	const userList = useRecoilValue(userListAtom);
  	  
	return (
    	<div>
        	{ userList.map(el => <Item {...el}/>) }
        	{ isAlone ? <혼자컴포넌트 /> : <다수컴포넌트 /> }
      	</div>
    )
}

이 방식이 조금 더 알맞은 예제인것 같다. 자 그럼 이제 이 예제를 가지고 한번 리렌더링을 알차게 막아보자.

처음 생각했던 방식은 useEffect를 사용하는 것이었다.

const App = () => {
  	const [isAlone, setIsAlone] = useState(true);
  	const userList = useRecoilValue(userListAtom);
  	  	
  	useEffect(() => {
    	if (userList.length === 1) setIsAlone(true);
      	else setIsAlone(false);
    }, [userList.length]);
  
	return (
    	<div>
        	{ userList.map(el => <Item {...el} />) }
        	{ isAlone ? <혼자컴포넌트 /> : <다수컴포넌트 /> }
      	</div>
    )
}

그런데 이 방식을 말씀드리니까 정답은 맞는데 원하는 방식이 아니라고 하셨다...ㅠㅠ (어려워...)

그래서 구글링과 다른 분들의 의견을 들어보고 useMemoReact.memo()를 사용하는 방법을 고안해냈다.

useMemo와 React.memo()

이 둘은 결국 메모이제이션된 값을 반환한다. 다만 useMemo는 값을 반환하고 React.memo()는 컴포넌트를 반환하다는 것이 다르다.

useMemo의 사용법은 다음과 같다.

const isAlone = useMemo(() => {
	if (userList.length === 1) return true;
  	else return false;
}, [userList])

useMemo는 콜백함수와 의존성을 받아들인다.
useMemo에 대해서는 이전에도 포스팅한 기억이 있어서 더 자세한 내용은 적지 않겠다.


React.memo의 사용법은 다음과 같다.

const Comp = ({ isAlone }) => {
	return <div> { isAlone ? <span>혼자</span> : <span>다수</span> } </div>
}

const MemoizedComp = React.memo(Comp);

export default MemoizedComp;

React.memo는 매개변수로 컴포넌트와 콜백함수를 받는다. 다만 콜백함수는 선택사항이다. 콜백함수가 없으면 React.memo는 컴포넌트의 props의 값을 비교함으로써 리렌더링을 방지한다.

더 자세히 말하자면 props의 previous값과 next값이 같은지 다른지를 비교해서 다르면 리렌더링 시켜주고 같으면 리렌더링을 하지 않는다. 부모 컴포넌트가 리렌더링 되어도 props가 바뀌지 않으면 리렌더링 되지 않는 것이다.

React.memo의 두번째 매개변수인 콜백함수에서는 조건을 걸수가 있다. 코드를 통해 알아보자.

const Comp = (props) => {
	return <div> { props.isAlone ? <span>혼자</span> : <span>다수</span> } </div>
}

const MemoizedComp = React.memo(Comp, (prev, next) => {
	if (prev.isAlone === next.isAlone) return true;
 	else return false;
});

export default MemoizedComp;

코드를 보면 콜백함수의 매개변수는 이전값과 이후의 값이다.
즉 이전 props의 isAlone과 이후 props의 isAlone이 같으면( true라면 ) 캐시된 값을 사용하여 리렌더링을 막고 다르면 ( false라면 ) 캐시된 값을 사용하지 않고 리렌더링 하는 것이다.


다시 퀴즈로 가자

const App = () => {
  	const [isAlone, setIsAlone] = useState(true);
  	const userList = useRecoilValue(userListAtom);
  	  
	return (
    	<div>
        	{ userList.map(el => <Item {...el}/>) }
        	{ isAlone ? <혼자컴포넌트 /> : <다수컴포넌트 /> }
      	</div>
    )
}

이제 React.memouseMemo를 가지고 위의 예제를 재구성해보자.

const Comp = ( props ) => {
	return <div> { props.isAlone ? <span>혼자</span> : <span>다수</span> } </div>
}
// React.memo가 Comp 컴포넌트의 이전값과 이후값을 비교해서 리렌더링 여부를 결정한다.
const MemoizedComp = React.memo(Comp);

const App = () => {
  	const userList = useRecoilValue(userListAtom);
  	  
  	// useMemo가 userList의 변화를 메모이제이션해서 isAlone에 적절한 값을 할당한다.
  	const isAlone = useMemo(() => {
    	if (userList.length === 1) return true;
      	else return false;
    }, [userList]);
    
	return (
    	<div>
        	{ userList.map(el => <Item {...el}/>) }
        	<Comp isAlone={isAlone} />
      	</div>
    )
}

끝~!

profile
2년차 FE 개발자 민순기입니다.

0개의 댓글