debounce란, 특정 이벤트가 연속으로 많이 호출되는 경우, 성능을 위하여 일정 시간 모았다 한번에 처리하는 함수를 통칭한다.
대표적으로 input 이벤트를 처리할 때 많이 사용된다.
// debounce 함수 - 지정한 시간까지 callback함수를 묶어놓았다 처리한다
const debouncer = (callback, limit) => {
let timeout
return (...args) => {
clearTimeout(timeout)
timeout = setTimeout(() => callback(...args), limit)
}
}
// debounce 함수 사용 예
const printValue = debouncer((value) => console.log(value), 500);
...
<input type="text" onChange={e => printValue(e.target.value)} />
그런데, debounce 로직을 react hook에서 그대로 가져다 사용하려고 하면 되지 않는다.
이러한 원인은 함수형 컴포넌트 내부의 함수는 컴포넌트가 re-render 되면, 초기화 되기 때문이다.
보통 debounce 함수가 많이 사용되는 change event의 경우, 계속해서 dom에 변화를 줘버리기 때문에 계속 리렌더링이 일어난다. 이렇게 리렌더링이 발생하면, 컴포넌트 내 모든 함수가 모두 초기화 되고, 열심히 setTimeout을 돌리던 우리 printValue도 무색하게 초기화 되어 값이 반환되지 못하게 된다.
이렇게 printValue가 초기화 되지 않도록 함수형 컴포넌트로 debouce 함수를 구현하는 경우, useCallback() 또는 useMemo()를 사용하여 처리한다.
useCallback과 useMemo를 이해하기 위해서는, 우선적으로 메모이제이션에 대한 이해가 있어야 한다.
메모이제이션(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. (위키백과)
쉽게말하면, 같은 이전에 저장된 값과 지금 막 저장된 값이 같은지를 비교하여, 같지 않은 경우에만 update를 해주는 방식이다.
“메모이제이션된 콜백 함수를 반환한다” 라고 리액트 공식 문서에 적혀있는데..
이렇게만 보면 감이 잘 오지 않는다.
쉽게 말하면, useCallback이 받는 callback함수의 리턴 값을 어디 저장해두었다가, 그 값이 어느 시점에 바뀔때만 리턴한다고 생각하면 쉽다.
const memoizedCallback = useCallback(
() => {
doSomething(a, b); // a나 b가 번경되었을 때, doSomething(a,b)의 값을 메모이제이션 된 값과 비교하여 리턴
},
[a, b],
);
이걸 왜쓰나요?
리액트를 다루다보면 내가 움직이지 않았으면 하는 부분도 렌더링이 일어나버려 값이 안나오거나(엄연히 말하면 초기화 되는거지만, 겉으로 보기에는 값이 아예 리턴되지도 않아보인다), 성능이 저하되는 경우가 생긴다.
특정 값이 어딘가에 저장되어 날라가지 않도록 하고 싶거나, 너무 많은 렌더링으로 떨어지는 성능을 개선시키고 싶을때 이러한 기능을 사용한다.
위의 debounce 케이스도, 렌더링으로 인해 setTimeout 대기를 탔다가 return이 되기도 전에 값이 리셋되는 안타까운 일이 발생한다.
역시, useCallback을 이용하면 렌더링이 일어나던 말던 우선 callback으로 보내준 함수가 리턴하는 값이 보전되기에, 렌더링 여부와 상관없이 값을 유지할 수 있다.
// useCallback 사용 예
const printValue = useCallback(
debouncer((value) => setKeyword(value), 800),
[],
)
이 아이도 결국 하는 일은 useCallback과 동일하다. 메모이제이션 된 값을 반환한다.
그럼 뭐가 useCallback과 다른거죠?
그건 나도 몰류 는 아니고,
쉽게 보이는 큰 차이점은 콜백 함수를 받느냐, 직접 새로 함수를 생성하는 형태이냐 이 차이다.
// 다시 보는 useCallback 사용 예
const printValue = useCallback(
debouncer((value) => setKeyword(value), 800), // 바로 debouncer 받아옴
[],
)
// useMemo 사용 예
const memoizedValue = useMemo(
() => debouncer((value) => setKeyword(value), 800), // () => {} 형태로 새로 함수를 생성
[]
);
useCallback의 경우 굳이 () ⇒ {} 이런식으로 callback의 callback을 쓸 필요 없이, 바로 내가 원하는 callback 함수를 기재해주면 되지만, useMemo의 경우 원칙적으로는, 새로이 함수를 생성하는 것이기에 callback 함수를 새로 생성하는 함수처럼 감싸서 제공해야한다.
이를 조금 있어보이게 말하면, useCallback은 인라인 callback을 사용하는 것이고, useMemo의 경우 create함수(생성함수)를 사용하는 방법으로 말할 수 있다.
useCallback
useCallback의 메모이제이션된 값은 콜백의 의존성이 변경되었을 때에만 변경된다. 수시로 렌더링하면서 자식 컴포넌트를 업데이트 해야할 필요가 없는 경우에 유용하다.
useMemo
**useMemo
로 전달된 함수는 렌더링 중에 실행된다는 특징이 있다**. 그래서 렌더링 중에 처리 필요한 것들만 useMemo로 처리하길 권장되고 있다.
의존성 배열이 없는 경우 매 렌더링 때마다 새 값을 계산하기 때문에 유의해서 사용할 필요가 있다.
헷갈렸던 부분인데 잘 정리해주셔서 대단히 감사합니다 ㅎㅎ