1.4.4 주의할 점
스코프 주의
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i)
}, i * 1000)
}
위 코드를 실행하면 0, 1, 2, 3, 4초 뒤에 5만 출력됨. i
가 전역 변수로 작동하기 때문. 자바스크립트는 기본적으로 함수 레벨 스코프를 따르고 있기 때문에 var
는 for
문의 존재와 상관없이 해당 구문이 선언된 함수 레벨 스코프를 바라보고 있으므로 함수 내부 실행이 아니라면 전역 스코프에 var i
가 등록돼 있을 것. for
문을 다 순회한 이후, 태스크 큐에 있는 setTimeout
을 실행하려고 했을 때, 이미 전역 레벨에 있는 i
는 5로 업데이트가 완료돼 있음.
그럼 0, 1, 2, 3, 4초 뒤에 각 0, 1, 2, 3, 4를 출력하고 싶다면?
let 사용
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i)
}, i * 1000)
}
let
은 기본적으로 블록 레벨 스코프를 가지게 되므로 let i
가 for
문을 순회하면서 각각의 스코프를 갖게 됨. 이는 setTimeout
이 실행되는 시점에도 유효해서 각 콜백이 의도한 i
값을 바라보게 할 수 있음
클로저 제대로 활용
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
을 가지게 되므로 올바르게 실행할 수 있음.
클로저의 비용
클로저는 생성될 때마다 그 선언적 환경을 기억해야 하므로 추가로 비용이 발생함. 아래 두 함수는 엄청나게 긴 작업(길이가 천만인 배열)을 동일하게 처리함. 클로저 유무에 따라 자바스크립트 코드에 어떤 차이가 있는지 알아보자.
// 일반적인 함수
const aButton = document.getElementById('a')
function heavyJob() {
const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1)
console.log(longArr.length)
}
aButton.addEventListener('click', heavyJob)
// 클로저라면??
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()
})
일반적인 함수와 클로저를 사용한 함수가 실제로 어떤 차이가 있는지 크롬 개발자 도구에서 직접 확인해 볼 수 있음.
- 무거운 작업을 일반적인 함수로 처리했을 때 메모리에 미치는 영향: 메모리의 전체 크기도 작고, 실행 전후로도 큰 차이가 없음.
- 클로저로 실행했을 때의 메모리 상태: 긴 배열을 어디에 사용하는지 상관없이 일단 해당 내용을 기억해 둬야 하기 때문에 메모리에 큰 배열이 올라가 있음.
클로저를 활용하는 쪽이 압도적으로 부정적인 영향을 미침. 클로저 heavyJobWithClosure()
로 분리해 실행하고, 이를 onClick
에서 실행하는 방식인데 이미 스크립트를 실행하는 시점부터 아주 큰 배열을 메모리에 올려두고 시작함(약 40MB). 클로저의 기본 원리에 따라, 클로저가 선언된 순간 내부 함수는 외부 함수의 선언적인 환경을 기억하고 있어야 하므로 이를 어디에서 사용하는지 여부에 관계없이 저장해둠. 실제로는 onClick
내부에서만 사용하고 있지만 이를 알 수 있는 방법이 없기 떄문에 긴 배열을 저장해 두고 있는 모습. 반면 일반 함수의 경우에는 클릭 시 스크립트 실행이 조금 길지만 클릭과 동시에 선언, 그리고 길이를 구하는 작업이 모두 스코프 내부에서 끝났기 때문에 메모리 용량에 영향을 미치지 않음.
클로저의 개념, 즉 외부 함수를 기억하고 이를 내부 함수에서 가져다 쓰는 메커니즘은 성능에 영향을 미침. 클로저에 꼭 필요한 작업만 남겨두지 않는다면 메모리를 불필요하게 잡아먹는 결과를 야기할 수 있고, 마찬가지로 클로저 사용을 적절한 스코프로 가둬두지 않는다면 성능에 악영향을 미침.
'Modern React Deep Dive > 01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트' 카테고리의 다른 글
1.5.2 이벤트 루프란? (1) | 2024.05.14 |
---|---|
1.5.1 싱글 스레드 자바스크립트 (0) | 2024.05.14 |
1.4.3 클로저의 활용 (1) | 2024.04.09 |
1.4.2 변수의 유효 범위, 스코프 (1) | 2024.04.09 |
1.4.1 클로저의 정의 (0) | 2024.04.09 |