useCallback과 useMemo

Winney·2021년 3월 28일
0

리액트

목록 보기
1/2
post-thumbnail

이전부터 useCallbackuseMemo를 알고 있었지만 최근에 실제로 써보면서 다시 관련 내용을 찾아보는 나를 발견하고 블로그에 간단히 정리해보기로 했다.
참조 : https://medium.com/@jan.hesters/usecallback-vs-usememo-c23ad1dc60

미리 알아야 하는 단어

  • 일급 함수(first-class function) : 함수를 다른 변수와 같이 다룰 수 있는 함수를 말한다. 예를 들면 함수를 다른 함수의 인자로 넘길 수 있고 다른 함수로 반환할 수 있다. 당연히 변수에 함수를 할당하는 것도 가능하다.
  • 고차 함수(higher-order function, HOF) : 함수를 인자로 전달 받거나 함수를 결과로 반환하는 함수.
  • 고차 컴포넌트(higher-order component, HOC) : 리액트 컴포넌트를 재사용하기 위한 기술. 컴포넌트를 가져와서 새 컴포넌트를 반환하는 기술이다.
  • 참조 평등(Referential equality) : 참조 평등은 두 객체에 대한 포인터가 동일하다는 것을 의미한다. 즉, 두 객체는 같은 메모리 위치에 포함되어 포인터가 동일한 객체를 참조하는 것을 알 수 있다. (=====)

1. 리액트 공식홈페이지 정의

1) useCallback

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

메모이제이션된 콜백을 반환한다. 메모이제이션된 콜백은 콜백의 의존성(dependency)이 변경되었을 때만 변경된다.

2) useMemo

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

메모이제이션된 값을 반환한다. 의존성(dependency)가 변경되었을 때만 메모이제이션된 값을 다시 계산한다. 이를 통해 불필요한 연산을 피한다.
useMemo로 전달된 함수는 렌더링 중에 실행된다! side effect는 useEffect에서 하는 일이고 useMemo에서 하는 일이 아니다.
의존성 배열에 아무 것도 없으면 매 랜더링마다 새 값을 계산한다.

useCallback(fn, deps)useMemo(() => fn, deps)와 같다.

2. 사전 지식

1) 일급 함수(first-function)

함수를 다른 변수와 같이 다룰 수 있는 함수를 말한다.

const number = 1;
const greeting = 'hello';
function foo() {
  return 'bar';
}

// 변수에 할당한 함수
const otherFoo = function() {
  return 'bar';
}
// 변수에 할당한 함수(Arrow Function)
const anotherFoo = () => 'bar';

number;
greeting;
foo();
otherFoo();
anotherFoo();

함수를 변수에 할당할 수 있으면 어떤 것들을 할 수 있을까?!

  • 다른 함수의 인자로 사용하기
  • 함수의 반환값으로 사용하기
    (함수의 반환 값이 함수가 될 수있기에)
  • 객체의 키 값으로 사용하기
  • 배열의 값으로 사용하기
  • Map 객체의 키로 사용하기

2) 고차 함수 (Higher-order function)

함수를 인자로 전달 받거나 함수를 결과로 반환하는 함수.
다음의 실행 값이 무엇이 나올지 생각해보자

const identity = x => x;
identity(1);
identity(foo); 
identity(foo)();

결과

identity(1); // 1
identity(foo); // ƒ foo() {
  return 'bar';
  }
identity(foo)(); // 'bar'

identity(foo)가 함수를 return한 부분이 중요하다.
identity(foo)()return한 함수를 실행시킨 결과를 반환했다.

3) 참조 평등(Referential equality)

참조 평등은 두 객체에 대한 포인터가 동일하다는 것을 의미한다.
결과를 예상해보자

const anotherFoo = () => 'bar';
function sameFoo() {
  return 'bar';
}
const fooReference = foo;

'hello' === 'hello';
greeting === otherGreeting;

foo === foo;
foo === otherFoo;
foo === anotherFoo;
foo === sameFoo;
foo === fooReference;

결과

'hello' === 'hello'; // true
greeting === otherGreeting; //true

foo === foo; // true
foo === otherFoo; // false
foo === anotherFoo; // false
foo === sameFoo; // false
foo === fooReference; //true

여기서 눈 여겨봐야 하는 부분은 greeting === otherGreeting; //truefoo === sameFoo; // false의 결과가 전혀 다른 것이다.
foosmaeFoo는 실제로 같지 않다. 즉, 포인터의 위치가 같지 않다.

3. useCallback과 useMemo

자, 위의 사전 지식을 열심히 습득했다면 이제 useCallbackuseMemo의 차이를 짐작할 수 있다!!!!
useCallbackuseMemo가 무엇을 반환하는지 생각해보자

useCallback(fn, deps)
useMemo(fn, deps)

결과
useCallback은 호출되지 않은 함수를 반환
useMemo호출한 함수의 결과를 반환

identity(foo); identity(foo)();의 결과 차이를 다시 생각해보자!!

useCallbackuseMemo의 사용

function foo() {
  return 'bar';
}

