리액트를 사용하면서 useState
, useEffect
, useRef
등은 써보았지만 아직 제대로 사용해보지 못한 훅들이 많았다...
그래서 이 참에 한 번 궁금했던 리액트의 hook
몇 가지에 대해 알아보고자 한다.
그 첫번째는 바로 useCallback
이 되시겠다!
컴포넌가 처음 렌더링 될 때 내부에 선언된 함수를 새로 생성한다. 그리고 컴포넌트가 리렌더링되면 다시 새로운 함수로 생성한다. 이런 특성으로 인한 성능 문제가 발생할 수 있다.
useCallback
: 리렌더링 간에 함수 정의를 캐시할 수 있는 hook
쉽게 얘기하면 useCallback
의 첫번째 인자로 들어온 함수를 기억하여 두번째 인자로 넘어온 배열 요소의 값이 변경되기 전까지 재사용한다.
즉, 두번째 인자 값이 변하지 않으면 함수를 새로 생성하지 않고 기억 된 함수를 재사용한다는 의미이다.
useCallback(fn, dependencies)
두 가지 전달 요소fn
: 캐시하려는 함수 정의 값이다.dependencies
: 첫번째 인자의 함수(fn)
가 의존해야하는 요소의 배열 useEffect
의 종속성과 유사컴포넌트 렌더링 시 함수를 새로 생선언하는 것은 성능 상 큰 문제가 되지 않기 때문에 단순히 컴포넌트 내에서 함수를 새로 생성하지 않기 위해 useCallback
을 사용하는 것은 의미가 없다.
useCallback()
을 언제 사용해야하는지 이해하려면 함수 간의 동등함이 어떻게 결정되는지 알 필요가 있다.
const func1 = () => console.log("func");
const func2 = () => console.log("func");
console.log(func1 === func2); // false
자바스크립트에서 함수도 객체로 취급 되기 떄문에 메모리 주소에 의한 참조 비교가 일어난다.
이런 특성은 리액트 컴포넌트 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제가 발생할 수 있다.
function MyInfo({userId}) {
const [userInfo, setUserInfo] = useState(null);
const getInfo = () =>
fetch(`https://getinfo-api.com/users/${userId}`)
.then((res) => res.json())
.then(({user}) => info);
useEffect(()=> {
getInfo().then((info) => setUserInfo(info));
}, [getInfo]);
}
위의 코드를 예를 들어보면 useEffect
는 getInfo
함수가 변경될 때마다 호출 된다. getInfo
는 함수이기에 userId
값에 상관없이 컴포넌트가 렌더링 될 때 마다 함수가 새로 생성되어 참조 값이 변경된다. 그러면 useEffect
함수가 호출되어 info
상태값이 바뀌고 다시 렌더링되고 또 다시 useEffect
함수가 호출되는 악순환이 반복된다.
function MyInfo({userId}) {
const [userInfo, setUserInfo] = useState(null);
const getInfo = useCallback (
() =>
fetch(`https://getinfo-api.com/users/${userId}`)
.then((res) => res.json())
.then(({user}) => info),
[userId]);
useEffect(()=> {
getInfo().then((info) => setUserInfo(info));
}, [getInfo]);
}
위와 같은 문제에서 useCallback
을 이용하여 함수를 래핑하게 되면 getInfo
함수의 참조 값을 동일하게 유지한다. 따라서 userId
가 변경되지 않는 한 useEffect
는 재호출되지 않는다.
useCallBack()
hook 함수는 자식 컴포넌트 렌더링의 불필요한 렌더링을 막기 위해 memo()
hook과 같이 사용할 수 있다.
예를 들면,
export const SmartLight = memo(function SmartLight({toggle, room, on}) {
console.log({room, on});
return (
<button onClick={toggle}>
{room} {on ? "💡" : "⬛"}
</button>
);
});
memo()
함수로 컴포넌트를 감싸게 되면 해당 컴포넌트 함수는 props 값이 변경되지 않는 한 재호출되지 않습니다.
SmartLight
컴포넌트를 적용시킬 컴포넌트 작성
function SmartHome() {
const [bathOn, setBathOn] = useState(false);
const [homeOfficeOn, setHomeOfficeOn] = useState(false);
const toggleBath = () => setBathOn(!bathOn);
const toggleHomeOffice = () => setHomeOfficeOn(!homeOfficeOn);
return (
<div>
<SmartLight room="작업실" on={homeOfficeOn} toggle={toggleHomeOfficeOn}/>
<SmartLight room="안방" on={bathOn} toggle={toggleBathOn} />
)
</div>
}
이 컴포넌트를 이용하여 안방의 불을 키면 다른 모든 방에 대한 컴포넌트 함수가 호출 되는 것이 콘솔에서 확인해볼 수 있다.
{room: "작업실", on: true}
{room: "안방", on: false}
조명을 키거나 끄는 방에 대한 SmartLight
컴포넌트 함수만이 아닌 다른 방의 컴포넌트도 같이 호출되는 이유는 toggleBath()
, toggleHomeOffice()
함수 참조값이 SmartHome
컴포넌트가 렌더링 될 때마다 새로 생성되어 바뀌어버리기 때문이다.
이 문제를 해결하려면 모든 toggle
함수에 useCallback
hook 함수로 감싸주어야 한다.
const toggleBath = useCallback (()=>
setBathOn(!bathOn) ,[bathOn]);
const toggleHomeOffice = useCallback(()=>
setHomeOfficeOn(!homeOfficeOn), [homeOfficeOn]);
이제 다시 toggle
을 해보면 해당 방에 대해서만 컴포넌트의 호출이 일어나는 것을 볼 수 있다.
{room: "작업실", on: true}
경우에 따라 메모화된 콜백의 이전 상태를 기반으로 상태를 업데이트해야 할 수 있다.
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((todo) => {
const newTodo = { index: nextIndex++, todo };
setTodos([...todos, newTodo]);
}, [todos]);
}
위 함수는 todos
를 종속성으로 지정하여 다음 할 일을 계산하는데 일반적으로 메모화된 함수는 가능한 한 적은 종속성을 갖는 것이 좋다.
다음 상태를 계산하기 위해서 일부 상태를 읽는 경우 대신 업데이트 함수를 전달하여 해당 종속성을 제거할 수 있다.
const handleAddTodo = useCallback((todo) => {
const newTodo = { index: nextIndex++, todo };
setTodos(todos => [...todos, newTodo]);
}, []);
useEffect
내부에서 함수를 호출하고자 할 때
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // roomId가 변경 될 때만 변경
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // createOptions가 변경될 때만 변경
// ...
이렇게 작성하면 동일한 createOptions
인 경우 리렌더링 시에도 동일한 기능을 수행한다.
그러나 함수 자체를 종속성으로 두지 않는 것이 훨씬 좋은 방법이면서 코드를 훨씬 간결하게 작성할 수 있다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // roomId가 변경될 때만 변경
사용자 지정 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,
};
}
hook 사용 시 필요할 때 코드를 최적화 할 수 있다는 이점이 있다.
React hook의 useCallback()
에 대해 알아보았다!
이 함수를 어떤 상황에서 사용해야 하는지, 어떤 효과가 있는지도 알 수 있는 시간이었다.
성능 최적화에 적합한 hook이지만 무조건 적으로 사용하게 되면 오히려 코드가 복잡해지고, 유지보수가 어려워지는 역효과가 나타날 수 있다고하니 주의해서 사용해야 겠다.
다음시간에는 useCallback
과 같이 사용 된useMemo
에 대해서 알아봐야겠다!
.
.
.
.
.
.
참고 사이트
[daleseo] - useCallback 사용법
React 공식 문서 - useCallback