자바스크립트 클로저

Date
Created
Apr 27, 2024 10:43 AM
Tags
1장

클로저

클로저는 함수와 함수가 선언된 어휘적 환경(Lexical Scope) 의 조합 이라고 되어있다.

선언된 어휘적 환경

function add() { const a = 10 function innerAdd() { const b = 20 console.log(a + b) } innerAdd() } add()
위 코드에 살펴보면 a의 유효 범위는 add 전체 b의 유효범위는 innerAdd.
즉 선언된 어휘적 한경 이라는 것은 변수가 코드 내부에서 어디서 선언 되었는지 말함.
호출되는 방식에 따라 동적으로 결정되는 this와는 다르게 코드가 작성된 순간에 정적으로 결정됨.
클로저는 이런 어휘적 환경을 조합해 코딩하는 기법.

전역 스코프

전역 레벨에 선언하는걸 전역 스코프라고 함.
브라우저는 window, node.js 는 global이 있고 이 객체에 전역 레벨의 스코프가 바인딩됨.

함수 스코프

다른 언어와 달리 자바스크립트는 함수 레벨 스코프를 따름.
{} 블록이 스코프 범위를 결정하지 않는다.
if (true){ var global = 'global' } console.log(global === window.global) // true
global은 {} 내부에 선언되어 있지만 {} 밖에서도 접근이 가능함을 볼 수 있고 이는 자바스크립트가 함수 레벨 스코프를 가지고 있기 때문.
var x = 10 function foo (){ var x = 100 console.log(x) function bar(){ var x = 1000 console.log(x) } } console.log(x) // 10
스코프가 중첩된 경우 자바스크립트는 가장 가까운 스코프에서 변수가 존재하는지를 먼저 확인함.

클로저의 활용

function outerFun() { var x = 'hello' function innerFun() { console.log(x) } return innerFun() } const innerFun = outerFun() innerFun() // 'hello'
이 코드에서 innerFun은 x라는 변수가 존재하던 환경을 기억하기 때문에 ‘hello’를 출력함.
전역 레밸의 변수는 어디서든 꺼낼 수 있다는 장점이 있지만 반대로 말하면 누구나 접근이 가능하다.
만약 리액트읜 useState가 전역에 저장되면 쉽게 리액트 어플리케이션을 망가트릴 것이다.
그래서 리액트가 관리하는 내부 상태 값은 리액트가 별도로 관리하는 클로저 내부에서만 접근이 가능하다.

리액트에서 클로저

리액트 함수형 컴포넌트의 훅에서 클로저는 어떻게 사용될까?
function Component(){ const [state, setState] = useState() function handleClick() { // useState의 호출은 위에서 끝났지만 // setState는 계속 내부의 최신값 pre를 알고 있다. setState((pre) => pre + 1))
위에서 본대로 클로저가 useState 내부에서 활용되었기 때문이다.
외부 함수(useState)가 반환한 내부 함수(setState)가 외부 함수의 호출이 끝났음에도 자신이 선언된 외부 함수의 선언 환경을 기억하기 때문에 계속 state 값을 사용할 수 있다.

주의할 점

for (var i = 0; i < 5; i++){ setTimeout(function () { console.log(i) }, i * 1000) }
위 코드는 1초 간격으로 0, 1, 2, 3, 4를 출력하는 것, 하지만 위 코드를 실행하면 4초 뒤 5만 출력됨.
이유는 var i가 전역 변수로 작동하기 때문.
자바스크립트는 기본적으로 함수 레벨 스코프를 따르기에 var i 는 전역에 등록되었다.
for문을 다 순회하고 태스크의 큐에 setTimeout을 수행하려 했을 때 i 는 5로 업데이트 되어있는 것.
for (let i = 0; i < 5; i++){ setTimeout(function () { console.log(i) }, i * 1000) }
위 코드는 의도대로 잘 작동한다.
let이 기본적으로 블록 레벨 스코프를 가지므로 let은 for문을 순회하며 각각의 스코프를 가진다.
이는 setTimeout이 실행되는 시점에도 유효해 각 콜백에 의도한 i값을 바라볼 수 있다.
for (let i = 0; i < 5; i++){ setTimeout((function (sec) { function(){ console.log(sec) } })(i), i * 1000) }
for 문 내부에 즉시 실행 익명 함수가 선언되었다.
이 즉시 실행 함수는 i을 인수로 받는데 이 내부에서는 sec을 인수로 저장했다가 setTimeout 콜백에 넘긴다.
이렇게 되면 setTimeout의 콜백 함수가 바라보는 클로저는 즉시 실행 익명 함수가 되고, 이 즉시 즉시 실행 익명 함수가 각for 문마다 생성되고 실행되기를 반복함.
각각의 함수는 고유의 스코프, sec을 가지기 때문에 올바르게 실행될 수 있다.
함수와 함수가 선언된 어휘적 환경(Lexical Scope) 의 조합을 잘 살펴 보아야 클로저를 활용할 수 있다.

클로저의 비용

클로저를 사용할 때 또 다른 주의 사항은 비용이다. 클로저는 생성될 때 마다 그 선언적 환경을 기억해야 하기에 추가로 비용이 발생한다.