티스토리 뷰
🔎 함수 메모이제이션
useCallback은 함수를 메모이제이션(memoization)하기 위헤서 사용되는 hook 함수입니다.
첫 번째 인자로 넘어온 함수를, 두 번째 인자로 넘어온 배열 내의 값이 변경될 때까지 저장해 놓고 재사용할 수 있게 해줍니다.
const memoizationCallback = useCallback(함수, 배열);
예를 들어, 어떤 React 컴포넌트 함수 안에 함수가 선언이 되어 있다면 이 함수는 해당 컴포넌트가 렌더링될 때마다 새로운 함수가 생성됩니다.
const add = () => x + y;
하지만 useCallback()을 사용하면, 해당 컴포넌트가 렌더링되더라도 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환합니다.
즉 x 또는 y 값이 바뀌면 새로운 함수가 생성되어 add 변수에 할당되고, x와 y 값이 동일하다면 다음 렌더링 때 이 함수를 재사용합니다. 다시 호출하지 않고요!
const add = useCallback(() => x + y, [x, y]);
📌 알고있자
사실 컴포넌트가 렌더링될 때마다 함수를 새로 선언하는 것은 자바스크립트가 브라우저에서 얼마나 빨리 실행되는지를 생각해보면 성능 상 큰 문제가 되지는 않습니다.
따라서 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 useCallback()을 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있습니다.
그럼 도대체 useCallback() 훅 함수는 어떻게 쓸 때 의미있는 성능 향상을 기대할 수 있을까요?
📌 자바스크립트 함수 동등성
useCallback() 훅 함수를 언제 사용해야하는지 제대로 이해하려면 먼저 자바스크립트 함수 간의 동등함이 어떻게 결정되는지 알 필요가 있는데요.
브라우저 콘솔을 열고 다음과 같이 동일한 코드의 자바스크립트 함수가 동일한지 === 연산자를 통해 비교를 해보면 false가 반환될 것입니다.
> const add1 = () => x + y;
undefined
> const add2 = () => x + y;
undefined
> add1 === add2
false
자바스크립트에서 함수도 객체로 취급이 되기 때문에 메모리 주소에 의한 참조 비교가 일어나게 됩니다.
이러한 자바스크립트 특성은 React 컴포넌트 함수 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제로 이어질 수 있습니다.
📌 의존 배열로 함수를 넘길 때
많은 React hook 함수들이 불필요한 작업을 줄이기 위해서 두 번째 인자로, 첫 번째 함수가 의존해야 하는 배열을 받습니다.
예를 들어, 다음과 같이 컴포넌트에서 API를 호출하는 코드는 fetchUser 함수가 변경될 때만 호출됩니다.
여기서 위에서 설명한 자바스크립트가 함수의 동등성을 판단하는 방식 때문에 예상치 못한 무한 루프가 발생하게 됩니다.
fetchUser는 함수이기 때문에, userId 값이 바뀌든 말든 컴포넌트가 렌더링될 때마다 새로운 참조값으로 변경이 됩니다.
그러면 useEffect() 함수가 호출되어 user 상태값이 바뀌고 그러면 다시 렌더링이 되고 그럼 또 다시 useEffect() 함수가 호출되는 악순환이 반복됩니다.
import { useState, useEffect } from "react";
function Profile({ userId }) {
const [user, setUser] = useState(null);
const fetchUser = () =>
fetch(`https://your-api.com/users/${userId}`)
.then((response) => response.json())
.then(({ user }) => user);
useEffect(() => {
fetchUser().then((user) => setUser(user));
}, [fetchUser]);
// ...
}
이와 같은 상황에서 useCallback() hook 함수를 이용하면 컴포넌트가 다시 렌더링되더라도 fetchUser 함수의 참조값을 동일하게 유지시킬 수 있습니다.
따라서 의도했던 대로, useEffect()에 넘어온 함수는 userId 값이 변경되지 않는 한 재호출되지 않게 됩니다.
import { useState, useEffect } from "react";
function Profile({ userId }) {
const [user, setUser] = useState(null);
const fetchUser = useCallback(
() =>
fetch(`https://your-api.com/users/${userId}`)
.then((response) => response.json())
.then(({ user }) => user),
[userId]
);
useEffect(() => {
fetchUser().then((user) => setUser(user));
}, [fetchUser]);
// ...
}
📌 React.memo와 함께 사용하기
useCallback() hook 함수는 자식 컴포넌트의 렌더링의 불필요한 렌더링을 줄이기 위해서 React.memo() 함수와도 사용할 수 있습니다.
예를 들어, 방이름(room), 조명 켜짐 여부(on), 조명 제어 함수(toggle)를 propfh Light 컴포넌트를 작성해보겠습니다.
import React from 'react';
function Light({room, on, toggle}) {
console.log({room, on});
return (
<button onClick={toggle}>
{room} {on ? '💡' : '⬛'}
</button>
);
}
// 여기에 추가해줄 예정!
그리고 React.memo() 함수로 이 컴포넌트를 감싸줍니다.
이렇게 React 컴포넌트 함수를 React.memo() 함수로 감싸주면
컴포넌트 함수는 props 값이 변경되지 않는 한 다시 호출되지 않습니다.
Light = React.memo(Light);
다음으로 3개의 방의 스위치를 중앙 제어해주는 SmartHome 컴포넌트를 작성합니다.
import { useState, useCallback } from "react";
function SmartHome() {
const [masterOn, setMasterOn] = useState(false);
const [kitchenOn, setKitchenOn] = useState(false);
const [bathOn, setBathOn] = useState(false);
const toggleMaster = () => setMasterOn(!masterOn);
const toggleKitchen = () => setKitchenOn(!kitchenOn);
const toggleBath = () => setBathOn(!bathOn);
return (
<>
<Light room="침실" on={masterOn} toggle={toggleMaster} />
<Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
<Light room="욕실" on={bathOn} toggle={toggleBath} />
</>
);
}
이 컴포넌트를 이용해서 침실의 조명을 켜보면 침실 뿐만 아니라 다른 모든 방에 대한 Light 컴포넌트 함수가 호출이 되는 것이 콘솔 로그로 확인될 것입니다.
{room: "침실", on: true}
{room: "주방", on: false}
{room: "욕실", on: false}
조명을 키거나 끄는 방에 대한 Light 컴포넌트 함수만 호출되게 하고 싶어서 React.memo()를 사용한 것인데 무엇이 문제일까요?
바로 조명을 제어할 때 쓰이는 toggleMaster(), toggleKitchen(), toggleBath() 함수의 참조값이 SmartHome 컴포넌트가 렌더링될 때마다 모두 바뀌어버리기 때문입니다.
이 문제를 해결하려면 모든 조명 제어 함수를 useCallback() hook 함수로 감싸고 두 번재 인자로 각 함수가 의존하고 있는 상태를 배열로 넘겨야 합니다.
import { useState, useCallback } from "react";
export default function SmartHome() {
const [masterOn, setMasterOn] = useState(false);
const [kitchenOn, setKitchenOn] = useState(false);
const [bathOn, setBathOn] = useState(false);
const toggleMaster = useCallback(() => setMasterOn(!masterOn), [masterOn]);
const toggleKitchen = useCallback(() => setKitchenOn(!kitchenOn), [kitchenOn]);
const toggleBath = useCallback(() => setBathOn(!bathOn), [bathOn]);
return (
<>
<Light room="침실" on={masterOn} toggle={toggleMaster} />
<Light room="주방" on={kitchenOn} toggle={toggleKitchen} />
<Light room="욕실" on={bathOn} toggle={toggleBath} />
</>
);
}
이제 침실에 조명을 켜보면 침실에 대한 Light 컴포넌트 함수만 호출되는 것을 확인할 수 있습니다.
{room: "침실", on: true}
'프론트엔드 > React' 카테고리의 다른 글
[React] 리액트 불변성? 불변성을 지켜야 하는 이유 (0) | 2023.02.18 |
---|---|
[React] 커스텀 Hooks - 중복되는 로직을 한줄로 처리하도록 (0) | 2023.02.13 |
[React] useMemo - 연산한 값을 재사용하자 (0) | 2023.02.13 |
[React] state 제대로 사용하자 - 얕은 복사 & 깊은 복사 (0) | 2023.02.11 |
[React] SOLID 원칙에 기초한 React 코드 작성법 (0) | 2023.02.11 |
- Total
- Today
- Yesterday
- CSS
- next.js
- 프로젝트 회고
- 자바스크립트 기초
- 프론트엔드 기초
- 스타일 컴포넌트 styled-components
- 타입스크립트
- 데이터분석
- styled-components
- 리액트 훅
- TypeScript
- 딥러닝
- HTML
- react-query
- Python
- 디프만
- 자바스크립트
- react
- 자바
- 인프런
- jest
- 프론트엔드
- testing
- rtl
- 프론트엔드 공부
- 리액트
- JSP
- 머신러닝
- frontend
- 파이썬
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |