[React] useCallback, useMemo, React.memo 사용하여 최적화하기


useCallback

React에서 이벤트 핸들링을 해야할 때 보통 컴포넌트 내부에 함수를 선언하게 된다.

React 컴포넌트는 컴포넌트 내부의 상태값이 변경될 때 리렌더링이 발생된다.

상태값이 변경되면서 리렌더링이 발생될 때 컴포넌트 내부에 선언한 함수 또한 새로 호출되게 된다.

이렇게 되면 불필요하게 메모리가 낭비되어 최적화에 좋지 못하다. 왜냐하면 특정 상태의 변경과 아무 상관도없는 함수도 계속해서 새로이 만들어지기 때문이다.

이러한 문제점을 방지할 수 있는것이 useCallback 이다.

useCallback을 사용하면 지정한 특정 상태값(의존성)이 변경되지 않는한 컴포넌트 내부에 선언한 함수가 리렌더링시 새로이 생성되는것을 방지할 수 있다.

const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

공식 문서를 보면 메모이제이션된 콜백을 반환하며 그러한 콜백은 의존성이 변경되었을 때에만 변경된다고 나와있다.

즉 메모이제이션된 콜백을 반환한다는 말은 이전에 생성한 함수를 캐싱해둿다가(기억해 뒀다가) 재사용할 수 있는 콜백함수를 반환한다는 말이고

두번째 인자인 deps 배열 내부에 작성한 상태값(의존성)이 변경되지 않으면 리렌더링시 함수가 새로이 생성되는것을 방지한다는것이다.

useCallback을 사용하면서 주의할 점은 useCallback 내부에서 사용되는 상태값(의존성)은 모두 useCallback의 두번째 인자에 의존성으로 모두 작성해 주어야한다. 그러지 않으면 초기의 상태값을 기억해 기존 상태값을 계속해서 반환하게 된다.

마지막으로 useCallback을 꼭 사용해야하는 때는 부모 컴포넌트에서 자식 컴포넌트에 props로 함수를 전달할 때 이다. 

그 이유는 함수가 객체로 취급될 수 있기 때문에 자식 컴포넌트에서는 이전에 props로 전달된 함수와 다른 함수라고 인식하여 리렌더링이 발생할 수 있기 때문이다.



useMemo

useMemo는 useCallback과 비슷하지만 조금 다르다. 

useCallback은 함수를 캐싱하지만, useMemo는 함수의 결과 값을 캐싱한다.


const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo는 첫번째 인자로 함수를 받고 두번째 인자로는 deps 배열을 받는다.

일단 함수를 호출하고 return 된 값이 memorizedValue가 된다.

이후 두번째 인자의 deps 배열에 특정 상태값을 작성해주게되면 해당 deps 배열안에 작성한 상태값(의존성)이 변경될때만 첫번째 인자로 작성한  callback 함수(값을 구하는 함수)가 실행된다.

useMemo를 사용하면 좋은때는 매우 많은 연산을 수행하여 값을 리턴하는 함수에 사용하면 좋다.

왜냐하면 매우 많은 연산을 수행하는 함수인데 상관없는 상태값이 변경되었을때에도 리랜더링되어

매우 많은 연산을 수행하는 함수를 계속해서 새로이 호출하면 그만큼 메모리가 낭비되어 최적화에 좋지 못하기 때문이다.



React.memo

사용 예시

import React, { memo } from "react";

const Welcome = ({ name }) => {
return <div>hello {name}</div>;
};

export default React.memo(Welcome);

React.memo 는 간단히 말하면 부모컴포넌트에서 전달되는 props의 값이 변경될때만 새로이 렌더링 되게 해준다.

즉. props 값을 memorized 하고 있다가 props의 값이 변경되었을 때에만 변경된 props 값을 가지고 컴포넌트를 새롭게 랜더링 한다.

이렇게 memorized된 내용을 재사용하면 렌더링시 가상 DOM에서 변경된 부분을 확인하지 않아도 되기 때문에 성능상의 이점이 생기게 된다.

또한 React.memo 메서드는 React.memo(component, compareFunc)와 같은 형태가 기본 형태인데, compareFunc 부분에는 수동으로 props 비교방식을 직접 정의할 수 있다.


* React.memo를 사용하면 좋은 경우

React 최적화 방식들을 공부하면서 접했던 내용은 무조건적인 사용을 지양하라는 것이었다. 그 이유는 최적화를 위한 연산이 불필요한 경우엔 비용만 발생시키기 때문이다. 그러므로 React.memo는 다음과 같은 상황에서 사용을 권장한다.


1. Pure Functional Component 에서

2. Rendering이 빈번하게 발생하는 경우

3. re-rendering이 되는 동안에도 계속 같은 props값이 전달 될 경우

4. UI element의 양이 많은 컴포넌트의 경우


일반적으로 불필요한 Render가 많이 발생하는 곳에서 사용하라는 말이라고 생각한다. 

반면에 React.memo를 사용하면 안되는 경우도 있다.


* React.memo를 사용하면 안되는 경우

props로 함수, 객체를 전달하는 경우 React.memo를 사용하면 안된다.

그 이유는 React.memo가 props를 비교할 때 얕은 비교를 진행하는데, 얕은 비교란 원시 값의 경우는 같은 값을 갖는지 확인하며, 객체나 배열과 같은 참조 값은 동일한 주소 값을 갖고 있는지 확인한다.

그런데 함수와 객체는 내용과 형태 즉 겉보기에 동일하다고 하더라도 참조값이 다르기 때문에 무조건 리랜더링이 발생하게 된다.

그러므로 React.memo를 사용해도 최적화가 이루어지지 못한다.


개발자 도구로도 함수와 객체의 참조와 관련된 특성을 간단히 확인해 볼 수 있다.




*요약 

useCallback : 함수를 캐싱(기억)

useMemo : 복잡한(연산이 많은?) 함수의 결과 값을 캐싱(기억)

React.memo : props를 기억, props로 함수, 객체 전달시 사용하면 안됨



useMemo는 아직 제대로 사용해본적이 없다. React.memo는 위에 예시처럼 사용하면되고 useCallback 사용 예시를 아래 작성해놨다.


사용예시 useCallback

signup.js

import React, { useCallback, useState } from "react";
import Head from "next/head";
import { Form, Input, Button, Checkbox } from "antd";
import styled from "styled-components";
import AppLayout from "../components/AppLayout";
import useInput from "../components/hooks/useInput";

const ErrorMessage = styled.div`
color: red;
`;

const Signup = () => {
const [id, setId] = useInput("");
const [nickname, setNickname] = useInput("");
const [password, setPassword] = useInput("");

const [passwordCheck, setPasswordCheck] = useState("");
const [passwordError, setPasswordError] = useState(false);
const [term, setTerm] = useState("");
const [termError, setTermError] = useState(false);

const handlePasswordCheck = useCallback(
(event) => {
setPasswordError(event.target.value !== password);
setPasswordCheck(event.target.value);
},
[password]
);

const handleTerm = useCallback((event) => {
setTerm(event.target.checked);
setTermError(false);
}, []);

const handleSubmit = useCallback(() => {
if (password !== passwordCheck) {
return setPasswordError(true);
}
if (!term) {
return setTermError(true);
}
console.log(id, nickname, password);
}, [password, passwordCheck, term]);

return (
<AppLayout>
<Head>
<title>회원가입 | nextweet</title>
</Head>
<Form onFinish={handleSubmit}>
<div>
<label htmlFor="user-email">이메일</label>
<br />
<Input
name="user-email"
type="email"
value={id}
required
onChange={setId}
/>
</div>
<div>
<label htmlFor="user-nickname">닉네임</label>
<br />
<Input
name="user-nickname"
value={nickname}
required
onChange={setNickname}
/>
</div>
<div>
<label htmlFor="user-password">비밀번호</label>
<br />
<Input
name="user-password"
type="password"
value={password}
required
onChange={setPassword}
/>
</div>
<div>
<label htmlFor="user-password-check">비밀번호 확인</label>
<br />
<Input
name="user-password-check"
type="password"
value={passwordCheck}
required
onChange={handlePasswordCheck}
/>
{passwordError && (
<ErrorMessage>비밀번호가 일치하지 않습니다.</ErrorMessage>
)}
</div>
<div>
<Checkbox name="user-term" checked={term} onChange={handleTerm}>
사진 활용에 동의합니다.
</Checkbox>
{termError && <ErrorMessage>약관에 동의하셔야 합니다.</ErrorMessage>}
</div>
<div style={{ marginTop: 10 }}>
<Button type="primary" htmlType="submit">
가입하기
</Button>
</div>
</Form>
</AppLayout>
);
};

export default Signup;


custom hooks

useInput.js

import { useState, useCallback } from "react";

const useInput = (initialValue = null) => {
const [value, setValue] = useState(initialValue);
const handler = useCallback((event) => {
setValue(event.target.value);
}, []);
return [value, handler];
};

export default useInput;



댓글