클로저(Closure) 란?

Dean H. Park·2020년 7월 6일
7

JS

목록 보기
1/8
post-thumbnail

잠깐! 🙌🏻

1. 자바스크립트는 싱글 스레드(Single thread) 기반의 동적 언어이다.

자바스크립트는 한번에 하나씩 일을 처리한다.
흔히 보는 이 에러가 스택 트레이스(Stack Trace), 일을 순차적으로 처리하는 호출 스택 단계를 보여준다.

2. 자바스크립트는 프로토타입(Protoype) 기반의 동적 언어이다.

자바스크립트에서 정의되는 대부분 함수들은 최초 Object로부터 쭉 복제되어 내려오며, 복제될때마다 부모를 참조할수 있는 정보가 property로 들어온다.

함수간 참조가 가능하도록 연결된 것을 스코프 체인(Scope chain) 이라고 한다.

[MDN] 상속과 프로토타입

[Insanehong] javascript 기초 - Scope, Scope Chain & arguments

3. 자바스크립트는 어휘적 범위지정 즉, 렉시컬 스코프(Lexical scope)를 따른다.

자바스크립트는 특정 함수가 선언되는 순간 해당 함수의 상위 스코프를 결정한다.



클로저(Closure) 란?

내부함수가 외부함수의 내부변수에 접근하는 것이다.

자바스크립트는 함수가 선언될때, 렉시컬 스코프의 정보를 [[Scopes]] 프로퍼티를 통해 스코프 체인(Scope chain)을 참조하여, 유효범위를 인지한다.

여기서 중요한 것은 선언되는 순간 렉시컬 스코프, 단순하게 말하면 상위 스코프의 정보가 선언된 함수의 참조 프로퍼티로 저장된다는 것이다.

때문에 위 클로저 정의를 비개발자가 이해하기 쉽게 풀이 한다면,

내부함수가 선언된 순간에 저장된 외부함수 참조 정보를 이용하는 것이다.

가 될 것이다.



개발자 도구를 열고 아래 코드를 콘솔 창에 입력해 보자.

function a() {
    let v = 1;
    return function b() {
        console.log(v);        
    }
}

const c = new a();
console.dir(c);

결과가 다음과 같이 나올 것 이다.
위 캡쳐에서 확인 할 수 있듯, [[Scopes]] 프로퍼티가 해당 함수와 연관된 모든 범위에 대한 정보들을 갖고 있다.

내부함수 b는 클로저를 통하여 외부함수의 변수 v를 언제든 사용 할 수 있는 것이다.



실 예제를 한번 살펴보자.

const array = [1, 2, 3, 4, 5];

for (var i = 0; i < array.length; i++) {
  setTimeout(function() {
    console.log('number is: ' + i);
  }, 0);
}

// result : number is: 5 (X 5)

위 예제의 의문점은 두 가지 이다.

  1. 0,1,2,3,4가 아닌 5가 5번 출력 되는 이유?
  2. for문 조건에 부합하는 i = 4가 아닌, i = 5가 출력는 이유?

1. 0,1,2,3,4가 아닌 5가 5번 출력 되는 이유?

setTimeout()은 비동기(Asyncronous)이며, 논 블록킹(Non Blocking) 성질을 가지는 함수 이다.

자바스크립트는 싱글 스레드 기반 언어이다.

setTimeout의 처리 순서를 간략히 정리하면 다음과 같다.

[zerocho] 호출 스택과 이벤트루프

  1. setTimeout()이 콜 스택(call stack)에 push 됨.
  2. 곧바로 callback 처리되어 pop 됨.
  3. 브라우저 Web API의 이벤트 리스너가 setTimeout() callback 처리를 인지함.
  4. Web API에서 setTimeout()의 두번째 인자 시간만큼 지연이 끝난 후, setTimeout()의 첫번째 인자에 있는 익명 함수가 태스크 큐(Task Queue)에 삽입(Enqueue)됨.
  5. 콜 스택에 남아있는 for문 등 함수들이 실행되고 pop됨.
    (for문 반복조건 수 만큼 setTimeout()이 push/pop되고 실행한다)
  6. 콜 스택이 비워진 후, 태스크 큐에 있는 익명함수가 삭제(Dequeue)되어, 콜 스택에 push 됨.
  7. 콜 스택에 있는 익명함수가 실행 후, pop 됨. 이때 for문은 이미 실행된 상태이므로, 조건문의 마지막 값을 저장함.

