클로저란 함수에서 반환된 내부 함수가 스코프를 기억하여 해당 스코프 외부에서 호출되어도 해당 스코프에 접근할 수 있는 함수를 의미합니다.
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를 사용하는 것이 좋습니다.