front-end/react

React - Memoization

hojung 2022. 3. 26.
728x90
반응형

1. 리액트에서의 Memoization

memoization이란 반복되는 연산이 많은 경우 이미 한 번 계산된 결과를 저장해두고 같은 component를 렌더링할 경우 저장해둔 값을 사용하는 방식이다. 프론트엔드는 화면에 많은 정보를 서버로부터 받아온 후 띄워야하는 과정이 있고 Dom Tree에 일부 컴포넌트가 추가되거나 삭제될 때마다 모든 Dom Tree의 값을 리렌더링해야한다는 점은 굉장히 비효율적이고 이 문제를 해결하기 위해 많은 고민을 해왔다. 그 중 React에서 렌더링 시간을 조금이라도 줄일 수 있는 방법을 제공하는 것이 오늘 소개할 React.memo, useMemo, useCallback과 같은 훅이다. 

 


2. React.memo

우리는 흔히 화면에서 같은 디자인의 컴포넌트지만 안의 내용만 다른 컴포넌트를 여러 개 그리는 경우가 많다. 나도 vscode에서 그 같은 구조를 설계해볼 것이다. 

 

내가 화면에 ui를 띄우는 구조는 다음과 같다. 

1. 각 리스트에 해당하는 컴포넌트

import styled from "styled-components";
import { ContentType } from "./types";
import React, { memo, Profiler } from "react";

const ContentItemContainer = styled.div`
	border-bottom: 1px solid;
	padding: 10px;
	cursor: pointer;
	background-color: red;
`;

const ContentItem = ({ content, title, likes }: ContentType) => {
	function onRenderCallback(
		id: any, // 방금 커밋된 Profiler 트리의 "id"
		phase: any, // "mount" (트리가 방금 마운트가 된 경우) 혹은 "update"(트리가 리렌더링된 경우)
		actualDuration: any, // 커밋된 업데이트를 렌더링하는데 걸린 시간
		baseDuration: any, // 메모이제이션 없이 하위 트리 전체를 렌더링하는데 걸리는 예상시간
		startTime: any, // React가 언제 해당 업데이트를 렌더링하기 시작했는지
		commitTime: any, // React가 해당 업데이트를 언제 커밋했는지
		interactions: any, // 이 업데이트에 해당하는 상호작용들의 집합
	) {
		// 렌더링 타이밍을 집합하거나 로그...
		console.log(`actual Duration ${title} ${actualDuration} `);
	}

	return (
		<Profiler id="CommntItem" onRender={onRenderCallback}>
			<ContentItemContainer>
				<span>{title}</span>
				<br />
				<span>{content}</span>
				<br />
				<span>{likes}</span>
			</ContentItemContainer>
		</Profiler>
	);
};

export default ContentItem;

각 리스트에 들어가는 contentitem component이다. 나는 이 아이템을 배열에 넣어서 map함수를 통해 화면에 보여줄 것이다. 이 contentitem component가 모여 있는 리스트 컴포넌트에 해당하는 것이 바로 2번째 contentitem component이다. 

2. Contents

첫 번째의 contentitem을 여러개 보유하고 있는 Contents component이다. 이는 부모 component로부터 contentList라는 props를 받아 그 props에 해당하는 정보를 map함수를 통해 ContentItem에 props를 전달하면서 리턴한다. 

3. memoList

가장 상위 컴포넌트인 memoList컴포넌트이다. 여기서는 하위 컴포넌트로 전달할 contentList라는 배열을 가지고 있고 

useEffect()훅을 통해서 1초마다 새로운 contentItem을 만들어내고 있다. 

useEffect(() => {
		const interval = setInterval(() => {
			setContents((prevContent) => [
				...prevContent,
				{
					title: `number${prevContent.length + 1}`,
					content: ` im content number ${prevContent.length + 1}`,
					likes: 1,
				},
			]);
		}, 1000);

		return () => {
			clearInterval(interval);
		};
	}, []);

이 훅이 새로운 배열 요소를 추가해주는 useEffect 훅이다. 

1. React.memo 전 후 비교

비교를 하려면 우선 React에서 기본적으로 제공하는 성능 측정 훅인 Profiler를 import해준 후 onRender()함수를 콜백 함수로 주어야한다. 나는 각 contentItem이 렌더링이 언제 되는지를 알고 싶기 때문에 contentitem에 Profiler와 onRender함수를 추가해주었다. 

const ContentItemContainer = styled.div`
	border-bottom: 1px solid;
	padding: 10px;
	cursor: pointer;
	background-color: red;
`;

