이전에 React Hooks에 대한 내용으로 useState와 useEffect를 다룬적이 있다.
오늘은 useMemo, useCallback, useRef를 한번 알아가보도록 하자

useMemo() : memoized value를 리턴하는 hook이다
여기서 memoized value는 무엇일까?
useMemo와 뒤에 소개해드릴 useCallback에서는 memoization이라는 개념이 나온다.
최적화를 위해서 사용하는 개념이다.
비용이 높은. 즉, 연산량이 많이 드는 함수의 호출의 결과를 저장해두었다가 같은 입력값으로 함수를 호출하면 새로 함수를 호출하지 않고 이전에 저장해놨던 호출 결과를 바로 반환하는것이다.
이렇게하면 함수 호출 결과를 받기까지의 시간을 단축시킬 수 있을뿐더러 불필요한 중복 연산도 하지 않기 때문에 컴퓨터의 자원을 적게 쓰게 된다.
memoization된 결과값을 영어로는 memoized value라고 부른다.
메모를 해두었다가 다시 사용하는 것을 말한다.
const memoizedValue = useMemo(
() => {
//연산량이 높은 작업을 수행하여 결과를 반환
return computeExpensiveValue(의존성 변수1, 의존성 변수2);
},
[의존성 변수1, 의존성 변수2]
);
의존성 배열에 들어있는 변수가 변했을 경우에만 새로 create함수를 호출하여 결과를 리턴한다.
그렇지 않은 경우에는 기존함수의 결과값을 그대로 반환한다.
useMemo()를 사용하면 컴포넌트가 다시 렌더링 될때마다 연산량이 높은 작업을 반복하는 것을 피할 수 있다.
결과적으로 빠른 렌더링 속도를 얻을 수 있다.
useMemo로 전달된 함수는 렌더링이 일어날때 실행된다.
따라서 일반적으로 렌더링될때 실행되어서는 안되는 작업을 useMemo에 넣으면 안된다.
예를들면 useEffect()에서 사용되어야하는 side Effect같은 것들이 있다.
서버에서 데이터를 받아오거나 수동으로 DOM을 변경하는 작업 등은 렌더링이 일어나는 동안에 실행되어서는 안되기 때문에 useMemo() 함수에 넣으면 안되고 useEffect()를 사용해야한다.
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b)
);
위의 코드와 같이 useMemo()에 의존성 배열을 넣지 않을 경우, 매 렌더링마다 함수가 실행된다.
따라서 useMemo()에 의존성 배열을 넣지 않고 사용하는 것은 아무런 의미가 없다.
const memoizedValue = useMemo(
() => {
return computeExpensiveValue(a, b);
},
[]
);
useMemo()에 빈 배열을 넣을 경우 컴포넌트 마운트시에만 호출된다.
결국 마운트 이후에는 값이 변경되지 않기때문에 마운트시점에서만 한번 값을 계산하면 되는 경우에는 의존성 배열에 빈 배열을 넣어줘도 괜찮다.
하지만 대부분의 경우에는 useMemo()의 의존성배열에 변수들을 넣고 해당 변수들이 바뀜에 따라 새로 값을 계산하는 경우에 사용한다.
useCallback() : useMemo()와 비슷하지만 값이 아닌 callback 함수를 반환한다.
즉, 컴포넌트가 렌더링 될때마다 매번 함수를 새로 정의하는 것이 아닌 의존성배열의 값이 바뀐 경우에만 함수를 새로 정의해서 리턴해주는 것을 의미한다.
const memoizedValue = useCallback(
() => {
doSomething(의존성 변수1, 의존성 변수2);
},
[의존성 변수1, 의존성 변수2]
);
useCallback()은 useMemo()와 마찬가지로 함수와 의존성 배열을 parameter로 받는다.
useCallback()에서 parameter로 받는 이 함수를 callback이라고 부른다.
의존성 배열에 있는 변수 중 하나라도 변경되면 memoization된 callback함수를 반환한다.
의존성 배열에 따라 memoized value를 반환한다는 점에서는 useMemo()와 완전히 동일하다.
useCallback(함수, 의존성 배열);
useMemo(() => 함수, 의존성 배열);
만약 useCallback()을 사용하지 않고 컴포넌트를 정의한다면 매번 렌더링이 일어날때마다 함수가 새로 정의된다.
따라서 useCallback()을 사용하여 특정 변수의 값이 변한 경우에만 함수를 다시 정의하도록 해서 불필요한 반복 작업을 없애주는 것이다.
import { useState } from "react";
function ParentComponent(props) {
const [count, setCount] = useState(0);
//재렌더링 될 때마다 매번 함수가 새로 정의됨
const handleClick = (event) => {
//클릭 이벤트 처리
};
return (
<div>
<button onClick={() => {
setCount(count + 1);
}}
>
{count}
</button>
<ChildComponent handleClick={habdleClick} />
</div>
);
}
위 예제코드처럼 useCallback()을 사용하지 않고 컴포넌트 내에서 정의한 함수를 자식 컴포넌트에 props로 넘겨 사용하는 경우에 부모 컴포넌트가 다시 렌더링이 될때마다 자식 컴포넌트도 렌더링이 된다.
import { useState } from "react";
function ParentComponent(props) {
const [count, setCount] = useState(0);
//컴포넌트가 마운트 될 때만 함수가 정의됨
const handleClick = useCallback((event) => {
//클릭 이벤트 처리
}, []);
return (
<div>
<button onClick={() => {
setCount(count + 1);
}}
>
{count}
</button>
<ChildComponent handleClick={habdleClick} />
</div>
);
}
하지만 위 예제코드처럼 const handleClick 에서 useCallback()을 사용하면 특정 변수의 값이 변한 경우에만 함수를 다시 정의하게 되므로 함수가 다시 정의되지 않는 경우에는 자식 컴포넌트도 재렌더링이 일어나지 않는다.
이 경우에는 의존성배열에 빈 배열이 들어갔기 때문에 컴포넌트가 처음 마운트되는 시점에만 함수가 정의되고 이후에는 다시 정의되지 않으며 결국 자식 컴포넌트도 불필요하게 재렌더링이 일어나지 않게 된다.
reference를 사용하기 위한 hook이다.
React에서 reference란 특정 컴포넌트에 접근할 수 있는 객체를 의미한다.
useRef()는 바로 이 reference 객체를 반환한다.
reference 객체에는 current라는 속성이 있는데 이것은 현재 레퍼런스하고 있는 element를 의미한다.
refObject.current
const refContainer = useRef(초기값);
파라미터에 초기값을 넣으면 해당 초기값으로 초기화된 레퍼런스 객체를 반환한다.
만약 초기값이 null이라면 current의 값이 null인 레퍼런스 객체를 반환한다.
이렇게 반환된 레퍼런스 객체는 컴포넌트의 라이프 타임 전체에 걸쳐서 유지된다.
즉, 컴포넌트가 마운트 해제 전까지는 계속 유지된다.
쉽게 말해 useRef()은 변경 가능한 current라는 속성을 가진 하나의 상자라고 생각하면 된다.
function TextInputWithFocusButton(props) {
const inputElem = useRef(null);
const onButtonClick = () => {
// 'current'는 마운트된 input element를 가리킴
inputElem.current.focus();
};
return (
<>
<input ref={inputElem} type="text" />
<button onClick={onButtonClick}>
Focus the input
</button>
</>
);
}
위 예제코드는 useRef()을 사용하여 버튼 클릭 시 input에 포커스를 하도록 하는 코드이다.
초기값으로 null을 넣어주었고 결과로 반환된 InputElem이라는 레퍼런스 객체를 input태그에 넣어주었다.
그리고 버튼 클릭 시 호출되는 함수에서 inputElem.current를 통해 실제 엘리먼트에 접근하여 focus 함수를 호출하고 있다.
<div ref={myRef} />
react에서는 위와 같이 코드를 작성하면 node가 변경될때마다 myRef의 current속성에 현재 해당되는 DOM node를 저장한다.
useRef()은 클래스의 인스턴스 필드를 사용하는 것과 유사하게 다양한 변수를 저장할 수 있다는 장점이 있다.
이런것이 가능한 이유는 useRef()이 일반적인 자바스크립트 객체를 리턴하기 때문이다.
useRef()는 매번 렌더링될때마다 같은 레퍼런스 객체를 반환한다.
여기서 하나 기억해야할 점은 useRef()는 내부의 데이터가 변경되었을때 별도로 알리지 않는다.
그래서 current속성을 변경한다고해서 재렌더링이 일어나진 않는다.
따라서 DOM node에 연결되었거나 분리되었을 경우에 어떤 코드를 실행하고 싶다면 callback Ref를 사용해야한다.
DOM node의 변화를 알기 위한 가장 기초적인 방법으로 callback Ref를 사용하는 방법이 있다.
이렇게 해주면 react에서는 ref가 다른 node에 연결될때마다 callback을 호출하게 된다.
function MeasureExample(props) {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>안녕, 리액트</h1>
<h2>위 헤더의 높이는 {Math.round(height)}px 입니다.</h2>
</>
);
}
위 예제코드는 레퍼런스를 위해서 useRef()를 사용하지 않고 useCallback()을 사용하는 Callback Ref방식을 사용하였다.
useRef()를 사용하면 레퍼런스 객체가 current속성이 변경되었는지 따로 알려주지 않기 때문에 callback Ref를 사용하였다.
callback Ref방식을 사용하게 되면 자식 컴포넌트가 변경되었을때 알림을 받을 수 있고, 이를 통해 다른 정보들을 업데이트 할 수 있다.
이 코드는 h1의 높이 값을 매번 업데이트 하고 있다.
그리고 useCallback()의 의존성 배열로 빈 배열을 넣어준것을 알 수 있는데, 이것은 h1태그가 mount, unmount 될때만 callback함수가 호출되며 재렌더링이 일어날때에는 호출되지 않는다.
참고 사이트 : https://ko.legacy.reactjs.org/docs/hooks-reference.html