본문 바로가기
📖 책 찢기/코어 자바스크립트

[코어자바스크립트] 5장. 클로저

by 짱돌보리 2024. 6. 17.
728x90

[5장] 클로저

1. 클로저의 의미 및 원리 이해

클로저

  • 함수와 그 함수가 선언될 당시의 lexical envrionment의 상호관계에 따른 현상
  • 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상

→ 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상

var outer = function () {
  var a = 1
  var inner = function () {
    console.log(++a)
  }
  return inner
}
var outer2 = outer()
console.log(outer2()) // 2
console.log(outer2()) // 3
  • inner 함수의 실행 결과가 아닌 inner함수 자체를 반환했다.
  • outer함수의 실행 컨텍스트가 종료될 때 outer2 변수는 outer의 실행결과인 inner함수를 참조하게 된다.
  • outer2를 호출하면 앞서 반환된 함수인 inner가 실행된다.
  • 스코프 체이닝에 따라 outer에서 선언된 변수 a에 접근해서 1만큼 증가시킨 후 2가 출력되고 inner함수의 실행 컨텍스트가 종료된다.
  • 다시 outer2를 호출하면 같은 방식으로 3이 출력된다.

가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다. outer함수는 실행 종료 시점에 inner 함수를 반환하기 때문에 외부 함수인 outer의 실행이 종료되더라도 내부함수인 inner는 언젠가 outer2를 실행함으로써 호출될 가능성이 있다. 그 덕에 inner함수가 a 변수에 접근할 수 있는 것!

2. 클로저와 메모리 관리

클로저가 생성된 이후에도 참조하고 있는 변수가 계속 메모리에 유지되어 있어서, 의도치 않은 메모리 누수가 발생할 수 있다.

🤔참조 카운트가 0이 되면 GC가 수거해갈 것이고 메모리가 회수될 것이다. 어떻게 0으로 만들까

function createCounter() {
  let count = 0
  let counterElement = document.getElementById('counter')

  function incrementCounter() {
    count++
    counterElement.textContent = count
  }

  return function () {
    incrementCounter()
    counterElement = null // 참조 해제
  }
}

const counter = createCounter()
counter() // 메모리 누수 방지

3. 클로저 활용 사례

📍콜백 함수 내부에서 외부 데이터를 사용하고자 할 때

var alertFruitBuilder = function (fruit) {
  // 클로저를 반환하여 각 과일 이름을 기억하는 함수 생성
  return function () {
    alert('Your choice is ' + fruit)
  }
}

var fruits = ['Apple', 'Banana', 'Cherry']

fruits.forEach(function (fruit) {
  var $li = document.createElement('li') // 새로운 li 요소 생성
  $li.innerText = fruit
  $li.addEventListener('click', alertFruitBuilder(fruit))
  document.body.appendChild($li)
})

alertFruitBuilder 함수가 각 과일 이름을 기억하는 클로저를 반환하여, 각 li 요소의 클릭 이벤트 핸들러가 올바르게 과일 이름을 표시할 수 있게 한다.

 

📍접근 권한 제어(정보 은닉)

❓정보 은닉

어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 것

  • 클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능하다.
function createBankAccount(initialBalance) {
  let balance = initialBalance // 외부에서 직접 접근할 수 없는 변수

  return {
    deposit: function (amount) {
      if (amount > 0) {
        balance += amount
        console.log(`$${amount} 입금되었습니다. 현재 잔액: $${balance}`)
      } else {
        console.log('유효하지 않은 입금 금액입니다.')
      }
    },
    withdraw: function (amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount
        console.log(`$${amount} 출금되었습니다. 현재 잔액: $${balance}`)
      } else {
        console.log('유효하지 않은 출금 금액이거나 잔액이 부족합니다.')
      }
    },
    getBalance: function () {
      return balance
    },
  }
}

// 은행 계좌 생성
const myAccount = createBankAccount(1000) // 초기 잔액 $1000으로 계좌 생성
myAccount.deposit(500) // $500 입금 -> 현재 잔액: $1500
myAccount.withdraw(200) // $200 출금 -> 현재 잔액: $1300
console.log('현재 잔액: $' + myAccount.getBalance()) // 현재 잔액: $1300
myAccount.withdraw(2000) // 잔액 부족으로 출금 실패

 

✨클로저로 접근권한 제어하는 방법

  1. 함수에서 지역변수 및 내부함수 등을 생성한다.
  2. 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터를 return한다.

    → return한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 된다.
function createSecureStorage() {
  // 1. 함수에서 지역변수 및 내부함수 등을 생성한다.
  let privateData = {} // 비공개 멤버: 외부에서 직접 접근할 수 없는 데이터 저장소

  function setItem(key, value) {
    privateData[key] = value // 데이터를 저장하는 내부 함수
  }

  function getItem(key) {
    return privateData[key] // 데이터를 가져오는 내부 함수
  }

  function removeItem(key) {
    delete privateData[key] // 데이터를 삭제하는 내부 함수
  }

  // 2. 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터를 return한다.
  return {
    setItem, // 공개 멤버: 데이터를 저장하는 메서드
    getItem, // 공개 멤버: 데이터를 가져오는 메서드
    removeItem, // 공개 멤버: 데이터를 삭제하는 메서드
  }
}