결과적으로, setTimeout()에 지정한 시간이 0 이어도, 비동기~논블록킹 속성 때문에 for문과 별개로 동작하여, for문이 시행된 후의 최종값을 받게 된다.

이는 호이스팅되는 var의 속성과도 관련이 있는데, 쉽게 풀이하면 반복되는 모든 익명함수가 동일한 변수 i를 바라보는 것과 같다.

2. for문 조건에 부합하는 i = 4가 아닌, i = 5가 출력는 이유?

for문은 조건이 부합하지 않을때까지 3번째 인자를 수행한다.

var i가 0에서 4가 될때 까지 수행된 후, 5가 됐을때 조건문에 걸려 더이상 수행되지 않는다. 하지만, var는 이미 5가 된 상태이다.

위에서 언급했듯, for문이 종료된 이후 최종값이 비동기로 setTimeout()의 익명함수에 전달 된다. 때문에 var의 최종값인 5가 출력되는 것이다.

만약 조건문 의도대로 작업하고 싶다면 아래와 같이 처리하면 된다.

const array = [1, 2, 3, 4, 5];

for (var i = 0; i < array.length; i++) {
  var j = i;
  setTimeout(function() {
    console.log('number is: ' + j);
  }, 0);
}

// result : number is: 4 (X 5)



그렇다면 이러한 문제들을 피하고 원하는 대로 출력하고 싶다면 어떻게 해야 할까?

이에 대한 해결법은 다음과 같다.

  1. 함수 scope인 var 대신, 블록 scope인 let을 사용 한다.
  2. IIFE(Immediately Invoked Function Expressions)를 사용하여 동기화 시켜준다.

1. 함수 scope인 var 대신, 블록 scope인 let을 사용 한다.

위 예제의 변수 i는 함수 scope로써 호이스팅되며, setTimeout() 내부 함수로부터 참조되는 형식으로 사용된다.

for문의 각 조건 마다 대응하는 변수 값을 할당 하고 싶다면, 변수 scope를 for문으로 제한해야 한다.

이때 블록 scope인 let을 사용하여 문제를 해결 할 수 있다.

const array = [1, 2, 3, 4, 5];

for (let i = 0; i < array.length; i++) {
  setTimeout(function() {
    console.log('number is: ' + i);
  }, 0);
}

// result : 
// number is: 0
// number is: 1
// number is: 2
// number is: 3
// number is: 4

2. IIFE(Immediately Invoked Function Expressions)를 사용하여 동기화 시켜준다.

IIFE는 선언되자마자 실행되는 표현식이다.

const array = [1, 2, 3, 4, 5];

for (var i = 0; i < array.length; i++) {
  setTimeout((function() {
    console.log('number is: ' + i);
  })(), 1000);
}

// result : 
// number is: 0
// number is: 1
// number is: 2
// number is: 3
// number is: 4

IIFE 함수는 setTimeout()이 Call stack에서 push~pop 과정에서 실행되므로, for문과 동기화 처리가 가능하다.

다만, 위 코드는 한가지 문제가 있다.

setTimeout()의 첫번째 파라미터는 반드시 함수가 와야하며, 그렇지 않을 경우 시간 지연 기능이 동작 하지 않는다.

때문에 아래와 같이 function()으로 한번 감싸주고, 외부함수의 내부변수인 i를 인자로 받아 내부함수로 전달해 주어야 한다.

const array = [1, 2, 3, 4, 5];

for (var i = 0; i < array.length; i++) {
  setTimeout(function(j) { 
    return function() {
    	console.log('number is: ' + j);
    }
  }(i), 1000)
};

// result : 

// . . . 1 second delay

// number is: 0
// number is: 1
// number is: 2
// number is: 3
// number is: 4

정상적으로 동작하는 것을 확인할 수 있다.

profile
Hi, I'm dean. Front-end developer who likes UI/UX Design.

0개의 댓글