[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) // 잔액 부족으로 출금 실패
✨클로저로 접근권한 제어하는 방법
- 함수에서 지역변수 및 내부함수 등을 생성한다.
- 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터를 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!" 출력
'📖 책 찢기 > 코어 자바스크립트' 카테고리의 다른 글
[코어자바스크립트] 7장. 클래스 (0) | 2024.06.18 |
---|---|
[코어자바스크립트] 6장. 프로토타입 (0) | 2024.06.18 |
[코어자바스크립트] 4장. 콜백 함수 (0) | 2024.06.14 |
[코어자바스크립트] 3장. this (0) | 2024.06.14 |
[코어 자바스크립트] 2장. 실행 컨텍스트 (3) | 2024.06.11 |