useVallback()
은 함수를 메모제이션하기 이해서 사용되는 hook함수이다. 첫번째 인자로 넘어온 함수를, 두번째 인자로 넘어온 배열내의 값이 변경될 때까지 저장해놓고 재사용할 수 있게 해준다.
const memorizationCallback = useCallback(함수, 배열);
예로, 어떤 React 컴포넌트 함수안에 함수가 선언이 되어 있다면, 이 함수는 해당 컴포넌트가 랜더링될 때 마다 새로운 함수가 생성된다.
const add = () => x + y;
그러나, useCallback()
을 사용하면, 해당 컴포넌트가 랜더링되더라도 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환한다. 즉 x
또는 y
값이 바뀌면 새로운 함수가 생성되어 add
변수에 할당되고, x
와 y
값이 동일하다면 다음 랜더링때 이 함수를 재사용한다.
const add = useCallback(() => x + y, [x, y]);
이렇게 랜더링에 큰역할을 하는 useCallback은 사실 잘사용하면 큰 득이되지만, 잘못쓰게되면 오히려 안좋게 작용할수도있다. 사실 컴포넌트가 렌더링될 때마다 함수를 새로 선언하는것은 자바스크립트가 브라우저에서 얼마나 발리 실행되는지를 생각해보면 성능 상 큰 문제는 되지않느다. 따라서 컴포넌트내에서 함수를 반복해서 생성하지 않기 위해 useCallback()
을 사용하는것은 큰의미가없을수도있다.
useCallback()
hook 함수를 언제 사용해야하는지 제대로 이해하려면 먼저 자바스크립트에서 함수 간의 동등함이 어떻게 결정되는지 알 필요가 있다.
const add1 = () => x + y;
const add2 = () => x + y;
add1 === add2 // false
자바스크립트에서는 함수도 객체로 취급이 되기 떄문에 메모리 주소에 의한 참조 비교가 일어나기 때문이다.
이러한 자바스크립트 특성은 react컴포넌트 함수 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 Prop으로 넘길떄 예상치 못한 성능 문제로 이어 질수있다.
많은 React hook 함수들이 불필요한 작업을 줄이기 위해서 두 번쨰 인자로, 첫 번째 함수가 의존해야하는 배열을 받는다. 예로 useEffect()
함수는 두번째 인자로 넘어온 의존 배열이 변경될 때만 첫 번째 인자로 넘어온 함수를 호출한다.
다음과 같이 컴포넌트에서 API를 호출하는 코드는 fetchUser
함수가 변경되 때만 호출된다. 여기서 위에서 설명드린 자바스크립트가 함수의 동등성을 판단하는 방식 때문에 예상치 못한 무한 루프에 발생하게된다. fetchUser
는 함수이기때문에, userId 값이 바뀌든 말든 컴포넌트가 랜더링될 때마다 새로운 참조값으로 변경이 된다. 그러면 useEffect()
함수가 호출되어 user
상태값이 바뀌고 그러면 다시 랜더링이 되고 그럼 또다시 useEffect
가 호출되는 악순환이 반복된다.
import React, { 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 React, {useState, useEffect} from 'react';
function Profile({userId}){
const [user, setUser] = useState(null);
const fetchUser = useCallback(
()=> fetch(`http://your-api.com/users/${userId}`)
.then((response) => response.json())
.then(({user}) => user),
[userId];
)
useEffect(() => {
fetchUser().then((user)=> setUser(user));
}, [fetchUser]);
//...
}
userCallback()
hook함수는 자식 컴포넌트의 랜더링의 불필요한 랜더링을 줄이기 위해서 React.memo()
함수와도 사용할수 있다. props에만 영향을 받는다.
예로 방이름(room), 조명 켜짐 여부(on), 조명 제어 함수(toggle)를 prop으로 Light 컴포넌트를 작성한다.
import React from 'react'
function Light({room, on, toggle}){
console.log({room, on})
return(
<button onClick={toggle}>
{room} {on ? "🌝" : "🌚"}
</button>
)
}
이 함수를 React.memo()함수로 컴포넌트로 감싼다. memo
로 감싸주게 되면 해당 컴포넌트 함수는 props값이 변경되지 않는 한 다시 호출되지 않는다.
Light = React.memo(Light)
3개의 방의 스위치를 중앙 제어하는 SmartHome
컴포넌트를 작성한다.
import React, { 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
컴포넌트 함수가 호출이 되는 것이 console에 찍힌다.
{room:'침실', on:true}
{room:'주방', on:false}
{room:'욕실', on:false}
React.memo를 사용하였음에도 불구하고 모든 방에대한 Light가 컴포넌트호출이 되는 이유는 toggleMaster()
, toggleKitchen()
, toggleBath()
함수의 참조값이 SmartHome
컴포넌트가 랜더링될 때마다 모두 바뀌어버리기 때문이다.
이럴때 해결할수있는 방법이 useCallback()
을 사용하는것이다. useCallback()
으로 감싸고 두번째 인자로 각함수가 의존하고있는 상태를 배열로 넘긴다.
import React, {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} />
</>
);
}
console에 값을 찍어보면 {room: "침실", on: true}
만이 찍히는것을 확인할수있다.