Language/React.js
[React] 클로저
JJcoding
2024. 11. 7. 15:35
클로저
- MDN 정의 : 함수와 함수가 선언된 어휘적 환경(Lexical Scope)의 조합
- 함수형 컴포넌트에 대한 이해는 클로저에 달려 있다.
- 함수형 컴포넌트의 구조와 작동 방식, 훅의 원리, 의존성 배열 등 대부분의 기술이 모두 클로저에 의존하고 있다.
예제
function add() {
const a = 10;
function innerAdd() {
const b = 20;
console.log(a + b);
}
innerAdd() // 30
}
add()
- 변수 a의 유효 범위는 add 전체이고, b의 유효 범위는 innerAdd 전체다. innerAdd는 add 내부에 선언돼 있어 a를 사용할 수 있게 된 것이다.
- 즉, “선언된 어휘적 환경” 이라는 것은, 변수가 코드 내부에서 어디서 선언됐는지를 말하는 것이다.
- 클로저는 이러한 어휘적 환경을 조합해 코딩하는 기법이다.
변수의 유효 범위, 스코프
- 변수의 유효 범위를 스코프(Scope)라고 하는데, 자바 스크립트에는 다양한 스코프가 있다.
전역 스코프
var global = 'global scope'
function hello() {
console.log(global);
}
console.log(global); // global scope
hello(); // global scope
- 변수를 전역 레벨에 선언하는 것을 전역 스코프라고 하며, 어디서든 호출 할 수 있다.
- 전역 스코프와 hello 스코프 모두에서 var로 선언된 global 변수에 접근할 수 있다.
함수 스코프
if(true) {
var global = 'global scope'
}
console.log(global) // 'global scope'
function hello() {
var local = 'local variable'
console.log(local) // local variable
}
hello();
console.log(local) // ReferenceError: local is not defined
- if문과 for문 같은 일반 블록{ } 안에서 선언된 var 변수를 블록{ } 밖에서도 접근할 수 있다.
- 하지만 함수 내부에서 선언된 var는 함수 스코프를 가지기 때문에 접근할 수 없다.
💡let과 const는 var와 달리 블록 스코프를 가지기 때문에 위 두 경우에 모두 접근 불가능하다.
스코프가 중첩돼 있다면 어떻게 될까?
var x = 10;
function foo() {
var x = 100;
console.log(x); // 100
function bar() {
var x = 1000;
console.log(x); // 1000
}
bar();
}
console.log(x);// 10
foo();
- 자바스크립트에서 스코프는 일단 가장 가까운 스코프에서 변수가 존재하는지를 먼저 확인한다.
클로저에 대한 개념을 조금 더 정확히 이해해보자.
function outerFunction() {
var x = 'hello';
function innerFunction() {
console.log(x);
}
return innerFunction;
}
const innerFunction = outerFunction();
innerFunction(); // hello
- outerFunction은 innerFunction을 반환하며 실행이 종료됐다. 여기에서 반환된 함수에는 x라는 변수가 존재하지 않지만, 해당 함수가 선언된 어휘적 환경, 즉 outerFunction에는 x라는 변수가 존재하며 접근할 수도 있다.
- 따라서 같은 환경에서 선언되고 반환된 innerFunction에서는 x라는 변수가 존재하던 환경을 기억하기 때문에 정상적으로 “hello”를 출력할 수 있는 것이다.
클로저의 활용
function Counter() {
var counter = 0;
return {
increase: function() {
return ++counter;
},
decrease: function() {
return --counter;
},
counter: function() {
console.log('counter에 접근!')
return counter;
},
}
}
var c = Counter();
console.log(c.increase()); // 1
console.log(c.increase()); // 2
console.log(c.increase()); // 3
console.log(c.decrease()); // 2
console.log(c.counter()); // counter에 접근! 2
- counter 변수를 직접적으로 노출하지 않음으로써 사용자가 직접 수정하는 것을 막았다.
- counter 변수의 업데이트를 increase와 decrease 제한해 무분별하게 변경되는 것을 막았다.
- 이처럼 클로저를 활용하면 전역 스코프의 사용을 막고, 개발자가 원하는 정보만 개발자가 원하는 방향으로 노출시킬 수 있다는 장점이 있다.
리액트에서의 클로저
function Component() {
const [state, setState] = useState();
function handleClick() {
// useState 호출은 위에서 끝났지만,
// setState는 계속 내부의 최신값(prev)을 알고 있다.
// 이는 클로저를 활용했기 때문에 가능하다.
setState((prev) => prev + 1);
}
}
- 외부 함수(useState)가 반환한 내부 함수(setState)는 외부 함수의 호출이 끝났음에도 자신이 선언된 환경(state가 저장돼 있는 어딘가)을 기억하기 때문에 계속해서 state 값을 사용할 수 있는 것이다.
주의할 점
예제
for(var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}
- 이 코드의 의도는 1초 간격으로 0, 1, 2, 3, 4를 차례대로 출력하는 것이다.
- 그러나 실제로 실행하면 5만 1초 간격으로 5번 출력된다.
- 이유는? i가 전역 변수로 작동하기 때문이다. var는 함수 레벨 스코프를 바라보고 있으므로 전역 스코프에 var i가 등록돼 있을 것이다. for문을 다 순회한 이후, 태스크 큐에 있는 setTimeout을 실행하려고 했을 때, 이미 전역 레벨에 있는 i는 5로 업데이트가 완료돼 있다.
해결방법 1.
for(let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}
- 첫 번째 방법은 블록 스코프를 갖는 let으로 수정하는 것이다. 처음 의도대로 실행된다.
해결방법 2.
for(var i = 0; i < 5; i++) {
setTimeout(
(function (sec) {
return function() {
console.log(sec)
}
})(i),
i * 1000
);
}
- 두 번째 방법은 클로저를 제대로 활용하는 것이다.
- setTimeout 내부에 즉시 실행 함수를 선언했다. 이 함수는 i 를 인수로 받고 sec에 저장했다가 setTimeout의 콜백 함수에 넘기게 된다.
- 이렇게 되면 setTimeout의 콜백 함수가 바라보는 클로저는 즉시 실행 익명 함수가 되는데, 이 즉시 실행 함수는 각 for문마다 생성되고 실행되기를 반복한다.
- 그리고 각각의 함수는 고유한 스코프, 즉 고유한 sec를 가지게 되므로 올바르게 실행할 수 있게 된다.
💡마지막으로, 클로저를 사용하는 데는 비용이든다는 점을 기억하자.
- 클로저는 생성될 때마다 그 선언적 환경을 기억해야 하므로 추가로 비용이 발생한다.
- 외부 함수를 기억하고 이를 내부 함수에서 가져다 쓰는 메커니즘은 성능에 영향을 미친다.
- 클로저에 꼭 필요한 작업만 남겨두지 않는다면 메모리를 불필요하게 잡아먹는 결과를 야기할 수 있고, 마찬가지로 클로저 사용을 적절한 스코프로 가둬두지 않는다면 성능에 악영향을 미친다.
- 클로저는 공짜가 아니므로 클로저를 사용할 때는 주의가 필요하다.
출처 : 모던 리액트 Deep Dive