안녕하세요.
지난 포스팅에 이어서 react rendering 최적화를 위한 hook, useCallback에 대해 살펴보도록 하겠습니다:)
useCallback은 기본적으로 useMemo와 매우 유사합니다.
일단 App.js와 List 컴포넌트를 다음과 같이 만들어보겠습니다.
//App.js
import React, { useState } from "react"
import List from "./List"
function App() {
const [number, setNumber] = useState(1)
const [dark, setDark] = useState(false)
const getItems = () => {
return [number, number + 1, number + 2]
}
const theme = {
backgroundColor: dark ? "#333" : "#fff",
color: dark ? "#fff" : "#333",
}
return (
<div style={theme}>
<input
type="number"
value={number}
onChange={e => setNumber(parseInt(e.target.value))}
/>
<button onClick={() => setDark(prevDark => !prevDark)}>테마 변경</button>
<List getItems={getItems} />
</div>
)
}
export default App
//List.js
import React, { useEffect, useState } from "react"
export default function List({ getItems }) {
const [items, setItems] = useState([])
useEffect(() => {
setItems(getItems())
console.log("숫자가 변동되었습니다.")
}, [getItems])
return items.map((item, index) => <div key={index}>{item}</div>)
}
다음과 같은 화면이 보이시나요?
useMemo 때의 예제코드와 얼추 비슷합니다. input 창을 통해 숫자를 입력할 경우 페이지의 숫자 항목들이 변경되고, theme 토글 버튼을 누를 경우 css 적 요소가 변하도록 예제코드를 작성했습니다.
이제 숫자를 변경해보면 console 창에 '숫자가 변경되었습니다.' 라는 메시지를 보실 수 있을겁니다.
그러나 ✋! 테마 토글 버튼을 누를 경우에도 해당 메시지가 발생한다는 점 확인하셨나요?
우리는 테마만 바꿀 뿐인데 getItems함수가 실행되다니...
...
이것은 제가 저번 포스팅에서도 말했던 점이 야기하는 현상입니다.
부모에게서 받는 getItems라는 함수 props가 부모 컴포넌트가 리렌더링 되면서 변경된 props로 인식되기에 발생하는 현상이죠. 그렇게 되기에 부모 컴포넌트의 number값이 새로 설정되며 해당 함수가 계속 반복되어서 실행되는 것이죠.
우리는 App.js파일에 useCallback을 추가하여 이를 고칠 수 있습니다!
number에 대한 의존성을 가지는 useCallback으로 getItems 함수를 매핑시켜주면 될 것 같습니다.
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
해당코드는 react 공홈에서 제공해주는 useCallback 예시코드입니다!
그대로 적용시켜주어보겠습니다.
//App.js
import React, { useState, useCallback } from "react"
import List from "./List"
function App() {
const [number, setNumber] = useState(1)
const [dark, setDark] = useState(false)
const getItems = useCallback(() => {
return [number, number + 1, number + 2]
}, [number])
const theme = {
backgroundColor: dark ? "#333" : "#fff",
color: dark ? "#fff" : "#333",
}
return (
<div style={theme}>
<input
type="number"
value={number}
onChange={e => setNumber(parseInt(e.target.value))}
/>
<button onClick={() => setDark(prevDark => !prevDark)}>테마 변경</button>
<List getItems={getItems} />
</div>
)
}
export default App
이렇게 App.js파일을 변경 시켜준 후 한번 테스팅을 해보시기 바랍니다.
어떤가요?? 테마를 변경할 때에는 console이 찍히지 않죠?
자 그럼 여기서 여러분들에게 의문점이 생길 것입니다.
엥? useMemo랑 아주 똑같은데요?
네 완전 비슷하죠? 그렇지만 분명한 차이점이 존재한답니다.🤗
이제부터 useMemo와 useCallback이 어떻게 다르고, 각각 어떤 상황에서 사용되는지 살펴보도록 하겠습니다:)
App.js파일의 getItems함수에서 우리가 특정한 값을 매개변수로 받아 요소 하나하나에 더해주는 식의 코드를 짠다고 가정해보겠습니다.
그렇다면 이런식으로 변경이 가능하겠죠?
// App.js
const getItems = useCallback(
increaseValue => {
return [
number + increaseValue,
number + 1 + increaseValue,
number + 2 + increaseValue,
]
},
[number]
)
그 후에 List.js에서 getItems에 increaseValue를 명시해줍니다.
저는 5를 넣었습니다.
// List.js
setItems(getItems(5))
자 이제 페이지를 살펴보겠습니다.
어떤가요? 함수의 매개변수가 부모와 자식간에 잘 오간다는 것이 보이죠?
우리가 만약 useMemo를 사용했다면 이러한 것이 가능했을까요??
가능하긴 합니다. 이런식으로 만들어준다면요.
useMemo를 사용
// App.js
const getItems = useMemo(
() => increaseValue => {
return [
number + increaseValue,
number + 1 + increaseValue,
number + 2 + increaseValue,
]
},
[number]
)
사실상 useCallback(fn, deps) 는 useMemo(() => fn, deps)와 같습니다. 그렇기에 해당 코드와 같이 useMemo를 적용시킬 수 있죠.
굳이 이렇게 선언해야 하는 이유가 뭘까요?
useMemo는 함수를 반환하지 않고, 함수의 값만 memoization해서 반환하기 때문입니다!
그와 다르게 useCallback은 함수 자체를 memoization 해서 반환하죠.
이것이 바로 useMemo와 useCallback의 핵심적인 차이점입니다. 🤗
자식 컴포넌트에서 특정한 props 값들의 변화를 최적화시키고 싶을때는 useMemo를, 부모 컴포넌트에서 계산량이 많은 props함수를 자식 컴포넌트에게 넘겨줄 때는 useCallback을 사용하는 것이 맞다고 저는 생각합니다.
제가 저번 포스팅과 이번 포스팅에서 예제로 제시했던 코드들에서는 사실 useMemo와 useCallback을 사용하지 않아도 됩니다. 음.. 사용하지 않는게 더 낫다고 해야하나.. 애매하네요.
그 이유는 useMemo와 useCallback은 저번 포스팅에서 언급했다시피 expensive한 연산과정에서 사용되어야 그 효과를 톡톡히 볼 수 있기 때문입니다.
✋효과만 미미할 뿐 더 나은 것 아닌가요?
아닙니다. 왜냐하면 useCallback과 useMemo를 사용할 경우 해당 hook을 호출하고, 그 안에 들어갈 함수를 만들어 넘기고, 의존성 체크를 목적으로 추가적인 비용이 발생하기 때문입니다. 더불어 memoization을 해놓는 다는 것은 결국 그 값을 메모리에 할당해놓고 있다는 뜻이기에 여기서도 추가적인 비용이 발생한다고 볼 수 있습니다.
퍼포먼스 최적화는 컴퓨터의 어떠한 부분에서도 마찬가지겠지만 절대 공짜가 아닙니다. 컴퓨팅 자원, 개발자의 자원 등 어디선가 반드시 소모되는 자원이 있을 것이고, 이에 대해 잘 저울질하여 책임감 있게 최적화를 진행해야 한다 생각합니다. (그리고 react 자체가 성능이 좋기에 이러한 것들이 좀 불필요하게 느껴지기도 합니다..ㅎㅎ)
저는 개인적으로 animation이 작동하는 자식컴포넌트에 대해서는 필수적으로 최적화를 시켜주어야 한다 생각합니다. user interface 관점에서 최적화가 되어있지 않다면 animation의 끊김 현상이 발생할 수 있기 때문이죠.
그 외 누가봐도 정말 한번 실행될때마다 계산 비용이 비싼 함수나 값을 넘겨줄 때에는 최적화를 하는 것이 당연하다고 생각해요.
물론 어디까지나 제 개인적인 견해일 뿐 이게 맞다고 제가 감히 말씀드릴 수는 없어요.😂
여러분들도 한번 useCallback과 useMemo를 적용시켜보며 이러한 고민을 해보는 것은 어떨까요?🙄🙄
이상 김용성이었습니다. 읽어주셔서 감사합니다. ⭐️
감사합니다