[React] useCallback
useCallback 개념설명
useCallback도 메모이제이션기법으로 컴포넌트를 최적화한다.
메모이제이션이란?
자주 사용하는 값을 받기 위한 반복적으로 계산을 해야 된다면
계산한 값을 캐싱해 두고 필요할 때마다 반복적으로 계산하는 것이 아니라 메모리에 꺼내서 재사용하는 최적화 기법이다.
useMemo 간단 설명
useMemo는 자주 쓰이는 값을 메모이제이션 즉 캐싱을 한다.
그 값이 필요할 때마다 다시 계산하는 것이 아니라 useMemo를 통해 캐싱해 둔 값을 메모리에서 꺼내와서 사용한다.
useMemo(()=>{
return value; // value를 메모이제이션을 해준다.
}, [item])
useMemo 인자로 콜백함수를 넣어주면 이 함수가 리턴하는 값을 메모이제이션을 해주는 것이다.
useCallback이란?
useCallback(()=>{
return value;
}, [item]);
인자로 전달해 준 콜백함수 자체를 메모이제이션을 해주는 것이다.
const calculate = useCallback((num)=>{
return num + 1;
}, [item])
만약 calculate라는 함수를 메모이제이션을 해주면 useCallback로 감싸주면 된다.
이 함수가 다시 필요할 때마다 함수를 새로 생성하는게 아니라 필요할때마다 메모리에서 가져와서 재사용하는 것이다.
Javascript의 함수
javascript에서의 함수는 객체의 한 종류이다.
const calculate = (num) => {
return num + 1;
};
calculate라는 변수에 (num) => {...}이라는 함수 객체가 할당되어 있다.
마치 변수에 숫자, 문자, 객체를 넣는 것과 같이 함수도 변수에 할당할 수 있다.
함수형 컴포넌트
function Component(){
const calculate = (num) => {
return num + 1;
};
return <div>{value}</div>
}
리액트에서 함수형 컴포넌트는 함수이다.
<Component/> 🎨 렌더링 → Component 함수 호출 → 모든 내부 변수 초기화
함수형 컴포넌트가 렌더링이 된다는 것은 그 컴포넌트를 나타내는 함수가 다시 호출이 된다는 것이다.
컴포넌트 내부에 있는 모든 변수가 초기화가 된다.
calculate함수를 보면 calculate변수 안에 아래와 같은 함수 객체가 할당이 되어 있는 것이다.
(num) => {
return num + 1;
};
컴포넌트가 랜더링 될 때마다 calculate변수가 초기화되기 때문에 새로 만들어진 함수 객체를 할당받게 된다.
function Component(){
const calculate = useCallback((num)=>{
return num + 1
}, [item]);
return <div>{value}</div>
}
<Component/> 🎨 렌더링 → Component 함수 호출 → Memoize된 함수를 재사용
만약에 calculate함수를 useCallback으로 감싸서 메모이제이션을 해주면
컴포넌트를 다시 렌더링이 되더라도 calculate를 초기화되는 것을 막을 수 있다.
컴포넌트가 제일 처음에 렌더링 될 때만 함수객체를 만들어서 calculate를 초기화시키고
이후의 렌더링이 될 때는 calculate변수가 새로운 함수 객체를 할당받는 게 아니라
이전에 미리 할당받은 함수객체를 계속해서 갖고 있으면서 재사용할 수 있게 되는 것이다.
useCallback의 구조
useCallback(()=>{
return value;
}, [item])
useCallback은 2개의 인자를 받는다.
첫 번째 인자는 메모이제이션해줄 콜백함수, 두번째 인자는 의존성 배열을 받는다.
const calculate = useCallback((num) => {
return num + 1;
}, [item]);
함수를 useCallback으로 감싸면 calculate라는 변수는 메모이제이션된 값을 가지게 된다.
메모이제이션된 함수(calculate)는 의존성 배열 내부에 있는 값(여기서는 item)이 바뀌지 않는 이상 다시 초기화되지 않는다.
만약 의존성 배열에 명시한 값이 변경이 돼야 calculate는 새로 만들어진 함수 객체로 초기화되는 것이다.
예제 1
const UseCallback = () => {
const [number, setNumber] = useState(0);
const someFunction = () => {
console.log(`someFunc: number: ${number}`);
};
useEffect(() => {
console.log("sumeFunction이 변경되었습니다.");
}, [someFunction]);
useEffect(() => {
console.log("sumeFunction이 변경되었습니다.");
}, [someFunction]);
return (
<div>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<br />
<button onClick={someFunction}>Call someFUnc</button>
</div>
);
};
export default App;
Call someFunc를 클릭할 때마다 useEffect실행된다.
someFunction은 그대로인데 useEffect가 불리는 걸까?
이유. 리액트 컴포넌트에서 state를 변경할 때마다 컴포넌트가 렌더링이 된다.
함수형 컴포넌트를 사용하고 있으니 렌더링이 된다는 것은 함수형 컴포넌트가 다시 호출이 되는 것이다.
자바스크립트에서 함수를 호출하면 함수 내부에 정의가 되어 있는 변수들을 다시 초기화가 된다.
sumFunction도 하나의 변수인데 아래와 같은 함수 객체를 가지고 있다.
() => {
console.log(`someFunc: number: ${number}`);
};
함수도 일종의 객체이기 때문에 변수에 할당할 수 있다.
자바스크립트에서 객체를 변수에 할당할 때 객체가 변수 안에 바로 들어가지 않는다.
객체라는 것은 사이즈가 크기 때문에 어떤 다른 메모리 공간에 저장이 되고 해당 메모리 공간의 주소가 변수에 저장이 된다.
그래서 sumFunction이라는 변수는 메모리 공간 안에 있는 객체의 주소를 참조하게 되는 것이다.
=> 함수도 객체이기 때문에 sumfunction이라는 변수 안에는 함수 객체에 들어있는 주소가 들어있는 거다.
number state 변경 -> 컴포넌트 렌더링 -> someFunction변수 초기화 -> someFunction의 함수 객체가 새로 만들어 짐 ->
number state가 변경되어 컴포넌트가 렌더링이 되면
someFunction의 함수객체가 다시 새로 생성이 돼서 또 다른 공간 안에 저장하게 된다.
그렇게 되면 someFunction변수 안에는 이전과 다른 메모리 주소가 들어가게 된다.
그렇기 때문에 useEffect에서는 이전 렌더링과 다음 렌더링 때의 someFunction안에 들어있는 주소값을 비교했을 때 다르다고 인식하는 것이다.
someFunction변수는 함수 객체가 들어있는 메모리의 주소를 가지고 있고 컴포넌트가 렌더링이 되어 someFunction이 초기화되면
함수 객체가 새로 만들어져 또 다른 메모리 공간에 저장하게 되기 때문에 새로 만들어진 주소가 someFunction안에 들어가게 된다.
useEffect입장에서는 주소값이 바뀌었으니 콜백함수를 호출해 주는 것이다.
그래서 값을 증가시키면 useEffect가 호출되는 것이다.
사실 위와 같은 코드를 작성하면 주의 문구가 뜨는데 번역기로 돌리면 아래와 같이 나오는데 useCallback()을 사용하라고 나온다.
someFunction' 함수는 모든 렌더에서 useEffect Hook(23번 행)의 의존성을 변경시킨다. 이를 해결하기 위해, 'someFunction'의 정의를 자체 useCallback() Hook으로 랩핑 한다.
useCallback을 사용해서 컴포넌트가 렌더링 되었어도 sumFunction이 바뀌지 않도록 수정
const someFunction = useCallback(() => {
console.log(`someFunc: number: ${number}`);
}, []);
의존성 배열에 아무것도 넣어주지 않았으니 콜백함수는 맨 처음 렌더링 될 때 만들어져서 메모이제이션 된다.
someFunction은 메모이제이션된 함수의 주소가 들어있게 된다.
다음 렌더링부터는 새로 생성해서 할당되는 게 아니라 이미 가지고 있던 메모이제이션된 함수 객체 주소를 계속 가지고 있으면서 재사용하게 된다.
아무리 컴포넌트가 렌더링 된다 해도 useEffect는 불리지 않게 된다.
숫자를 증가시킨 뒤 Call someFunc를 클릭하면 0이 출력된다.
왜냐하면 함수를 메모이제이션 할 당시에 number state는 0이었고 다음 렌더링부터는 이미 메모이제이션된 값을 사용했기 때문에
number state가 바뀌었다 해도 메모이제이션 된 함수 안에 들어있는 number에는 계속해서 0이 들어가게 되는 거다.
메모이제이션된 함수도 업데이트시켜주고 싶으면 의존성 배열 안에 number을 집어넣으면 된다.
const someFunction = useCallback(() => {
console.log(`someFunc: number: ${number}`);
}, [number]);
예제 2 (박스 키우기, 테마)
const UseCallback2 = () => {
const [size, setSize] = useState(100);
const [isDark, setIsDark] = useState(false);
const createBoxStyle = () => {
return {
backgroundColor: "pink",
width: `${size}px`,
height: `${size}px`,
};
};
return (
<div style={{ background: isDark ? "black" : "white" }}>
<input
type="number"
value={size}
onChange={(e) => setSize(e.target.value)}
/>
<button onClick={() => setIsDark(!isDark)}>Change Theme</button>
<Box createBoxStyle={createBoxStyle} />
</div>
);
};
const Box = ({ createBoxStyle }) => {
const [style, setStyle] = useState({});
useEffect(() => {
console.log("박스 키우기 🎨");
setStyle(createBoxStyle());
}, [createBoxStyle]);
return <div style={style}></div>;
};
박스 사이즈 바꿀 때 useEffect가 실행되고(박스 키우기 출력)
Change Theme을 클릭 시에도 useEffect가 실행된다.(박스 키우기 출력)
이유는 isDark state에 변화가 있으니 다시 렌더링이 되고 createBoxStyle이 초기화돼서 그런 것이다.
이러한 현상을 막아주기 위해 createBoxStyle이 size state가 바뀔 때만 초기화해주면 된다.
해결방법은 createBoxStyle의 함수객체를 useCallback으로 감싸면 된다.
const createBoxStyle = useCallback(() => {
return {
backgroundColor: "pink",
width: `${size}px`,
height: `${size}px`,
};
}, [size]);
박스사이즈가 변경되었을 때만 useEffect가 실행이 되고 테마가 바뀔 때는 실행되지 않는다.
※ 이 글은 별코딩 리액트 훅 강의를 보고 정리한 글입니다.