개요
클로저는 공식 문서에서 다음과 같이 설명된다.
클로저는 주변 상태(어휘적 환경)에 대한 참조와 함께 묶인(포함된) 함수의 조합입니다. 즉, 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공합니다. JavaScript에서 클로저는 함수 생성 시 함수가 생성될 때마다 생성됩니다.
직관적으로는 다음과 같은 의미를 가진다.
클로저는 함수가 “자기 주변의 변수”를 기억해서, 나중에도 계속 사용할 수 있게 해주는 기능이다.
즉 클로저를 쓰면 다음과 같은 이점을 가질 수 있다.
- 변수를 보호하고 감출 수 있다. (은닉)
- 값을 유지할 수 있다. (상태 유지)
- 콜백, 비동기 함수 등에서 엄청 유용하게 쓰인다.
해당 내용들을 이해하기 위해선 먼저 자바스크립트의 스코프에 대한 이해가 필요하다.
2024.01.03 - [CS] - [JS] 스코프에 대하여
[JS] 스코프에 대하여
개요 Scope 는 범위라는 뜻을 가진다. Javascript에서 scope는 변수에 접근할 수 있는 범위를 뜻한다. 주로 동적 스코프, 정적 스코프(렉시컬 스코프), 블록레벨 스코프의 3가지 스코프가 있다. 동적 스
mk-develop.tistory.com
클로저(Closure)
- 함수 1
function init() {
var name = "Mozilla"; // name은 init에 의해 생성된 지역 변수이다.
function displayName() {
// displayName() 은 내부 함수이며, 클로저다.
console.log(name); // 부모 함수에서 선언된 변수를 사용한다.
}
displayName();
}
init();
- 함수 2
function makeFunc() {
const name = "Mozilla";
function displayName() {
console.log(name);
}
return displayName;
}
const myFunc = makeFunc();
myFunc();
두 함수는 모두 클로저를 사용하는 예제 코드이다.
makeFunc 함수와 init 함수는 모두 const name = "Mozilla"; 라는 변수를 가진다.
내부 함수 displayName() 은 외부 변수 name을 참조한다.
displayName() 함수는 부모 함수의 변수인 name을 기억한다.
즉 외부 함수가 실행이 종료되어도 name 변수에 접근이 가능하다.
이는 자바스크립트가 클로저를 형성하기 때문이다.
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.
이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.
때문에 displayName() 함수는 변수 name에 대한 참조를 유지한다.
1번 함수는 init() 함수를 호출만 하고 끝나기 때문에 재사용이 불가능 하지만 2번 함수의 경우엔 클로저가 myFunc의 실행 환경을 계속해서 기억하고 있기 때문에 재사용이 가능하다.
React에서의 클로저
그렇다면 의문이 생길 수 있다.
상태 관리라면 React에서 useState나 useReducer와 같은 hook을 사용한다.
실제 React와 같은 프레임워크에서 클로저는 어떻게 쓰이는가?
React에서 클로저는 컴포넌트 내부 함수가 옛날 상태(state)나 props를 기억하는 현상 으로 자주 등장한다.
이것이 원인으로 이상한 동작이나 버그 가 생기기도 한다.
useState에서의 클로저
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
alert("Count is: " + count); // ❗ 클로저: 오래된 count 값 기억
}, 1000);
};
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={handleClick}>Show in 1s</button>
</>
);
}
위 코드에서 첫 번째 버튼을 눌러서 아무리 count를 증가시켜도 alert는 0만 출력한다.
왜냐하면 클로저가 함수 작성 당시의 count 값만 기억하기 때문이다.
이는 다음과 같이 해결한다.
setTimeout(() => {
setCount(prev => {
alert("Count is: " + prev);
return prev;
});
}, 1000);
또는 useRef 를 사용하여 현재 값을 따로 관리한다.
useEffect 안에서의 클로저
useEffect(() => {
const id = setInterval(() => {
console.log("count: ", count); // ❗ 여기도 클로저: 오래된 count 참조
}, 1000);
return () => clearInterval(id);
}, []);
위 코드는 컴포넌트가 처음 렌더링 될 때 한번만 수행된다.
하지만 내부 함수는 처음 컴포넌트가 렌더링 됐을 당시의 count 값만 기억한다.
때문에 다음과 같이 작성하여 해결한다.
useEffect(() => {
const id = setInterval(() => {
setCount(prev => {
console.log("count: ", prev);
return prev + 1;
});
}, 1000);
return () => clearInterval(id);
}, []);
종합하여 아래와 같은 방식으로 최신 값을 유지할 수 있다.
function Timer() {
const [time, setTime] = useState(0);
const timeRef = useRef(time); // 상태처럼 최신 값을 유지할 수 있음
useEffect(() => {
timeRef.current = time;
}, [time]);
const showCurrentTime = () => {
setTimeout(() => {
alert("시간: " + timeRef.current); // ✅ 최신 값 출력!
}, 1000);
};
return (
<div>
<p>{time}</p>
<button onClick={() => setTime(time + 1)}>+1</button>
<button onClick={showCurrentTime}>1초 후 시간 보기</button>
</div>
);
}
마무리
React에서 클로저는 버그의 씨앗이자, 강력한 도구이다.
렌더링마다 상태가 새로 생성되기 때문에, 함수가 기억하고 있는 "과거의 상태"가 클로저로 남아 예상치 못한 결과를 초래할 수 있다.
때문에 React에서 렌더링과 상태, 비동기 처리가 얽힌 상황에서 클로저를 잘 이해해야 버그를 피할 수 있다.
또한 클로저를 통하여 캡슐화라는 개념을 구현할 수 있고 정보 은닉과 캡슐화가 가져다주는 이점들을 얻을 수 있다.
본 글은 다음 문헌을 참고하여 작성되었습니다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Closures
클로저 - JavaScript | MDN
클로저는 주변 상태(어휘적 환경)에 대한 참조와 함께 묶인(포함된) 함수의 조합입니다. 즉, 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공합니다. JavaScript에서 클로저는 함수 생
developer.mozilla.org
'CS' 카테고리의 다른 글
Event Interface and Document Object Model Event (2) | 2025.04.09 |
---|---|
HTTP 프로토콜과 HTTP 프로토콜의 진화 과정(0.9~3.0) (0) | 2025.03.28 |
웹 브라우저의 동작 원리와 웹 최적화 (2) | 2025.03.25 |
[JS] 스코프에 대하여 (0) | 2024.01.03 |