JS 이모저모 - Closure

박기범·2021년 12월 22일
0

JavaScript 이모저모

목록 보기
1/3

본 시리즈는 JS를 공부하며 알게된 JS만의 특성이나 프로그래밍 기본기로써 중요한 내용들을 묶어 정리하는 시리즈입니다.
정해져있는 순서는 없으며, 각 항목들이 큰 연관을 갖고있지는 않습니다.
추후에 체계적으로 정리하여 글을 수정할 계획은 있습니다.

클로저, 클로저, 클로저. 그래서 클로저는 알고?

으악! 클로저는 도대체 무엇일까. 인터넷에 클로저를 검색하면 lexical scope, [[environment]], 외부 변수 등등 너무 어려운 말이 많다. 지금부터 이 모든 것들을 알아보자.

lexical environment

클로저를 공부하면서 항상 나오는 말이 바로 lexical scope 혹은 lexical environment이다. 클로저를 사용하면, 자신이 선언된 외부 lexical environment의 변수에 접근할 수 있다고 한다. 그렇다면 이 lexical environment는 대체 무엇인가?

//code start -> [phrase : uninitialized]

let phrase; // -> [phrase : undefiend]

phrase = 'hello'; // -> [phrase : 'hello']
phrase = 'world'; // -> [phrase : 'world']

자바스크립트에서는 현재 실행중이 스코프에 대해 항상 lexical environment라는 객체가 존재한다. 이 객체는 이론적으로 존재하는 객체이며, 개발자가 직접적으로 접근하여 제어할 수는 없다. 다만 자바스크립트 엔진이 JS 코드를 실행할때 이 객체를 참고하여 실행한다.

현재 위 코드에서 phrase는 전역 스코프에 선언되어있다. 즉, 전역 스코프에 대한 lexical environment에 phrase라는 변수가 존재하는 것이다. 자바스크립트는 스크립트 언어이므로, 코드를 위에서부터 한줄씩 읽어 해석한다.

코드 해석을 시작할때 모든 변수가 uninitialized 형태로 lexical environment에 담긴다. (함수는 처음부터 선언된 상태로 담긴다. 이게 함수의 선언 및 정의가 이루어진 부분보다 앞서서 호출해도 제대로 동작하는 이유다.) 그 이후 한줄씩 읽어가며 선언과 초기화를 진행한다. 그 결과, 마지막으로 초기화한 phrase='world' 구문에 의해 phrase 값이 결정되었다.

이제 다음의 예를 보자.

let phrase = 'hello'

function hello(name){		// lexical E of hello	   // outer lexical scpoe
  console.log(phrase, name);	// [name:'john]		=> // [phrase: 'hello']
}

hello('john') // 'hello' 출력

함수가 실행될때는 함수 내부의 lexical environment를 갖고, 함수가 선언된 외부 스코프를 가리키는 lexical environment도 갖게된다. 우선 함수 내부에서 필요한 변수를 찾고, 없으면 다음 외부 lexical environment를 조회하는 방식으로 함수에서 전역변수를 찾는다.

한가지 예를 더 보자.

[[environment]]

function f(){
  const value = 'hello'
  return function(){
    console.log(value)
  }
}
const g = f();
g(); // 'hello' 출력

분명 함수 내부에서 선언되고 초기화된 변수는 함수의 호출이 끝나면 스택에서 제거되어 접근할 수 없게된다. 그런데, 위의 코드에서 g를 실행하면 f를 이미 호출하여 더 이상 접근할 수 없어야 하는 value에 대해 접근이 가능하다. g에는 f의 결과로 return된 함수만 존재하는데 말이다.
lexical environment는 이런 마법같은 일을 가능케 한다.

사실 JS에서 모든 함수는 자신이 생성된 lexical environment를 기억한다. 함수의 숨김 객체인 [[environment]]에 자신이 선언된 외부 스코프의 lexical environment를 저장해두기 때문이다.

따라서, 위 코드의 g.[[environment]]에는 function f()의 lexical environment가 저장되어있어 value에 대한 접근이 가능해지는 것이다.

closure

즉, 클로저란 자신이 가진 스코프 외부의 변수를 기억하고, 그 외부 변수에 접근할 수 있는 함수를 의미한다. 누군가 '그래서 클로저가 무엇인가요?' 라고 물으면 이 정의를 대답한 후에, lexical environment와 [[environment]] 숨김 객체를 언급하며 JS의 모든 함수는 클로저가 된다고 설명하면 되겠다.

예제

간단한 예제를 보자.

아래 코드의 실행 결과는 어떻게 될까?

코드를 읽고 잠시 생각한 뒤에 글을 마저 읽도록 하자.

let x = 1;

function func() {
  console.log(x); // ?

  let x = 2;
}

func();

답이 궁금하겠지만 참아라.

충분히 생각했다고?

거짓말치지 말아라.

조금만 더 생각해보자.

그럼 이제 정답을 확인하자.

정답은 에러 이다.

하나의 lexical environment가 생성되는 과정을 기억하는가? 해당 스코프가 시작할때, lexical environment가 생성되며 그 스코프 내부의 변수와 함수 정보를 저장한다. 이 때, 변수는 uninitialized 형태로, 함수는 정의된 형태로 저장된다.

function f() 스코프를 보아라. 이곳에는 x가 지역 변수로 선언되어 있다. 따라서 function f() 가 시작되는 순간, 그 스코프의 lexical environment에 x : uninitialized로 저장된다.

그 뒤에 console.log()x를 찾는다. 물론 전역 스코프에도 x가 존재하지만, 함수에서 다른 변수에 접근할 시 우선 자신의 lexical environment를 먼저 탐색한다. 즉, function f() 자기 자신의 x를 먼저 찾는 것이다. 안타깝게도 이 x는 uninitialized 형태로 lexical environment에 존재한다. 즉, 에러가 나게 된다.

예제를 하나 더 보자.

sum(1)(2) = 3
sum(5)(-1) = 4

위 코드가 정상적으로 수행되도록 sum 함수를 만들 수 있겠는가?
잠시 생각한뒤에 글을 마저 읽도록 하자.

답이 궁금하겠지만 참아라.

충분히 생각했다고?

거짓말치지 말아라.

조금만 더 생각해보자.

그럼 이제 답을 확인하자.


function sum(a){
  return function(b){
  	return a+b;
  }
}

단순하지 않은가? 클로저를 사용하면 이런것들이 가능해진다. 이 함수에서 return 되는 함수는 a의 값을 외부 lexical environment에서 가져오게 된다.

마무리

  • 클로저란?
    • 클로저는 자신의 스코프 외부의 변수를 기억하며, 이 변수에 접근할 수 있는 함수를 의미한다. 이게 가능한것은 JS의 lexical environment와 [[environment]] 숨김 객체 덕분이다. lexical environment는 모든 스코프에 대해 존재하며, 이곳에 해당 스코프의 변수와 함수 정보가 저장된다. 그리고 JS의 모든 함수는 [[environment]] 숨김 객체를 가지는데, 이 객체는 그 함수가 생성된 외부 스코프의 lexical environment를 기억하게 된다. 이를 통해 함수에서 반환된 함수는 자신을 반환한 함수의 lexical environment를 기억하고 있고, 자기 자신이 존재하는한 언제나 외부 스코프의 변수에 접근할 수 있다. 따라서, 모든 JS 함수는 클로저이다.

참고자료

https://ko.javascript.info/closure

profile
원리를 좋아하는 개발자

0개의 댓글