💡 JS 기술 면접 단골질문인 "클로저"... 항상 면접 준비할 때만 바짝 공부하고 제대로 이해하지 못하는 기분이라 이참에 벨로그에 확실하게 정리해서 이해해 보고자 한다! "모던 리액트 Deep Dive" 서적을 많이 참고했음을 미리 밝힙니다 ☺️
함수와 함수가 선언된 어휘적 환경(Lexical Scope)의 조합
변수가 코드 내부에서 어디서 선언됐는지!
function add() {
conat a = 10
function innerAdd() {
const b = 20
console.log(a + b)
}
innerAdd() // 30
}
add()
a
변수의 유효 범위는 add 전체
이고, b
변수의 유효 범위는 innerAdd의 전체
innerAdd
는 add
내부에서 선언돼 있어 a
사용 가능→ "클로저"는 이러한 어휘적 환경을 조합해서 코딩하는 기법!
변수의 유효 범위!
전역 레벨에서 선언하는 것
window
,global
var global = 'gloabl scope'
function hello() {
console.log(global)
}
console.log(global) // global scope
hello() // global scope
console.log(global === window.global) // true
전역 스코프
와 hello 스코프
모두에서 global
변수에 접근 가능{}
블록이 스코프 범위를 결정하지 않는다!if (true) {
var global = 'global scope'
}
console.log(global) // 'global scope'
console.log(global === window.global) // true
→ var global
가 분명 {}
내부에 선언되어 있는데, {}
밖에서도 접근 가능!!
function hello() {
var local = 'local variable'
console.log(local) // local variable
}
hello()
console.log(local) // Uncaught ReferenceError: local is not defined
→ 함수 레벨 스코프이기 때문에, 이러한 경우에는 ReferenceError
만약 스코프가 중첩되어 있다면?
var x = 10
function foo() {
var x = 100
console.log(x) // 100
function bar() {
var x = 1000
console.log(x) // 1000
}
bar()
}
console.log(x) // 10
foo()
10
, 100
, 1000
순으로 출력됨function outerFunction() {
var x = 'hello'
function innerFunction() {
console.log(x)
}
return innerFunction
}
const innerFunction = outerFunction()
innerFunction() // "hello"
outerFunction
은 innerFunction
을 return하며 실행 종료됨x
라는 변수가 존재하지 않지만,outerFunction
에는 x
라는 변수가 존재하며 접근할 수도 있다!→ 같은 환경에서 선언 · 반환된 innerFunction
에서는 x
라는 변수가 존재하던 환경을 기억하기 때문에, 정상적으로 "hello"가 출력되는 것!!
var counter = 0
function handleClick() {
counter++
}
counter
변수의 문제점→ 전역 레벨에 선언되어 있어서 누구나 수정 가능
(window.counter
를 활용하면 쉽게 해당 변수에 접근 가능)
React가 관리하는 내부 상태 값은, React가 별도로 관리하는 클로저 내부에서만 접근 가능!
위 코드를 클로저를 활용한 코드로 변경해 보자! ↓↓↓
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()) // counter에 접근! 2
↑ 사이트에서 실행시켜 본 모습
counter
변수를 직접적으로 노출하지 않음으로써 사용자가 직접 수정하는 것을 막음counter
변수의 업데이트를 increase
와 decrease
로 제한해 무분별하게 변경되는 것을 막음useState
의 변수를 저장해 두고, useState
의 변수 접근 및 수정을 클로저 내부에서 확인 가능해, 값이 변하면 렌더링 함수를 호출하는 등의 작업이 이루어질 것!function Component() {
const [state, setState] = useState()
function handleClick() {
// useState 호출은 위에서 끝났지만,
// setState는 계속 내부의 최신값(prev)을 알고 있다.
// 이는 클로저를 활용했기 때문에 가능하다.
setState((prev) => prev + 1)
}
// ...
}
useState
)가 반환한 내부 함수(setState
)는 외부 함수(useState
)의 호출이 끝났음에도 자신이 선언된 외부 함수가 선언된 환경(state
가 저장되어 있는 어딘가)를 기억하기 때문에,state
값 사용 가능!useState
내부에서 활용됐기 때문!for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
console.log
로 0, 1, 2, 3, 4를 차례대로 출력하는 것→ i
가 전역 변수로 작동하기 때문에!
var
는 for
문의 존재와 상관없이 해당 구문이 선언된 함수 레벨 스코프를 바라보고 있으므로, 함수 내부 실행이 아니라면 전역 스코프에 var i
가 등록돼 있을 것for
문을 다 순회한 후, 태스크 큐에 있는 setTimeout
을 실행하려고 했을 때, 이미 전역 레벨에 있는 i
는 5로 업데이트 되어 있음1. let
으로 수정하는 법
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
let
으로 수정2. 클로저를 활용해 수정하는 법
for (var i = 0; i < 5; i++) {
setTimeout(
(function (sec) {
return function () {
console.log(sec)
}
})(i),
i * 1000,
)
}
for
문 내부에 즉시 실행 익명 함수 선언i
를 인수로 받는데,sec
라고 하는 인수에 저장해 두었다가 setTimeout
의 콜백 함수로 넘기게 됨setTimeout
의 콜백 함수가 바라보는 클로저는 즉시 실행 익명 함수가 되는데,for
문마다 생성 · 실행되기를 반복sec
를 가지게 되므로, 올바르게 실행된다!클로저를 사용할 때 주의할 점
클로저를 사용하는 데는 비용이 든다! (성능 이슈)
1. 긴 작업을 일반적인 함수로 그냥 처리
// 일반적인 함수
const aButton = document.getElementById('a')
function heavyJob() {
const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1)
console.log(longArr.length)
}
aButton.addEventListener('click', heavyJob)
2. 긴 작업을 클로저로 처리
// 클로저
function heavyJobWithClosure() {
const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1)
return function () {
console.log(longArr.length)
}
}
const innerFunc = heavyJobWithClosure()
bButton.addEventListener('click', function () {
innerFunc()
})
→ 개발자 도구에서 살펴보면, 클로저를 활용하는 쪽이 압도적으로 부정적인 영향을 미친다는 것을 확인할 수 있음