모던 리액트 Deep Dive - Chapter 01-4. 클로저
작성일: 2025년 11월 25일 오전 08:58(마지막 수정: 2025년 12월 3일 오후 01:53)
조회수: 84
리액트에서 '클로저'가 중요한 이유?
'클래스 컴포넌트'에 대한 이해는 클래스, 프로토타입, this에 달려있다면,
함수 컴포넌트에 대한 이해는 클로저에 달려있다.(구조와 작동방식, 훅, 의존성 배열 등)
1. 클로저의 정의
- 함수와 함수가 선언된 렉시컬(어휘적) 환경의 조합
- 클로저는 '선언된 어휘적 환경'을 조합해 코딩하는 기법
2. 스코프: 전역 스코프와 함수 레벨 스코프
-
전역 레벨 선언 -> 전역 스코프(window, global)
-
자바스크립트는 기본적으로 '함수 레벨 스코프'를 따름(
var-> 함수 레벨 스코프)- 이를 활용해 함수 내부적으로 상태를 관리할 수 있다.
- 상태를 사용자가 직접 수정하는 것을 방지
- 상태를 변경하는 방식도 개발자가 원하는 방식만 노출
- React에서
useState가 이런 식의 클로저 활용이 이루어짐
js// counter 변수를 직접 노출하지 않음으로써 사용자가 직접 수정하는 것을 방지 // 변경 방식도 개발자가 원하는 두 가지 방식(increase, decrease)만 노출시킴 function Counter() { var counter = 0; return { increase: function () { return ++counter; }, decrease: function () { return --counter; }, counter: function () { console.log("counter에 접근"); return counter; }, }; } var c = Counter(); console.log(c.increase()); // 1 console.log(c.increase()); // 2 console.log(c.increase()); // 3 console.log(c.decrease()); // 2 console.log(c.counter()); // 2 - 이를 활용해 함수 내부적으로 상태를 관리할 수 있다.
2-1. 전역 실행 컨텍스트

2-2. Counter 함수 실행 컨텍스트

2-3. Counter 함수 실행 후 클로저

2-4. increase 함수 실행 컨텍스트

2-5. increase 함수 실행 후 클로저

3. React에서 javascript 클로저의 활용 예시: useState
다른 훅들도 많지만, 대표적으로 useState가 있다. (그 외 모든 훅을 파헤치는 것은 3장에서 다룬다)
jsxfunction Component() { const [state, setState] = useState(); // 함수 호출로 클로저 생성 function handleClick() { // useState 함수 호출은 끝났어도 클로저에 의해 최신값 prev를 알고 있음 setState((prev) => prev + 1); } // ... }
4. 클로저 사용 시 주의사항
클로저는 굉장히 어렵고 다루기 쉽지 않은 개념이므로, 클로저를 직접 사용할 때는 주의를 요한다.
4-1. 주의사항 1: var과 클로저
다음 코드의 동작을 예상해보자.
jsfor (var i = 0; i < 5; i++) { setTimeout(function () { console.log(i); }, i * 1000); }
- 예상: 0초 뒤에 0, 1초 뒤 1... 4초 뒤에 4를 출력
- 실제: 0초 뒤에 5, 1초 뒤에 5.. 5초 뒤에 5(n초는 정확하지 않을 수 있음)
- var은 함수 레벨 스코프를 가지므로, for문 안에 작성되어 있지만 전역 스코프를 가진다.
- setTimeout 호출 시 두 번째 인자인
i * 1000은 호출 시점에 연산되어 처리되지만, 첫 번째 인자인 콜백 함수function () {console.log(i);}는 아직 호출되지 않았다. 콜백 함수는 Web API로 이동하고 이 상태로 setTimeout의 호출을 완료된다. 그 결과 i는 5가 된다. - Web API에서 타이머 (대략)
i * 1000가 끝나면 해당된 콜백 함수가 태스크 큐로 이동했다가, 콜 스택이 비면 콜 스택으로 이동 후 호출된다. - 호출된 콜백함수는 렉시컬 환경을 기억하는데, 먼저 함수 내부에서는 i를 찾을 수 없으니 for문 block 스코프에서 i를 찾는데, 없으므로 마지막으로 전역 변수
i는 5이므로 5를 출력한다.
만약 스코프 내부에서 클로저를 제대로 활용해 i를 내부적으로 관리하고 싶었다면 잘못된 활용이라 볼 수 있다.
- 해결책 1 -
var가 아닌let으로 for문 내에 블록 레벨 스코프의 i를 선언한다: 이러면 각 반복문 마다 별개(각각 다른 렉시컬 환경)의 i를 가진다.jsfor (let i = 0; i < 5; i++) { setTimeout(function () { console.log(i); }, i * 1000); } - 해결책 2 - 즉시 실행 함수로 선언: 위 함수를 return하는 즉시 실행 함수 형태를 상위에서 선언한다면, 매 반복마다 상위 함수(즉시 실행 함수)가 호출되며 그 순간 함수 인수(i)는 함수 레벨 스코프 에서 저장된 클로저를 생성한다. 이로 인해 내부 함수 호출 시에는 렉시컬 환경을 기억하여 상위 함수 i인 0, 1, 2, 3, 4를 올바르게 참조하게 된다.
js
for (var i = 0; i < 5; i++) { setTimeout( (function (i) { return function () { console.log(i); }; })(i), i * 1000 ); }
4-1. 주의사항 2: 클로저는 비용이 든다.
클로저는 생성될 때마다 그 렉시컬 환경을 기억해야 한다(=메모리 차지). 이는 곧 추가적인 비용이 발생함을 의미한다.
그러므로 꼭 필요한 것이 아니라면 클로저 사용은 지양하는 것이 좋다. 메모리를 불필요하게 잡아먹는 결과를 야기할 수 있기 때문이다.
js// 일반 함수의 적절한 활용: 함수 실행 전후의 큰 메모리 크기 차이가 적다.(약 1.3MB) const btn = document.getElementById("a"); function heavyJob() { const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1); console.log(longArr.length); } btn.addEventListener("click".heavyJob); // 클로저의 과도한 사용: 어디 사용하는 지와 상관없이 일단 메모리에 큰 배열을 올려둔다.(약 40.1MB) const btn = document.getElementById("a"); function heavyJobWithClosure() { const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1); return function () { console.log(longArr.length); }; } const innerFunction = heavyJobWithClosure(); btn.addEventListener("click".innerFunction);
2개의 댓글
김영인
4개월 전클로저는 참 어렵죠...! 포기하지 않고 힘내보자!
김영인
4개월 전그래그래 힘내보자!