클로저란 함수에서 반환된 내부 함수가 스코프를 기억하여 해당 스코프 외부에서 호출되어도 해당 스코프에 접근할 수 있는 함수를 의미합니다.
function createCounter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const counter = createCounter();
// 클로저를 통해 counter 변수 참조
counter(); // 1
counter(); // 2
만약 중첩된 구조로 클로저가 구현되어 있다면 가장 안쪽에 있는 클로저 함수는 모든 스코프를 참조하고 있기 때문에 모든 변수가 메모리에 존재하며 가비지 수집 대상에서 제외됩니다.
function first() {
const firstValue = 1;
function second() {
const secondValue = 2;
function third() {
console.log(firstValue, secondValue);
}
return third;
}
return second();
}
const fn = first();
fn(); // 1, 2
실제 fn
함수의 스코프 현황
import { useState, useEffect } from "react";
function App({ id }) {
const [count, setCount] = useState(0);
const handleClick = () => {
// 클로저를 이용해 count 변수를 계속 참조
setCount(count + 1);
};
useEffect(() => {
// 클로저를 이용해 전달받은 props id 참조
console.log(id);
}, [id]);
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
useCallback
을 활용하면 handleClick
가 참조하고 있는 counter
값이 변동이 없다면 handleClick
함수는 재생성 하지 않으며 리렌더링을 방지할 수 있습니다.import React, { useState, useCallback } from "react";
function App() {
const [count, setCount] = useState(0);
음
// useCallback을 이용한 최적화: count가 변경되지 않으면 함수를 재생성 하지 않음
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<p>{count}</p>
<ChildComponent onClick={handleClick} />
</div>
);
}
하지만 아래와 같이 클로저를 활용하는 함수가 여러 개 존재할 경우 문제가 발생할 수 있습니다.
handleClick()
함수는 count 변수에 대한 클로저를 생성합니다.handleClick()
함수의 스코프를 확인하면 bigData에 접근하지 않더라도 handleClick()
이 bigData에 대한 참조를 유지합니다.handleButtonClick()
함수가 bigData의 클로저를 생성하고, bigData는 공통 컨텍스트 객체에 의해 참조됩니다.handleClick()
함수는 공통 컨텍스트를 참조하기 때문에 bigData가 가비지 컬렉션되지 않으며, 사용하지 않는 데이터까지 클로저로 가지고 있게 됩니다. 이 참조는 count가 변경되어 handleClick()
함수가 다시 생성될 때까지 유지됩니다.import React from "react";
import { useState, useCallback } from "react";
import "./App.css";
import ChildComponent from "./component/ChildComponent";
const createBigData = () => {
return new Uint8Array(1024 * 1024 * 10);
};
function App() {
const [count, setCount] = useState(0);
const bigData = createBigSata();
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
const handleButtonClick = () => {
console.log(bigData);
};
return (
<div>
<button onClick={handleButtonClick} />
<ChildComponent onClick={handleClick} />
</div>
);
}
export default App;
handleClick
함수의 스코프 현황useCallback
으로 최적화한 여러 개의 이벤트 핸들러 함수가 존재하고 이 함수를 참조하는 다른 함수가 존재할 경우 클로저 체이닝 현상이 발생하며 메모리 누수에 아주 취약한 상태가 됩니다.import React from "react";
import { useState, useCallback } from "react";
import "./App.css";
const createBigData = () => {
return new Uint8Array(1024 * 1024 * 10);
};
function App() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
const bigData = createBigData();
const handleClickA = useCallback(() => {
setCountA(countA + 1);
}, [countA]);
const handleClickB = useCallback(() => {
setCountB(countB + 1);
}, [countB]);
const handleClickBoth = () => {
console.log(bigData);
handleClickA();
handleClickB();
};
return (
<div>
<button onClick={handleClickA}>Increment A</button>
<button onClick={handleClickB}>Increment B</button>
<button onClick={handleClickBoth}>Increment Both</button>
<p>
A: {countA}, B: {countB}
</p>
</div>
);
}
export default App;
handleClickA
함수를 처음 클릭하면 countA
가 변경되므로 handleClickA
함수가 재생성되어 handleClickA-1
함수가 만들어집니다.countB
는 변경되지 않았으므로 handleClickB-0
함수는 재생성되지 않습니다.handleClickB-0
은 여전히 이전의 AppScope-0
에 대한 참조를 유지합니다.handleClickA-1
함수는 AppScope-1
을 참조합니다.handleClickB
함수를 처음 클릭하면 countB
가 변경되므로 handleClickB
함수가 재생성되어 handleClickB-1
함수가 만들어집니다.countA
는 변경되지 않았으므로 handleClickA-1
함수는 재생성되지 않습니다.handleClickA-1
함수는 AppScope-1
을 참조하게 되고 handleClickB-1
함수는 AppScope-2
를 참조하게 되면서 무한으로 클로저 체이닝이 발생하게 되고 bigData 때문에 메모리가 무한으로 늘어나게 됩니다.useCallback
함수들이 클로저 스코프를 통해 서로와 다른 데이터를 참조할 수 있다는 문제가 있습니다. 이러한 클로저들은 useCallback
함수들이 재생성될 때까지 메모리에 유지됩니다. 컴포넌트에 useCallback
함수가 여러 개 있으면 메모리에 무엇이 보관되고 언제 해제되는지 이해하기 매우 어려워지며 해당 문제 겪을 가능성이 높아집니다.useCallback
를 적용한 함수를 참조하게 된다면 클로저 체이닝이 발생할 수 있습니다.useCallback
과 useMemo
는 불필요한 재렌더링을 피하기 위한 훌륭한 도구이지만, 그만큼 비용이 따르기 때문에 렌더링으로 인한 성능 문제가 발생했을 때만 사용해야 합니다.useRef
사용하기useRef
를 사용하여 생명주기를 직접 관리합니다.useCallback
같은 메모이제이션 기술은 예상치 못한 메모리 누수를 일으킬 수 있습니다. useRef
를 사용하는 것이 좋습니다.