// 클로저를 사용하여 안전한 저장소 생성
const storage = createSecureStorage()

// 데이터를 저장
storage.setItem('username', 'JohnDoe')
storage.setItem('password', 'supersecret')

// 데이터를 가져오기
console.log(storage.getItem('username')) // JohnDoe
console.log(storage.getItem('password')) // supersecret

// 데이터를 삭제
storage.removeItem('password')
console.log(storage.getItem('password')) // undefined

// privateData는 외부에서 접근할 수 없다.
console.log(storage.privateData) // undefined

 

📍부분 적용 함수

  • n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수

bind 메서드를 활용한 부분 적용 함수

// 가변 인자를 받아 합을 계산하는 기본 함수 정의

var add = function () {
  var result = 0
  // 전달된 모든 인자의 합을 계산
  for (var i = 0; i < arguments.length; i++) {
    result += arguments[i]
  }
  return result // 최종 합계를 반환
}

// 부분 적용 함수를 생성, 첫 다섯 개의 인자(1, 2, 3, 4, 5)를 미리 적용
var addPartial = add.bind(null, 1, 2, 3, 4, 5)

// 나머지 인자(5, 6, 7, 8, 9, 10)를 전달하여 부분 적용 함수 호출
console.log(addPartial(5, 6, 7, 8, 9, 10)) // 출력: 55

 

✨디바운스 함수

// 부분 적용 함수를 사용하여 디바운스 함수 구현
function debounce(func, delay) {
  let timeoutId

  // 부분 적용 함수를 반환
  return function (...args) {
    // 이전 타임아웃을 클리어하고 새로운 타임아웃 설정
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      func(...args) // 인자를 받아 실행
    }, delay)
  }
}

// 예시 함수 - 입력받은 인자를 콘솔에 출력하는 함수
function logInput(input) {
  console.log('Input:', input)
}

// 300ms 딜레이를 가진 디바운스 함수 생성
const debounceLogInput = debounce(logInput, 300)

// 버튼 클릭 시 디바운스 함수 호출
document
  .getElementById('debounceButton')
  .addEventListener('click', function () {
    debounceLogInput('Clicked!')
  })

디바운스 함수는 사용자의 입력에 반응하는 이벤트 처리기에서 유용하게 사용될 수 있다.

예) 검색어 입력 창에서 사용자가 연속적으로 타이핑할 때마다 실시간으로 검색을 실행하는 것이 아니라, 마지막 입력 후 일정 시간이 지난 후에 검색을 수행하도록 제어할 수 있다.

 

📍커링 함수

  • 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것
  • 커링은 한 번에 하나의 인자만 전달하는 것이 원칙이다.
  • 함수를 실행한 결과는 그다음 인자를 받기 위해 대기만 할 뿐으로, 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다.
// 커링 함수 정의
var curry3 = function (func) {
  // 첫 번째 인자를 받아 두 번째 함수를 반환
  return function (a) {
    // 두 번째 인자를 받아 최종 함수 실행
    return function (b) {
      return func(a, b) // func 함수에 a와 b 인자를 적용하여 결과 반환
    }
  }
}

// getMaxWith10: Math.max 함수에 첫 번째 인자를 10으로 고정한 부분 적용 함수 생성
var getMaxWith10 = curry3(Math.max)(10)

// getMinWith10: Math.min 함수에 첫 번째 인자를 10으로 고정한 부분 적용 함수 생성
var getMinWith10 = curry3(Math.min)(10)

// 부분 적용 함수 사용 예시
console.log(getMaxWith10(8)) // 출력: 10 (10과 8 중 더 큰 값)
console.log(getMaxWith10(25)) // 출력: 25 (10과 25 중 더 큰 값)

console.log(getMinWith10(8)) // 출력: 8 (10과 8 중 더 작은 값)
console.log(getMinWith10(25)) // 출력: 10 (10과 25 중 더 작은 값)

 

✨지연 실행 함수

function delayExecution(func, delay) {
  return function (...args) {
    setTimeout(() => {
      func(...args)
    }, delay)
  }
}

function greet(name) {
  console.log(`Hello, ${name}!`)
}

// 지연 실행 함수를 사용하여 greet 함수를 호출 지연시키기
const delayedGreet = delayExecution(greet, 2000) // 2초 후에 실행되도록 설정

// 호출
delayedGreet('Alice') // 2초 후에 "Hello, Alice!" 출력
delayedGreet('Bob') // 2초 후에 "Hello, Bob!" 출력
delayedGreet('Carol') // 2초 후에 "Hello, Carol!" 출력