매 렌더링마다 새 객체 생성 방지 위해 useMemo 사용함

상황
WYSIWIG 에디터를 사용하는 AdminEditor.tsx
에서 발생. 이미지 버튼 클릭 시 기존 내장 기능이 아니라 handleClickImage
함수 실행되게 핸들러 연결하는 중이었다.
이를 위해 modules
변수 선언을 컴포넌트 내부로 들여와야 했다.
그랬더니 다음과 같은 에러 발생:
# Unhandled Runtime Error
TypeError: Cannot read properties of undefined (reading 'delta')
## Call Stack
### ReactQuill.componentDidUpdatenode_modules\react-quilllibindex.js (174:1)
문제 정의
modules
객체를 컴포넌트 내부에서 직접 정의했더니 렌더링할 때마다 ReactQuill에서 매번 새 객체가 생성되면서 발생한 문제.
참조 안정성 문제의 일종이다.
해결 방법
❌ modules
객체를 컴포넌트 외부로 옮기기
아까 왜 안 되는지 위에서 말했지. handler 함수 쓰려면 컴포넌트 내에 있어야 한다고.
⭕ useMemo
사용
useMemo
: 계산 비용이 큰 값을 메모이제이션해서 불필요한 재계산을 방지하는 리액트 내장 훅.
const modules = useMemo(() => {
return {
toolbar: {
container: [
생략
],
handlers: {
image: handleClickImage,
},
},
};
}, []);
맨 아래 의존성 배열에 빈 배열을 넣어 처음 한 번만 실행되게 한다. 의존성 배열 안에 든 게 바뀔 때마다 내부 콜백이 실행되는 거니까.
그래서 처음에 한 번만 객체가 생성되고, 이후로는 메모이제이션되어 렌더링 시마다 기존에 저장해 둔 값을 참조하게 된다.
내가 전에 알던 내용과 이번에 배운 거 비교
useMemo
불필요한 리렌더링을 줄여 리액트에서 성능을 최적화하는 한 방법으로서 메모이제이션에 대해 배웠었다. 메모이제이션은 이미 계산된 결과를 저장해두고 다음에 같은 계산을 반복하는 대신 불러오는 방법.
React.memo
로 컴포넌트를 감싸면 부모 컴포넌트가 리렌더링되더라도 자식의 props는 변경되지 않았을 경우 리렌더링하지 않는다.하지만 props에 전달되는 값이 함수나 객체일 경우에는, 부모가 리렌더링될 때 새로운 함수나 객체가 생성되기 때문에 props가 변경된 것으로 인식되는 한계
useCallback
: 함수 메모이제이션.useMemo
: 객체, 또는 긴 배열의 필터링 결과 같이 비용이 큰 계산을 메모이제이션.
다만
메모이제이션에도 비용이 드니까, 많이 복잡한 계산이 아니면 오히려 성능이 저하되므로 사용 지양.
리액트가 점점 더 효율적인 리렌더링 방식으로 업데이트되고있기 때문에, 점점 사용이 줄어드는 추세.
라고 배웠었다.
특히 두 번째 줄 때문에 useMemo에 대해 그리 중요하게 다루지 않았더랬다.
이번엔 그동안 배웠던 성능 최적화가 아니라, 객체 참조 안정성 용도로 useMemo를 사용했다.
이럴 때 여전히 유용한 도구였군.
useRef
예전에 렌더링 영향 안 주는 값 저장하려고 useRef
를 썼던 기억이 나서, 그거랑은 뭐가 다른 건지 정리해봤다.
useMemo
: 의존성 변경 시에만 함수 실행 - 빈 배열일 땐 첫 한 번만 객체 생성됨.useRef
: 매 렌더링마다 새 객체를 만들지만, 그 값의 변경이 리렌더링을 유발하지 않음.