함수형으로 react component를 설계할때 여러가지 hooks를 사용할 수 있다. 보통 useEffect, useState로 웬만한 props, state 로직을 구현할 수 있기 때문에 부가적인 기능을 도와주는(정확히 말하자면 렌더링 성능을 최적화해주는) useMemo나 useCallback은 필수로 사용하지 않아도 된다. 하지만 이 두가지 hook을 잘 사용하기만 한다면 훨씬 높은 퍼포먼스를 기대할 수 있다.
그전에 몇가지 짚고 넘어가야할 것들이 있다. 기본적으로 리액트는 shallow copy를 실행한다. 참조값만 비교를 한다는 것이다. (즉, object !== object인셈) 그리고 component가 렌더링 된다는 것은 함수로 작성된 것을 call하여 실행되는것을 말한다. 그렇게 때문에 함수가 실행될 때마다 그 내부에 선언되어 있던 코드들(변수, 또다른 함수들) 또한 매번 다시 선언되어서 사용된다. 이를 잘 유념하면서 useMemo, useCallback을 생각해보도록 하자.
useMemo()
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);리액트 공식문서에는 useMemo를 메모이제이션(Memoization)된 값을 반환합니다 라고 설명한다. 위의 예제를 설명해보자면, deps로 넣어준 a 또는 b가 변경이 될때에만 해당 값을 재계산 하는 것이다. 한 component내에 여러개 state를 가지고 있을때에 특정한 값에 useMemo를 사용하게 되면 최적화를 할 수 있다.
사용예시
//const count = countMember(members);
const count = useMemo(() => countMember(members), [members]);
// members가 바뀔때에만 계산을 해주면 된다. 그외에 memberName, age 등은 관심밖이다.
return (
<>
<Member
memberName={memberName}
age={age}
onChange={onChange}
onCreate={onCreate}
/>
<MemberList members={members} onRemove={onRemove} onToggle={onToggle} />
<div>멤버수(명) : {count}</div>
</>
);아래 코드는 useMemo가 내부적으로 동작하는 코드이다 (출처:Facebook github)
areHookInputsEqual이라는 함수를 통해, 받아올 deps 변경이 없을 경우 이전의 prevState를 반환한다.
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*/
function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
workInProgressHook = createWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
if (workInProgressHook !== null) {
const prevState = workInProgressHook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
}
// ... 중간생략
}useCallback()
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);리액트 공식문서에는 useCallback를 메모이제이션(Memoization)된 콜백(함수)을 반환합니다 라고 설명한다. 위의 예제를 설명해보자면, deps로 넣어준 a 또는 b가 변경이 될때에만 해당 함수를 다시 생성하는 것이다.
component가 렌더링 될때마다 내부적으로 사용한 함수가 새롭게 생성되는 경우, 자식 component에 props으로 새로 생성된 함수가 넘겨지게 되면 불필요한 렌더링이 발생할 것이다.
사용예시
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const onSaveMemberInfo = useCallback(() => saveMemberInfo(name, age), [name, age]);
// 실제 렌더링에 영향을 끼치는 name, age가 변경될때에 함수를 생성하여 그외의 상황에서 <Member />가 렌더링되는것을 막는다
return(
<>
<div>멤버 이름: {name}</div>
<div>나이 : {age}</div>
<Member
onSave={onSaveMemberInfo}
setName={setName}
setAge={setAge}
/>
</>
)아래는 useCallback이 내부적으로 동작하는 코드이다 (출처:Facebook github)
useMemo를 기반으로 함수를 받아오는 형태를 취한다.
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*/
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
return useMemo(() => callback, deps);
}그리고 useMemo, useCallback을 사용할 때에 지켜야할 몇가지 주의사항이 있다.
leaf component에는 사용하지 말아야한다 (당연)- 해당
hook안에서 사용하는state, 혹은props가 있다면 꼭deps배열안에 포함시켜야한다. 만약 그렇지 않으면 해당 값에 대해 최신값을 참조할 것이라고 보장할 수 없다. - 의존성 배열에 새로운 객체와 배열을 전달해서는 안된다. 이는 의존성이 달라질 수 있다는 것을 의미하고 메모이제이션을 하는 의미가 없다.
const [first, ...rest] = array;
const dontDoLikeThis = useCallback(() => { someFunction(); }, [rest]);- [중요] 무조건
useMemo와useCallback을 사용한다고 해서 성능이 더 좋아지지는 않는다.
React devtool의 profiler를 사용하면 컴포넌트 속도를 측정할 수 있다. 이를 이용해 필요한 경우 useMemo, useCallback을 사용하여 상태변경을 좀 더 효율적으로 만들 수 있다.
- https://ko.reactjs.org
- https://github.com/facebook/react
- https://kentcdodds.com/blog/usememo-and-usecallback