eslint를 다시 정리하고 실행해서 나온 에러를 보며 코드를 수정하던 중 다음과 같은 에러를 만났다.
The 'setUpRoom' function makes the dependencies of useEffect Hook (at line 88) change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of 'setUpRoom' in its own useCallback() Hook react-hooks/exhaustive-deps
문제가 되는 코드는 다음과 같았다.
const setUpRoom = async (shouldCreateRoom: boolean) => {
if (shouldCreateRoom) {
socket?.emit('createRoom', { roomId });
} else {
socket?.emit('joinRoom', { roomId }, () => {});
socket?.on('chatClosed', () => {
setShowModal(true);
});
}
setIsJoinedRoom(true);
};
useEffect(() => {
if (!isConnected || !socket || !roomId || isJoinedRoom) return;
setUpRoom(isProducer);
// ...
}, [isConnected, roomId, socket, isJoinedRoom, isProducer, setUpRoom]); // 여기서 문제 발생
이렇게 되면 컴포넌트가 리렌더링될 때마다 setUpRoom 함수가 새로 생성되고, 이로 인해 useEffect가 불필요하게 다시 실행되게 된다. 그렇기에 setUpRoom이 새로 생성될 필요가 없을 때는 다시 생성되지 않도록 해주는 것이 좋다.
이럴때 사용하는 것이 useCallback
!
😮 useCallback이 뭐하는 애죠?
useCallback은 리렌더링 간에 함수 정의를 캐싱해 주는 React Hook입니다.
출처: https://ko.react.dev/reference/react/useCallback
import { useCallback } from 'react';
const func = useCallback(fn, dependencies);
React는 첫 렌더링에서 인자로 전달한 fn
을 반환한다. 이 함수는 캐싱할 함숫값으로, 컴포넌트가 리렌더링됐을 때 두번째 인자인 dependencies
의 값이 계속 동일하다면 React는 같은 함수를 다시 반환한다. 반대로 dependencies
의 값이 변경되었다면 이번 렌더링에서 전달한 함수를 반환하고 나중에 재사용할 수 있도록 이를 저장한다.
🤔 useCallback을 왜 사용해요?
1️⃣ 컴포넌트의 리렌더링 건너뛰기
React는 기본적으로 컴포넌트가 리렌더링될 때 자식 컴포넌트도 재귀적으로 리렌더링한다. 그렇지만 부모 컴포넌트가 리렌더링되어야 할 때, 자식 컴포넌트는 리렌더링될 필요가 없는 경우도 많다. 이런 경우에 사용하는 것이 memo
이다.
memo
로 자식 컴포넌트를 감싸면 마지막 렌더링과 동일한 props일때 리렌더링을 건너뛰도록 할 수 있다. (memo
에 대한 개념 정리를 하기 위한 글은 아니니 이 부분에 대한 자세한 설명은 건너뛴다.)
이런 때가 함수 캐싱이 중요해지는 순간이다. 만약 useCallback
이 없어서 부모 컴포넌트에서 정의한 함수가 계속 리렌더링되는데 이게 자식 컴포넌트에 props로 전달되는 함수라면? 그럼 memo
로 백날 감싸봤자 아무 소용 없다. memo
는 props가 변경되면 리렌더링되는데, props로 전달되는 함수의 참조가 바뀐다면 결국 리렌더링되게 된다.
그러니까 자식 컴포넌트에 props로 함수를 넘기고 있고, memo
로 렌더링을 최적화하고 있다면 useCallback
을 써서 불필요한 리렌더링을 막자!
2️⃣ Effect가 너무 자주 실행되는 것을 방지하기
이게 바로 위에서 useCallback
을 알아보게 된 이유다. 구현을 하다보면 useEffect
안에서 함수를 호출하는 경우가 많은데 이럴 때 useCallback
이 유용하다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
// ...
이런 경우 createOptions
를 의존성 배열에 넣어야 하는데, 여기서 문제가 발생한다. 컴포넌트가 리렌더링될 때 createOptions
가 다시 실행되며 함수 참조가 바뀌게 되고, 그러면 useEffect
는 의존성 배열의 값이 바뀌었다고 인식을 하므로 effect가 불필요하게 재실행되어 connection이 계속 다시 연결되는 일이 발생한다.
이를 해결하기 위해서는 effect에서 호출되는 함수를 useCallback
으로 감싸면 된다.
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]);
이렇게하면 roomId
가 변경되지 않는 한, createOptions
함수는 같은 참조를 가짐을 보장할 수 있다.
그렇지만 이보다 좋은 방법은 함수 의존성을 제거하는 것이다. 방법도 쉽다. 그냥 함수를 effect 안으로 이동시키기만 하면 된다. (React 공식 문서에서 이렇게 권장하고 있다!)
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ useCallback이나 함수 의존성이 필요하지 않습니다.
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ roomId가 변경될 때만 변경됩니다.
// ...
3️⃣ 커스텀 Hook 최적화하기
커스텀 Hook을 사용하는 경우, 반환하는 모든 함수를 useCallback
으로 감싸는 것이 좋다.
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
}
이것도 위의 내용과 비슷한 이유인데,
- 커스텀 훅을 사용하는 컴포넌트가
React.memo
나useMemo
등으로 최적화되어 있을 때도 의도한대로 동작한다. - 커스텀 훅 사용자가 별도의 최적화 작업을 하지 않아도 된다.
- 재사용 가능한 훅을 사용하면서도 성능상의 이점을 가져갈 수 있다.
즉, 해당 커스텀 훅에서 선언해서 리턴하는 함수도 useCallback
을 이용하여 최적화해줄 수 있다는 말이다.
🔗 참고 자료
'공부 > React' 카테고리의 다른 글
[React/리액트] TypeScript+SCSS 눈 내리는 효과 만들기 (1) | 2023.12.14 |
---|---|
React는 프레임워크인가 라이브러리인가? (1) | 2023.12.05 |
[React/리액트] CORS 에러 방지 Proxy 추가 (0) | 2023.10.27 |
[React] 코드 스플리팅 (0) | 2023.05.05 |