[React JS] memo, useMemo 그리고 useCallback

이번에는 memo와 useMemo 그리고 useCallback에 대해서 공부하던 도중 성능 최적화를 위해서 정리가 필요하다고 판단해서 정리해본다.

 

참고로 log js 파일을 생성하여 컴포넌트의 레벨에 따라 렌더링이 되는지 확인을 해보았다.

export function log(message, level = 0, type = 'component') {
  let styling =
    'padding: 0.15rem; background: #04406b; color: #fcfabd';

  if (type === 'other') {
    styling = 'padding: 0.15rem; background: #210957; color: #ede6b2';
  }

  const indent = '- '.repeat(level);

  console.log('%c' + indent + message, styling);
}

1. Memo

우리가 이전에 useState를 통해서 값이 변경되거나 상태가 변경이되면 useState가 실행된 컴포넌트 하위 자식 컴포넌트 모두 재생성을 해서 다시 렌더링 된다는것을 알고있다.

 

하지만 우리가 다시 렌더링할 필요가 없는 컴포넌트가 존재한다면 어떻게 해야할까??

예시로 아래의 코드를 보자. [ 코드는 대충 썼으니 따라하지마세요 :) ]

import { useCallback, useState } from 'react'

import { log } from './log.js'
import Test from './components/Test.jsx'

function App() {
	log('<App /> rendered') // 컴포넌트 렌더링시 LOG

	const [testNumber, setTestNumber] = useState(1)

	function testClick() {
		console.log('testClick')
	}

	function changeTestNumber() {
		setTest((prevTestNumber) => prevTestNumber + 1)
	}

	return (
		<div>
        	<main>
			<button onClick={changeTestNumber}>ChangeTestNumber</button>
			<Test onClick={testClick} />
			</main>
		</div>
	)
}

export default App

위는 App Component이고 아래는 Test Component이다

import { memo } from 'react'
import { log } from '../log'

export default function Test({ onClick }) {
	log('testClick', 1) // 컴포넌트 렌더링시 LOG
	return <button onClick={onClick}>test</button>
}

화면은 아래와 같이 옹졸하다 :)

 

아무튼 우리가 처음 페이지에 접근했을때 다음과 같이 렌더링 된다.

여기서 test버튼은 단순히 console.log('testClick') 만을 보여주기 위한 컴포넌트다.

그런데 우리가 ChangeTestNumber 버튼을 클릭하게 되면 TestNumber의 값이 변하게 되고 그 하위 컴포넌트인 Test 까지 렌더링이 되버린다.

 

그렇다면 우리는 어떻게 해야할까?

여기서 memo가 필요해진다.

아래와 같이 memo를 활용하여 컴포넌트를 감싼후, onClick이라는 prop이 변하지 않는 이상 Test 컴포넌트는 App 컴포넌트가 다시 렌더링되었다해도 다시 렌더링 되지 않는다.

import { memo } from 'react'
import { log } from '../log'

const Test = memo(function Test({ onClick }) {
	log('testClick', 1)
	return <button onClick={onClick}>test</button>
})

export default Test

 

ChangeTestNumber 버튼을 클릭한후 콘솔창을 보면 

memo 적용 후 changeTestNumber 버튼 클릭후 콘솔

어?? 이상하다.

분명히, prop의 값이 변하지 않으면 Test 컴포넌트는 렌더링 되지 않는다고 했다.

그렇다면 onClick의 prop이 변경되었다고 볼수밖에없다.

 

다시 App 컴포넌트를 보자

import { useCallback, useState } from 'react'

import { log } from './log.js'
import Test from './components/Test.jsx'

function App() {
	log('<App /> rendered') // 컴포넌트 렌더링시 LOG

	const [testNumber, setTestNumber] = useState(1)

	function testClick() { // 이부분!!!!!!!!!!!!!!!
		console.log('testClick')
	}

	function changeTestNumber() {
		setTest((prevTestNumber) => prevTestNumber + 1)
	}

	return (
		<div>
        	<main>
			<button onClick={changeTestNumber}>ChangeTestNumber</button>
			<Test onClick={testClick} />
			</main>
		</div>
	)
}

export default App

우리가 TestClick이라는 함수를 Test 컴포넌트의 prop으로 전해주고있다.

testClick이라는 함수가 값이 변한것인가?? 엄밀히 말하면 맞는 말이다.

App 컴포넌트가 재 랜더링 되면서 함수가 다시 생성되었다고 볼 수 있다.

왜냐하면 함수는 컴포넌트가 재생성이 되면 메모리에 새롭게 할당이 된다. 그렇게 되면 기존에 Test 컴포넌트에 주어진 onClick prop의 값이 변한다고 볼수 있다.

 

여기서 우리는 useCallback을 알아야 한다.


2. UseCallBack

useCallback은 무엇이냐?

우리가 특정함수를 새롭게 생성해서 사용하고 싶지 않을때 사용한다.

쉽게 말해서 이미 메모리에 할당된 함수를 새롭게 할당하지 않는다고 생각하면 된다.

 

