JavaScript 7분 읽기

closure

클로저는 자바스크립트의 꽃이라고 할 수 있다. 클로저는 함수와 함수가 선언된 렉시컬 환경의 조합이다.

Closure

클로저는 자바스크립트의 꽃이라고 할 수 있다.

클로저는 함수와 함수가 선언된 렉시컬 환경의 조합이다.

클로저의 정의는 위와 같다. 차근차근 클로저를 알아가보자.

스코프

스코프는 식별자의 유효 범위이다. 유효 범위가 아닌 곳에서는 식별자를 찾을 수 없는 것이다.

function foo(){
  let x = 'foo';
  console.log(x);
}

function bar(){
  console.log(x);
}

bar();

위 함수를 보았을 때 bar가 호출되면, 함수 몸체에서 x라는 변수를 찾게 된다. 이 x라는 변수의 유효 범위는 foo라는 함수 내부이기 때문에 이 외부에서는 해당 변수를 참조할 수 없게 된다. 이러한 범위를 나타내는 것이 스코프다.

렉시컬 스코프

스코프는 알았다. 그렇다면 렉시컬 스코프는 무엇일까? 렉시컬 스코프는 스코프가 정해지는 방식이라고 생각하자. 특히, 함수의 스코프 결정을 위한 것이다.

위에서 스코프는 식별자의 유효 범위라고 했다. 그렇다 함수명도 식별자다. 즉, 함수도 스코프가 있다는 것이다. 그렇다면 함수의 스코프는 어디서 결정될까? 이 의문이 들게 만드는 예시는 아래와 같다.

let x = 10;
function foo(){
  let x = 3;
  bar();
}
function bar(){
  console.log(x);
}
foo();

위 코드를 실행해보면, 결국 bar함수가 호출된 결과가 console.log로 콘솔창에 출력된다. 그 결과를 보기 전에 어떤 값이 나올지 예상해보자. 경우는 크게 두 가지가 있을 수 있다. 전역 x를 출력하는 경우, foo함수의 지역변수 x를 출력하는 경우.

각 경우는 서로 다른 값을 출력한다. 이때, 고려할 것은 스코프를 고려한다. 자신의 스코프에서 x라는 변수가 있으면 그 값을 출력하고, 없으면 상위 스코프에서 다시 찾고, 이런 방식이 계속 될 것이다.

그렇다면, bar의 스코프는 어디일까? 만약 bar의 스코프가 호출된 foo의 스코프를 상위 스코프로 둔다면, x는 3을 출력할 것이다. 그게 아니라 bar의 스코프가 선언된 전역의 스코프를 상위 스코프로 둔다면, x는 10을 출력할 것이다.

결과는 10이 출력된다. 이 뜻은 함수의 스코프는 자신이 호출된 곳이 아닌 정의된 곳에서 결정된다는 것이다.

렉시컬 환경

자바스크립트 엔진은 코드를 실행하기 전에 실행 컨택스트를 만들어 실행 컨택스트 스택에 넣게 된다. 이것이 전역 실행 컨택스트가 된다. 이후 함수를 호출하면 함수의 실행 컨택스트를 생성해 다시 스택에 넣게 된다.

스택이라는 구조는 후입선출의 방식으로 이루어진다. 즉, 나중에 쌓인 것이 먼저 나가는 구조이다. 이를 실행 컨택스트 스택에 적용하면, 가장 최근에 호출된 함수가 가장 먼저 스택을 빠져 나오게 된다. 그렇게 되면 스택의 가장 위에 있는 컨택스트가 현재 실행 중인 실행 컨택스트가 된다.

이 실행 컨택스트에는 여러 내용이 담겨있다. 그 중 하나가 렉시컬 환경이다. 렉시컬 환경은 다시 특정 환경 레코드들을 갖는데, 함수의 경우 단순히 함수 환경 레코드를 갖는다. 여기서 매개변수나 식별자의 호이스팅 등이 이루어져 등록되게 된다.

그리고 외부 렉시컬 환경에 대한 참조를 결정한다. 말이 어렵지만, 내 렉시컬 환경의 외부에 어떤 렉시컬이 있는지 결정한다는 것이다. 그런데 무언가 비슷한 것이 있지 않은가?

