홈으로 돌아가기

JS 클로저: 렉시컬 환경과 예제

JavaScript 클로저는 외부 함수가 완료된 후에도 내부 함수가 외부 변수에 접근할 수 있게 합니다. 이 자료는 람다 계산법 이론, 루프 실전, OOP 에뮬레이션, 엔진 최적화를 다룹니다. 중/시니어 개발자를 위한 코드 예제.

JS에서 클로저가 작동하는 방식: 코드와 루프 함정
Advertisement 728x90

JavaScript 클로저: 렉시컬 환경 메커니즘과 실전 활용법

JavaScript에서 클로저는 함수와 그 함수의 렉시컬 환경의 조합으로, 내부 함수가 외부 함수의 실행이 완료된 후에도 외부 스코프의 변수에 접근할 수 있게 합니다. 이를 통해 비동기 컨텍스트에서 상태 캡슐화와 예측 가능한 동작을 구현할 수 있습니다.

이 개념은 Alonzo Church의 람다 계산법에서 비롯되었습니다. 고차 함수는 다른 함수를 반환하거나 받아들이며, 자유 변수(함수 내부에서 사용되지만 외부에서 선언된 변수)를 캡처합니다.

기본적인 카운터 예제로 원리를 살펴보겠습니다:

Google AdInline article slot
function counter() {
  let count = 0
  return function(step) {
    count = count + step
    return count
  }
}

const myCounter = counter()
console.log(myCounter(1)) // 1
console.log(myCounter(1)) // 2
console.log(myCounter(1)) // 3

내부 함수는 count를 클로저로 감싸 가비지 컬렉션을 방지합니다. 이는 실시간으로 변경 사항이 반영되는 라이브 참조입니다.

클로저를 객체지향 프로그래밍에 비유하기

클로저는 객체의 private 필드와 public API를 모방합니다. 메서드를 가진 카운터 예제:

function Counter(initialValue = 0) {
  let count = initialValue
  return {
    increment: function(step = 1) {
      return count += step
    },
    decrement: function(step = 1) {
      return count -= step
    },
    getValue: function() {
      return count
    },
    reset: function() {
      return count = initialValue
    }
  }
}

const myCounter = Counter(10)
console.log(myCounter.getValue()) // 10
console.log(myCounter.increment(5)) // 15

private 필드(#count)를 사용한 클래스로 동일한 로직을 구현할 수 있습니다. Pinia/Vuex 스토어에서는 클로저가 반응형 상태를 캡슐화합니다:

Google AdInline article slot
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const initValue = 0
  function increment(step) {
    count.value += step
  }
  return { count, increment }
})

반복문과 이터레이터에서의 미묘한 차이

반복문에서 클로저의 동작은 호이스팅과 블록 스코프에 따라 달라집니다.

var 사용 시 — 모든 반복에서 공유되는 변수:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i) // 3, 3, 3
  }, 100)
}

let 사용 시 — 반복마다 별도의 렉시컬 환경 생성:

Google AdInline article slot
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i) // 0, 1, 2
  }, 100)
}

forEach, map, filter 같은 이터레이터는 자동으로 클로저를 생성합니다:

[0, 1, 2].forEach(function(i) {
  setTimeout(function() {
    console.log(i) // 0, 1, 2
  }, 100)
})

실전: 콜백과 이벤트

비동기 코드에서 클로저는 컨텍스트 문제를 해결합니다.

고유 ID를 사용한 Ajax 요청:

function ajaxRequest(url) {
  let requestId = Math.random().toString(36).substring(2)
  fetch(url).then(data => {
    console.log(`Request ${requestId} completed:`, data)
  }).catch(error => {
    console.log(`Request ${requestId} failed:`, error.message)
  })  
}

이벤트 핸들러:

function clickSetup(elemId, message) {
  const elem = document.getElementById(elemId)
  elem.addEventListener('click', function() {
    console.log(message)
  })
}

화살표 함수도 일반 함수와 동일하게 작동합니다.

최적화와 디버깅

JS 엔진(V8)은 메모리를 절약하기 위해 클로저에서 사용되지 않는 변수를 제외합니다. DevTools(Scope → Closure)에서는 관련 변수만 표시됩니다.

핵심 실천법:

  • 블록 스코프를 위해 반복문에서 let/const를 사용하세요.
  • 클래스 없이 private 상태를 구현할 때 클로저를 선호하세요.
  • 비동기 콜백에서 컨텍스트를 조기에 고정하세요.
  • 장기간 유지되는 클로저에서 메모리를 모니터링하세요.

핵심 요약

  • 클로저 = 함수 + 변수에 대한 라이브 참조를 가진 렉시컬 환경.
  • 반복문에서 let은 반복마다 클로저를 생성하지만, var는 그렇지 않습니다.
  • 객체지향 프로그래밍의 private 필드를 모방하며, Vuex/Pinia 스토어에서 사용됩니다.
  • 엔진에 의해 최적화: 사용되지 않는 변수는 제외됩니다.
  • 비동기 콜백과 이벤트에 필수적입니다.

— Editorial Team

Advertisement 728x90

다음 읽기