안녕하세요! JavaScript의 클로저(closure)에 대해 설명드리겠습니다.
클로저의 개념:
클로저는 JavaScript의 강력한 기능 중 하나로, 함수가 선언될 때의 렉시컬 환경(Lexical Environment)에 대한 참조를 유지하는 함수를 말합니다. 이를 통해 함수는 자신이 선언될 당시의 환경 밖에서 호출되더라도 그 환경에 접근할 수 있습니다.
클로저의 특징:
- 외부 함수의 변수 접근: 클로저를 통해 내부 함수는 외부 함수의 변수에 접근할 수 있습니다.
- 은닉화와 캡슐화: 외부에서 접근할 수 없는 private 변수를 만들 수 있습니다.
- 상태 유지: 함수가 호출될 때마다 새로운 환경이 생성되지 않고, 기존 환경을 유지할 수 있습니다.
클로저의 예시:
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
console.log(counter()); // 0
console.log(counter()); // 1
이 예시에서 makeCounter
함수는 내부 함수를 반환합니다. 이 내부 함수는 makeCounter
의 count
변수에 접근할 수 있습니다. counter
함수를 호출할 때마다 count
변수의 값이 증가하고, 이 값은 함수 호출 간에 유지됩니다. 이것은 클로저 덕분에 가능합니다.
용례:
- 데이터 은닉과 캡슐화: 특정 데이터를 외부로부터 보호하고 싶을 때 사용합니다.
- 상태 유지: 예를 들어, 이벤트 핸들러나 비동기 호출에서 특정 상태를 유지해야 할 때 유용합니다.
- 커링과 부분 적용 함수: 함수의 인자 일부를 미리 "설정"하고 나중에 나머지 인자를 받아 최종 결과를 계산할 때 사용됩니다.
주의점:
클로저는 유용하지만 메모리 누수의 원인이 될 수 있습니다. 클로저를 통해 참조되는 변수는 가비지 컬렉터에 의해 회수되지 않기 때문에, 더 이상 필요하지 않은 경우에는 클로저를 해제하는 것이 중요합니다.
클로저는 JavaScript의 중요한 개념 중 하나로, 함수형 프로그래밍에서 매우 중요한 역할을 합니다. 이 개념을 이해하면 JavaScript 코드를 더 효율적으로 작성하고 관리할 수 있습니다.
물론입니다! 클로저는 자바스크립트에서 다양한 상황에서 유용하게 사용됩니다. 여기 몇 가지 일반적인 사례와 그에 대한 설명을 드리겠습니다:
1. 데이터 은닉과 캡슐화
클로저를 사용하면 특정 데이터를 외부의 접근으로부터 보호할 수 있습니다. 이는 객체 지향 프로그래밍에서 'private' 변수를 구현하는 데 유사합니다.
function createBankAccount() {
let balance = 0; // 외부에서 접근 불가능한 private 변수
return {
deposit: function(amount) {
balance += amount;
return balance;
},
withdraw: function(amount) {
if (amount <= balance) {
balance -= amount;
return balance;
} else {
return 'Insufficient funds';
}
}
};
}
const account = createBankAccount();
console.log(account.deposit(100)); // 100
console.log(account.withdraw(50)); // 50
console.log(account.balance); // undefined, 외부에서 직접 접근할 수 없음
이 예에서 balance
는 외부에서 직접 접근할 수 없습니다. 오직 deposit
과 withdraw
메서드를 통해서만 balance
에 접근할 수 있습니다.
2. 상태 유지
이벤트 핸들러나 콜백에서 클로저는 특정 상태를 유지하는 데 사용됩니다.
function setupButton(buttonId, initialCount) {
let count = initialCount;
document.getElementById(buttonId).addEventListener('click', function() {
count++;
console.log(`Button ${buttonId} clicked ${count} times.`);
});
}
setupButton('myButton', 0);
이 코드에서 클릭 이벤트 핸들러는 count
변수의 상태를 유지합니다. 버튼을 클릭할 때마다 카운트가 증가하고, 이 값은 이벤트 핸들러 내부에서만 접근 가능합니다.
3. 커링과 부분 적용 함수
함수의 인자 중 일부를 미리 "세팅"해두고 나중에 나머지 인자를 받아 최종 결과를 계산할 때 클로저가 사용됩니다.
function multiply(a, b) {
return a * b;
}
function curriedMultiply(a) {
return function(b) {
return multiply(a, b);
};
}
const double = curriedMultiply(2);
console.log(double(5)); // 10
여기서 curriedMultiply
는 클로저를 반환합니다. 이 클로저는 a
의 값을 "기억"하고, 나중에 b
가 주어지면 최종 결과를 계산합니다.
클로저는 자바스크립트에서 강력하고 유연한 기능을 제공하며, 위와 같은 경우에서 코드를 보다 간결하고 효율적으로 만들어 줍니다.
JSX에서 사용되는 React의 useState
훅 자체를 클로저라고 부르는 것은 정확하지 않습니다. 그러나 useState
와 같은 훅을 사용할 때 클로저가 발생하는 상황은 있습니다.
useState
와 클로저
useState
훅은 함수형 컴포넌트에서 상태 관리를 위해 사용됩니다. 이 훅을 사용할 때, 상태 업데이트 함수(예: setState
)는 클로저를 생성할 수 있습니다. 이 함수는 해당 컴포넌트의 렌더링 시점에서의 상태를 "캡처"하며, 이후에도 그 상태에 접근할 수 있게 합니다.
예를 들어, 다음과 같은 상황을 고려해 보겠습니다:
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
setCount(count + 1);
}, 1000);
}, []); // 의존성 배열이 빈 배열이므로 컴포넌트 마운트 시 한 번만 실행됩니다.
return <div>{count}</div>;
}
위 코드에서 setTimeout
내부의 콜백 함수는 클로저입니다. 이 콜백 함수는 setCount
를 호출할 때 count
의 값을 "캡처"합니다. 그러나 이 경우, count
는 항상 0일 것이며, 이는 useEffect
의 의존성 배열이 빈 배열로 설정되어 있기 때문입니다.
클로저의 영향
이러한 클로저의 동작 방식은 때때로 예상치 못한 결과를 초래할 수 있습니다. 예를 들어, 상태 값이 업데이트되었을 때 이전 상태 값을 기반으로 다음 상태 값을 계산해야 하는 경우, 항상 최신 상태 값을 참조하는 것이 중요합니다.
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(currentCount => currentCount + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}
위 예시에서 setCount
는 함수를 인자로 받고 있습니다. 이 함수는 현재의 count
값을 인자로 받아 새로운 상태를 계산합니다. 이 방식은 클로저를 통해 항상 최신 상태를 참조할 수 있게 해줍니다.
결론
useState
자체를 클로저라고 부르기보다는, useState
를 사용하는 과정에서 클로저가 발생할 수 있다고 이해하는 것이 더 적절합니다. 이러한 클로저의 특성을 이해하는 것은 React에서 상태 관리를 효과적으로 하기 위해 중요합니다.
MDN 의 조금 알기 쉬운 예제
const makeCounter = function () {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment() {
changeBy(1);
},
decrement() {
changeBy(-1);
},
value() {
return privateCounter;
},
};
};
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1.value()); // 0.
counter1.increment();
counter1.increment();
console.log(counter1.value()); // 2.
counter1.decrement();
console.log(counter1.value()); // 1.
console.log(counter2.value()); // 0.
성능 관련 고려 사항
앞에서 언급했듯이, 각 함수 인스턴스는 자체 범위와 클로저를 관리합니다. 특정 작업에 클로저가 필요하지 않는데 다른 함수 내에서 함수를 불필요하게 작성하는 것은 처리 속도와 메모리 소비 측면에서 스크립트 성능에 부정적인 영향을 미치기 때문에, 현명하지 않습니다.
예를 들어, 새로운 객체/클래스를 생성할 때, 메소드는 일반적으로 객체 생성자에 정의되기보다는 객체의 프로토타입에 연결되어야 합니다. 그 이유는 생성자가 호출될 때마다 메서드가 다시 할당되기 때문입니다(즉, 모든 객체가 생성될 때마다).
다음 예를 생각해보세요.
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function () {
return this.name;
};
this.getMessage = function () {
return this.message;
};
}
앞의 코드는 특정 인스턴스에서 클로저의 이점을 활용하지 않음으로 다음과 같이 클로저를 사용하지 않도록 다시 쓸 수 있습니다.
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName() {
return this.name;
},
getMessage() {
return this.message;
},
};
그러나, 프로토타입을 다시 정의하는 것은 권장되지 않으므로, 기존 프로토타입에 추가하는 다음 예제가 더 좋습니다.
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function () {
return this.name;
};
MyObject.prototype.getMessage = function () {
return this.message;
};
'javascript' 카테고리의 다른 글
this 바인딩 정리 (0) | 2024.03.10 |
---|---|
함수형프로그래밍, 렉시컬스코프, 클린코드 (0) | 2024.03.07 |
[자바스크립트 32] 에러 처리 (0) | 2023.06.14 |
[자바스크립트 31] 모듈 (0) | 2023.06.14 |
[자바스크립트 30] 프로토타입과 상속 3 - 클래스 (0) | 2023.06.14 |