본문 바로가기
Modern React Deep Dive/01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트

1.4.4 주의할 점

by Whiimsy 2024. 4. 23.

1.4.4 주의할 점

스코프 주의

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i)
    }, i * 1000)
}

 

위 코드를 실행하면 0, 1, 2, 3, 4초 뒤에 5만 출력됨. i가 전역 변수로 작동하기 때문. 자바스크립트는 기본적으로 함수 레벨 스코프를 따르고 있기 때문에 varfor 문의 존재와 상관없이 해당 구문이 선언된 함수 레벨 스코프를 바라보고 있으므로 함수 내부 실행이 아니라면 전역 스코프에 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 ifor문을 순회하면서 각각의 스코프를 갖게 됨. 이는 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 내부에서만 사용하고 있지만 이를 알 수 있는 방법이 없기 떄문에 긴 배열을 저장해 두고 있는 모습. 반면 일반 함수의 경우에는 클릭 시 스크립트 실행이 조금 길지만 클릭과 동시에 선언, 그리고 길이를 구하는 작업이 모두 스코프 내부에서 끝났기 때문에 메모리 용량에 영향을 미치지 않음.

클로저의 개념, 즉 외부 함수를 기억하고 이를 내부 함수에서 가져다 쓰는 메커니즘은 성능에 영향을 미침. 클로저에 꼭 필요한 작업만 남겨두지 않는다면 메모리를 불필요하게 잡아먹는 결과를 야기할 수 있고, 마찬가지로 클로저 사용을 적절한 스코프로 가둬두지 않는다면 성능에 악영향을 미침.