본문 바로가기
언어/JavaScript

(JavaScript)컨텍스트와 클로져

by 흥부와놀자 2021. 9. 6.

컨텍스트에 대해서

실행가능한 자바스크립트 코드 블록이 실행되는 환경이다.

컨텍스트가 실행되는 경우는 전역코드, Eval() 함수로 실행되는 코드, 함수코드 이다.  이러한 코드가 실행되면 해당 코드에 맞는 실행 컨텍스트가 실행되게 된다. C나 Java의 콜스택과 같이 스택에 쌓이고 가장 최상단 스택의 내용이 현재 실행되고 있는 컨텍스트이다. 

 

활성객체(변수객체)

컨텍스트가 생성되면 JS엔진은 컨텍스트의 정보를 담을 객체를 같이 생성함. 해당 객체 안에는 그 컨텍스트의 Arguments(함수코드일때), 스코프체인([[scope]]), 일반변수(정의된 함수 포함), this가 들어있다. 

 

스코프 체인

변수와 함수들을 인식하기 위한 연결리스트이다. 활성객체안에 있는 [[scope]] 프로퍼티를 통해 해당 스코프 체인을 참조 할 수 있다.

처음 생성되는 전역컨텍스트의 스코프체인에는 전역객체가 들어있는데, 이후로 실행되는 컨텍스트의 스코프체인엔 해당 컨텍스트를 호출한 컨텍스트의 스코프체인을 그대로 복사한 후 가장 상위에 현재 컨텍스트객체를 추가해준다.

렉시컬 스코핑

스코프는 함수를 호출할 때가 아니라 함수를 어디에 선언했는지에 따라 결정됨

 

작동과정

var value = "value1";

function printFunc(){
  var value = "value2";
  
  function printValue(){
    return value;
  }

  console.log(printValue());
}
printFunc()

참조(https://okky.kr/article/503999)

1. 컨텍스트가 처음 시작되면 전역 컨텍스트가 생긴다. 해당 컨텍스트의 변수객체안엔 value 변수, printFunc 변수, this,스코프 체인이 들어있다. 스코프체인의 경우 전역객체만 존재한다.

2. 전역 컨텍스트 코드에서 printFunc를 실행하기 때문에 printFunc 컨텍스트가 생기게 되고 변수객체엔 해당 함수의 변수들과 함수 정의가 있고, 스코프체인엔 해당 함수가 정의된 전역컨텍스트의 스코프체인에 현재 printFunc변수객체가 추가된다.

3. printFunc에서 printValue를 실행하기 때문에 printValue컨텍스트가 생기게 되고 해당 스코프 체인엔 해당 함수가 정의된 printFunc의 스코프체인에다가 현재 printValue의 변수객체가 추가된다.

 

그렇다면 PrintValue의 value는 어떤 변수를 인식한 것일까?

답은 스코프체인에 있다. 가장 먼저 현재 컨텍스트의 변수객체 안에서 value의 정의를 찾는다. 만약 없으면 다음 스코프체인으로 넘어가게 되고, printFunc에서 찾게 되는데, 해당 value의 정의가 존재하므로 printFunc의 value값인 "value2"를 리턴한다. 

 

비교예시

var value = "value1";

function printValue() {
    return value;
}
function printFunc(func) {
    var value = "value2";
    console.log(func());
}

printFunc(printValue);

참조(https://okky.kr/article/503999)

위의 예시와 다르게 printValue의 스코프 체인이 전역객체와 자신의 변수객체밖에 존재하지 않는데, 이는 스코프체인이 복사되는 곳은 바로 현재 함수가 정의된 곳이기 때문이다. printValue함수가 비록 printFunc내부에서 실행됬지만 정의된건 전역 컨텍스트 내부이기 때문에 전역 컨텍스트의 스코프체인을 복사한 후 자신의 변수객체를 붙여준다. 그렇기에 위의 실행결과는 printValue 컨텍스트의 바로 밖에 있는 전역컨텍스트 활성객체의 value값 "value1"이 된다.

 

클로저

function otherFunc(arg1,arg2) {
  var local = 8;
  function innerFunc(innerArg) {
    console.log((arg1 + arg2) / (innerArg + local));
  }
  return innerFunc;
}
var exam1 = outerFunc(2,4);
exam1(2);

위의 코드에서 보듯이 otherFunc가 리턴되어 종료되었지만 리턴된 함수 innerFunc에서 종료된 otherFunc의 변수 local을 참조하고 있다. 외부함수가 이미 종료됬지만 해당 함수의 스코프체인은 그대로 남아있어서 참조할 수 있는데,  종료된 외부 함수의 변수를 참조하는 함수를 클로저라고 보면 된다. 이 코드에선 innerFunc가 클로저가 된다.

 

그럼 이러한 클로저는 어디에 써먹을 수 있을까?

 

함수의 캡슐화

만약 어떤 함수에서 사용하는 전역변수가 있을때, 이 변수를 다른곳에서 사용하지 못하게 하고 싶을때 사용할 수 있다. 

아래의 코드는 전역변수 age를 활용해 해당 Person객체의 BrotherAge를 구하는 코드이다.

var age = 13
var Person = function (){
        var myBrotherAge = age - 5
        console.log(myBrotherAge)
}
Person()

위와 같이 전역변수 age를 Person내부가 아닌 곳에서 사용할 수 없도록 하고 싶다고 하자.

 

var Person = (function (){
    var age = 13
    return (function(){
        var myBrotherAge = age - 5
        console.log(myBrotherAge)
    })
})()
Person()

이때 위와 같이 클로저를 사용하여 해당하는 전역변수 age를 접근할 수 있는 방법이 없어진다. 외부에서 Person객체를 통해 접근해봐도 클로저함수가 반환되기때문에 age는 접근할 수 없게 된다. 

 

루프안에서의 클로저 사용 시 주의점

function countSeconds(howMany) {
  for (var i =1; i <= howMany; i++) {
    setTimeout(function () {
      console.log(i);
    }, i * 1000 );
  }
};
countSeconds(10)

위와 같은 코드를 실행했을때 결과가 어떻게 나올것 같은가?

자칫 찍히는 로그가 1~11까지 1초마다 찍힌다고 생각할 수도 있겠지만 위의 결과는 11이 1초에 한번씩 10번 찍힌다.

첫번째 SetTimeout의 함수가  실행되기전에 루프문이 끝나게 되며, 루프문에 따라 두번째 매개변수인 시간 매개변수엔 i값이 차례로 들어가지만 이후에 setTimeout내부 함수가 참조하는 i는 이미 howMany만큼 증가한 i값을 참조하게 된다. 이미 닫힌 setTImeout함수의 내부 함수인 익명함수는 외부의 i를 계속 추적하기에 생기는 결과인 것이다.

 

만약 10초 동안 1부터 11까지 로그를 찍고싶다면 setTimeout밖에 함수하나를 덧씌운 후 i값을 복제해 사용하면 된다.

function countSeconds(howMany) {
  for (var i =1; i <= howMany; i++) {
    var count = function (current){
        setTimeout(function () {
      console.log(current);
    }, i * 1000 );
  }
  count(i)
    }
    
};
countSeconds(10)

이러면 내부 클로저 함수가 참조하는 값은 계속 변하는 i가 아닌 고정값 current가 되기 때문이다.