맞다. 스코프다. 스코프에서 식별자를 찾을 때 자신의 스코프에 존재하지 않는다면 상위 스코프로 올라가게 된다.

이때 어떤 스코프가 상위 스코프인지 정해 놓은 것이 외부 렉시컬 환경애 대한 참조인 것이다. 그리고 이는 정적으로 결정되는 것이다. 이것이 바로 렉시컬 스코프의 정체인 것이다.

클로저

클로저를 설명하기 위해 위의 내용을 설명했다. 앞의 두 내용은 이전에 포스팅했던 것이지만, 다시 짚은 이유는 클로저를 알기 위해서 반드시 알아야 하는 동작 방식이기 때문이다.

먼저 클로저의 정의를 다시 살펴보자

클로저는 함수와 함수가 선언된 렉시컬 환경의 조합이다.

함수는 알겠다. 렉시컬 환경도 알겠다. 그렇다면 이 조합은 무엇일까? 여기서 렉시컬 환경은 외부 렉시컬 환경에 대한 참조를 포함한다. 즉, 함수 자체와 함수가 참조하고 있는 외부의 세계를 의미하는 것이다.

그렇다면 이게 무슨 의미가 있을까? 어떤 경우에 이 클로저의 개념이 사용되었는지 알아보자.

function foo(){
  const x = 10;
  return function bar(){
    console.log(x);
  }
}
const bar = foo();
bar();

위 코드는 아주 간단히 클로저 개념을 사용했다. 먼저 함수를 보면 foo는 반환값으로 함수를 리턴한다. 이는 자바스크립트의 함수가 일급 객체이기 때문인데, 일급 객체라 함은 값으로 평가 될 수 있는 객체를 의미한다고 생각하면 된다.

즉, 함수는 값이 된다는 것이다. 값이 된다는 의미는 변수에 할당하거나, 반환값이 되거나, 함수의 인자로 넘겨줄 수 있다는 의미이다. 이에 대해서는 함수형 프로그래밍을 공부하고 설명해보겠다.

이런 것을 기억하면 foo함수를 호출하게 되면 받는 반환값으로 함수를 받게 되는 것이고, 그 함수를 const bar에서 bar라는 변수에 할당한 것이다. 그렇다면 bar변수가 가지고 있는 것은 함수이고,( 엄밀히 말해서 함수는 객체이므로 변수에 할당하게 되면 함수 자체가 할당되는 것이 아니라 그 함수의 주소값이 할당된다.) 변수에 함수를 할당한 것이 이상하다고 생각할 수 있지만, 우리는 이미 방식을 공부했다. 바로 함수 표현식이다.

const foo = function(){
  return 'foo';
}

넘어가서, 함수라는 것은 호출이 가능하다는 것이므로, 마지막 코드를 보면 bar()로 호출하고 있다는 것이다. 그렇다면 예시에서 bar를 호출하게 되면 어떤 값이 출력될까?

bar함수에는 console.log를 통해 x를 출력한다. 그런데 그 함수에는 x가 없다. 그렇다면 찾아 가야지. 여기서 렉시컬 스코프를 기억해야 한다. 렉시컬 스코프는 렉시컬 환경이 외부 렉시컬 환경에 대한 참조를 결정해 할당한 것이다. 렉시컬 렉시컬 말이 많다.

그냥 쉽게 생각하면 함수가 처음 정의된 곳에서 상위 스코프로 식별자를 찾아가라는 것이다. 그럼 찾아가 보자. 반환된 함수의 상위 스코프는 foo함수이다. 이 함수에는 지역 변수로 x가 존재하므로 x의 값을 가져온다. 그런데 이상하다. 이 foo함수는 호출되면서 끝난 것이 아닌가?

함수를 설명하면서 변수의 생명 주기에 대해 설명했었다. 변수는 함수가 종료되면 생명이 다한다. 즉, 위 예시코드도 이미 foo함수는 생명이 다했기 때문에, x라는 변수도 생명이 다했을 것이다. 하지만, 결과는 x의 값을 가지고 있다는 것을 알 수 있다. 즉, 함수가 끝났더라도 함수 내부의 지역 변수를 참조하고 있는 어떤 객체가 있다면, 그 값은 계속 참조가 가능한 것이다. 이것이 바로 클로저를 이용한 함수이다.