물론, 함수를 재생성한다고 해서 메모리에 큰 차이가 있지는 않을것이다. 하지만, 다시 렌더링될 필요가 없는 컴포넌트를 방치하는 것은 비효율적이다.

 

그래서 testClick 함수를 useCallback으로 감싸주면 된다.

아래의 코드를 보자 

// useCallback 적용 전
function testClick(){
	console.log('testClick')
}


// useCallback 적용 후
const testClick = useCallback(() => console.log('testClick'), [])

위와 같이 useCallback으로 감싸주었다. 

그런데 맨뒤에 대괄호는 무엇인가???? 

뒤의 대괄호는 함수 내에서 의존하고 있는 것들을 선언하는 곳이다.

예를 들어서 testClick이  A라는 파라미터를 받아서 출력해주는 함수라면 대괄호에 A를 추가해주어야한다.

const testClick = useCallback((A) => console.log(A), [A])

위의 의미는 A라는 변수가 변할 시에, 함수가 재생성된다고 볼수 있다.

만약 A를 명시해 주지 않는다면, 이 함수는 쭈우우욱 그대로 같은 메모리에 할당된 함수라고 볼 수 있다.

 

그리하여 다시 ChangeTestNumber 버튼을 클릭하면 아래와 같이 Test 컴포넌트는 다시 렌더링 되지 않는것을 볼 수 있다.

memo와 useCallback 적용 후 콘솔


3. UseMemo

useMemo는 무엇일까??

개인적으로 이해하고 있는바는 복잡한 계산식이나 특정 함수를 매번 실행시키지 않고 싶을때 사용하는 것이다.

예를 들어서, 우리가 Test 컴포넌트가 App 컴포넌트가 렌더링 될때마다 다시 렌더링 된다고 가정해보자.

import { log } from '../log'

function onCal(A, B) { // 계산식
	console.log(A, B)
	return A + B
}

export default function Test({ onClick, ChangeNumber, NotChangeNumber }) {
	log('testClick', 1)

	const onCalcl = onCal(1, 3) // 커스텀 함수
    
	return (
		<div>
			<button onClick={onClick}>test</button>
		</div>
	)
}

위와 같이 onCal처럼 간단한 계산 함수가 있다고 가정해보자. 

그렇다면 Test 컴포넌트가 매번 렌더링 될때마다 onCal 함수는 실행될것이다. 위처럼 간단한 계산식은 영향이 작지만, 만약 엄청나게 복잡한 계산 로직이 담긴 함수라면, 매번 렌더링 될때마다 과부하가 걸릴 수 밖에 없다.

 

그 과부하를 막을때 사용하는것이 useMemo 이다. 적용된 아래의 코드를 보자

// useMemo 적용 전
const onCalcl = onCal(1,3)

// useMemo 적용 후
const onCalcl = useMemo(() => onCal(1, 3), [])

useCallback과 동일하게 useMemo역시 의존하고있는 것을 대괄호에 선언해주어야 한다.

현재 Test 컴포넌트에 ChangeNumber와 NotChangeNumber를 받고있다. 한번 테스트를 해보자.

 

먼저 아래는 ChangeNumber를 의존하고 있다고 가정하여 작성한 결과다.

import { useMemo } from 'react'
import { log } from '../log'

function onCal(A, B) {
	console.log(A, B)
	return A + B
}

export default function Test({ onClick, ChangeNumber, NotChangeNumber }) {
	log('testClick', 1)

	const onCalcl = useMemo(() => onCal(1, 3), [ChangeNumber])  // 이부분!!!!!
	return (
		<div>
			<button onClick={onClick}>test</button>
		</div>
	)
}

ChangeNumber가 적용된 후 실행

 

그리고 NotchangeNumber가 선언되어있다면

import { useMemo } from 'react'
import { log } from '../log'

function onCal(A, B) {
	console.log(A, B)
	return A + B
}

export default function Test({ onClick, ChangeNumber, NotChangeNumber }) {
	log('testClick', 1)

	const onCalcl = useMemo(() => onCal(1, 3), [NotChangeNumber]) // 이부분!!!!!
	return (
		<div>
			<button onClick={onClick}>test</button>
		</div>
	)
}

NotChangeNumber가 적용된 후 실행

 

위처럼 onCal 함수가 실행되지 않은것을 볼 수 있다.


4. 결론

우리는 위에서 useCallback, useMemo, 그리고 memo 에 대해서 알아보았다.

 

처음에 공부하면서 성능 최적화가 그리 중요한가 싶었다.

하지만 정리하면서 매우매우 중요한 개념이라는 것을 알게되었고, 이것을 잘 다뤄야 실력있는 프론트엔드 개발자라고 생각을 한다.

 

이번에는 참조글 없이 스스로 내 생각을 정리해서 쓴글이기에 퀄리티가 떨어질 수 있다. 하지만 혼자 정리한다는것에 의미를 크게두고 보람을 느낀다 :)

 

오늘도 꾸준히 공부해야지!!!!! ☺☺☺

 

 

 

'FE > ReactJS' 카테고리의 다른 글

[React JS] useState 동작방식  (0) 2024.03.31
[React JS] Virtual DOM이란 무엇인가??  (0) 2024.03.31