CONST memoizedCallback = useCallback( foo, []);
CONST memoizedResult = useMemo( foo, []);

위를 바탕으로 다음의 실행 결과를 예상해보자

memoizedCallback;
memoizedResult;
memoizedCallback();
memoizedResult();

결과

memoizedCallback; 
// f foo() {
  return 'bar';
}
memoizedResult; // 'bar'
memoizedCallback(); // 'bar'
memoizedResult(); // TypeError

4. 실제 리액트에서

리액트에서 useCallbackuseMemo가 사용되는 모습

function MyComponent({foo, initial}) {
  const memoizedCallback = useCallback(() => {
    someFunc(foo, bar);
  }, [foo, bar]);
  
  const memoizedResult = useMemo(() => someOtherFunc(foo, bar), [foo, bar]);
  // 등등
}
  • useCallback은 deps를 사용하는 함수를 호출하는 inline callback을 사용한다.
  • useMemo은 일부 함수를 호출하고 그 결과를 반환하는 "create function"을 사용한다.

1) 잘 못 사용된 useCallback!!

왜 다음은 잘 못 되었을까?

function sum(a, b) {
  console.log('sum() ran');
  return a + b;
}

function App() {
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  const [name, setName] = useState('Jim');

  const result = useCallback(sum(val1, val2), [val1, val2]);

  return (
    <div className="App">
      <input
        value={val1}
        onChange={({ target }) =>
          setVal1(parseInt(target.value || 0, 10))
        }
      />
      <input
        value={val2}
        onChange={({ target }) =>
          setVal2(parseInt(target.value || 0, 10))
        }
      />
      <input
        placeholder="Name"
        value={name}
        onChange={({ target }) => setName(target.value)}
      />
      <p>{result}</p>
    </div>
  );
}

힌트 : useCallback은 함수를 반환하지 값을 반환하지 않는다


: useCallback는 값의 메모이제이션에 사용할 수 없다. 즉. useCallback(fn(), [deps])로는 사용 불가

2) 앗, 나의 실수는?

import React, { useEffect, useState } from 'react';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';

function User({ userId }) {
 const [user, setUser] = useState({ name: '', email: '' });

 const fetchUser = async () => {
   const res = await fetch(
     `https://jsonplaceholder.typicode.com/users/${userId}`
   );
   const newUser = await res.json();
   setUser(newUser);
 };

 useEffect(() => {
   fetchUser();
 }, []);
 return (
   <ListItem dense divider>
     <ListItemText primary={user.name} secondary={user.email} />
   </ListItem>
 );
}

export default User;

해당 컴포넌트를 실행시킨다면 Infinity Loop를 확인 할 수 있다.
클로저 때문...!

const fetchUser = async () => {
 const res = await fetch(
   `https://jsonplaceholder.typicode.com/users/${userId}`
 );
 const newUser = await res.json();
 setUser(newUser); // 🔴 setState triggers re-render
};

useEffect(() => {
 fetchUser();
}, [fetchUser]); // fetchUser is a new function on every render

setState가 계속 랜더링이 되도록 만든다.
그럼 해결책은 어떤 것이 있을까?

(1) 클로저 해결

: useEffect내로 fetchUser 함수를 이동시켜 클로저 문제를 해결

// 1. Way to solve the infinite loop
useEffect(() => {
 const fetchUser = async () => {
   const res = await fetch(
     `https://jsonplaceholder.typicode.com/users/${userId}`
   );
   const newUser = await res.json();
   setUser(newUser); // Triggers re-render, but ...
 };

 fetchUser();
}, [userId]); // ✅ ... userId stays the same.

(2) useCallback 사용!

: userId가 바뀌면 fetchUser가 호출된다.

// 2. Way to solve the infinite loop
const fetchUser = useCallback(async () => {
 const res = await fetch(
   `https://jsonplaceholder.typicode.com/users/${userId}`
 );
 const newUser = await res.json();
 setUser(newUser);
}, [userId]);

useEffect(() => {
 fetchUser();
}, [fetchUser]); // ✅ fetchUser stays the same between renders

3) 비효율적인 함수를 어떻게 바꿀까?

기능에는 문제가 없지만 랜더링 될 때마다 실행되는 함수... 어떻게 할까?

// Some FP magic 🧙🏼‍♂️
const filter = (f, arr) => arr.filter(f);
const prop = key => obj => obj[key];
const getName = prop('name');
const strIncludes = query => str => str.includes(query);
const toLower = str => str.toLowerCase();
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const nameIncludes = query =>
 pipe(
   getName,
   toLower,
   strIncludes(toLower(query))
 );

function UserList({ query, users }) {
 // 🔴 Recalculated on every render
 const filteredUsers = filter(nameIncludes(query), users);
 // ...
}

해결!

: queryusers가 변경 되지 않으면 filteredUser 호출 없이 기존 값을 가져오도록!

function UserList({query, users}) {
  // ✅ Recalculated when query or users change
  const filteredUsesr = useMemo(
  () => filter(nameIncludes(query), users),
  [query, users]
  );
};
profile
프론트엔드 엔지니어

0개의 댓글