const ContentItem = ({ content, title, likes }: ContentType) => {
	function onRenderCallback(
		id: any, // 방금 커밋된 Profiler 트리의 "id"
		phase: any, // "mount" (트리가 방금 마운트가 된 경우) 혹은 "update"(트리가 리렌더링된 경우)
		actualDuration: any, // 커밋된 업데이트를 렌더링하는데 걸린 시간
		baseDuration: any, // 메모이제이션 없이 하위 트리 전체를 렌더링하는데 걸리는 예상시간
		startTime: any, // React가 언제 해당 업데이트를 렌더링하기 시작했는지
		commitTime: any, // React가 해당 업데이트를 언제 커밋했는지
		interactions: any, // 이 업데이트에 해당하는 상호작용들의 집합
	) {
		// 렌더링 타이밍을 집합하거나 로그...
		console.log(`actual Duration ${title} ${actualDuration} `);
	}

	return (
		<Profiler id="CommntItem" onRender={onRenderCallback}>
			<ContentItemContainer>
				<span>{title}</span>
				<br />
				<span>{content}</span>
				<br />
				<span>{likes}</span>
			</ContentItemContainer>
		</Profiler>
	);
};

Profiler와 onRender콜백함수를 추가해준 모습 

그 후 살펴보면 이렇게 1초에 한 번씩 컴포넌트가 계속 생겨나는데 그냥 배열을 spread 연산자를 통해 (얕은 복사)를 통해 추가해주면 다음과 같이 1234, 12345, 123456 ...과 같이 처음서부터 모든 component가 생겨나는 것을 알 수 있다. 

하지만 React.memo를 사용하여 contentItem을 감싸준다면 이전의 계산했던 값을 저장해준 후 props를 비교하여 props값이 달라졌을 때만 re-render를 한다. 하지만 우리는 그저 생성해서 추가해주는 것이기 때문에 새로운 component만 렌더링이 될 것이다. 

콘솔창을 보면 알 수 있듯이 중복되는 번호 없이 쭉 생성된다. 이것이 React가 제공하는 memo 기능이다. 


3. useCallback

앞서 React.memo는 component의 props를 비교한 후 props가 같으면 저장해둔 값을 사용하고 props가 다르면 새로 렌더링한다고 하였다. 이 때 부모 컴포넌트에서 자식 컴포넌트로 inline함수를 전달하면 어떨까? inline함수는 익명함수라 새로 component를 부를 때마다 새로 불려진다. 그러면 당연히 props가 다른 것으로 판단하게 되어 memo는 무용지물이 되게 된다. 

1. callback함수를 자식에게 내려주는 과정

1. props 타입 수정 

2. contentItem에 props로 전달 

3. 부모 컴포넌트에서 onClick inline 함수 정의 

2. 해결법 = useCallback()!

useCallback함수는 callback함수를 memo해줄 수 있는 hook이다. 아까의 React.memo가 컴포넌트 값을 기억해두었다가 재사용했다면 useCallback훅은 callback함수를 memo해줄 수 있는 훅이다. 

부모컴포넌트에서 useCallback hook을 사용하여 자식에게 props로 내려주는 모습 

이렇게 한다면 새로 불리우지 않는다. 


4. useMemo()

1. 확인법 

contentitem component에 상태를 판별해주는 함수 rate()를 추가해준다. 

이것은 likes가 10보다 크다면 popular로 작으면 woooo를 표현해주는 함수이다. 

그 후 rate함수가 동작할 때마다 console에 로그를 찍어주겠다. 

다음과 같이 매 컴포넌트가 호출될 때마다 rate함수가 불리는 것을 알 수 있다. 

1. 컴포넌트 안에서 event handler가 있을 때 

useState 훅을 이용해서 클릭이 될 때마다 클릭 수를 증가시켜주는 기능을 만들어보았다. 그 후 실행하면 다음과 같다. 

당연한 말이지만 상태가 변하였기 때문에 눌린 컴포넌트는 re-rendering 된다. 결과를 보면 눌린 3번째 컴포넌트가 다시 그려지는 것을 확인할 수 있다. 여기서의 문제는 상태만 변했을 뿐인데 컴포넌트의 모든 기능을 다시 수행하고 있다. (나 동작중! =  rate()함수이다. )

이 것을 막기위해 사용하는 것이 useMemo()훅이다. 

2. 사용

아까 컴포넌트가 re-rendering되면서 계속 같이 불리던 rate함수에 useMemo hook을 주고 dependency array에 likes를 주어 likes가 변할 때만 rate함수를 호출하기로 하였다. 

또한 useMemo의 리턴 값은 특정한 값이기에 함수로 주면 안되고 값으로 주어야한다. 

useMemo로 rate함수를 감싸주고 나니 눌러서 component가 re-rendering되어도 나 눌림!이라는 console message가 뜨지 않는다. 즉 rate함수가 불리지 않은 것을 확인할 수 있다. 

따라서 특정 값을 memoization할 때는 useMemo를 특정 함수를 memoization할 때는 useCallback을 사용하는 것을 알 수 있었다. 

 

728x90
반응형

'front-end > react' 카테고리의 다른 글

[React] react-router실험  (2) 2022.06.22
[React] 파일 업로드 기능 구현  (2) 2022.04.05
React - React Slick  (0) 2022.03.24
useParams를 통한 라우팅  (0) 2022.03.03
리액트 로그인 패턴 (with Redux, Axios)  (0) 2022.02.18

댓글