이번 글은 'Secrets of the JavaScript Ninja'의 'Chapter 5. Functions for the master: closures and scopes'를 바탕으로 작성하였습니다.
핵심 Concepts
- 개발 과정을 간소화하기 위한 클로저 사용법
- 실행 컨텍스트(execution context)를 통한 JavaScript 프로그램의 실행 절차 추적
- 렉시컬 환경(lexical environments)를 통한 변수 스코프(variable scopes) 추적
- 변수 종류에 대한 이해
- 클로저 작동 방법에 대한 탐구
사전 지식 체크!
- 스코프는 각기 다른 변수나 메소드를 몇 개나 가질 수 있으며 종류는 무엇인가?
- 식별자(indentifier)는 어떻게 값을 가지며 추적되는가?
- 수정 가능한 변수는 무엇이며 어떻게 JavaScript에서 정의하는가?
Understanding types of JavaScript variables
JavaScript에는 변수를 정의하는 키워드가 var, let, const 총 3가지 있다. 이들은 2가지 측면에서 다른데 하나는 '가변성(mutability)'이고 다른 하나는 렉시컬 환경과의 관계이다.
Variable mutability
가변성을 기준으로 함수 선언 키워드를 나누면 const와 var, let으로 나눌 수 있다. const로써 선언되는 변수는 불가변적(immutable)인데, 이는 값을 단 한 번만 설정할 수 있다는 것이다. 이와 반대로 var와 let은 전형적인 변수로 필요할 때마다 값을 바꿀 수 있다.
Const variables
const는 선언할 때 설정한 초기값을 향후 바꾸지 못한다는 점만 빼면 일반적인 변수와 같다. 그렇다면 왜 변수(變數)라고 하는 것일까?
const 변수는 아래와 같은 조금 다른 목적을 위해 사용한다.
- 재할당되어서는 안 되는 변수를 특정한다.
- 고정값을 참조한다.
따라서 const 변수를 활용하면 원치 않거나 예기치 않은 변수값 변경을 방지할 수 있음은 물론 JavaScript 엔진 성능을 최적화할 수 있다.


firstConst는 말 그대로 '항수(恒數)'이므로 새로운 값을 할당할 수 없다. 따라서 위 코드를 실행하면 JavaScript 엔진은 firstConst의 값을 'samurai'에서 'ninja'로 수정하지 않고 에러를 출력하게 되는 것이다.
두 번째 const 변수로 secondConst에 빈 객체를 초기값으로 설정했다. 여기서 const의 중요한 특징에 대해 알아보겠다. 앞서 살펴봤듯이 const 변수에는 새로운 값을 할당할 수 없다. 그러나 값을 '수정(modifying)'하는 데는 아무 지장이 없다. 예를 들어 빈 객체에다 아래와 같은 프로퍼티를 추가할 수 있다.

세 번째 const 변수를 보면 알 수 있듯 const 변수가 빈 배열을 참조한다면 해당 배열 역시 얼마든지 수정할 수 있다.
즉 const 변수의 값은 오직 처음에만 설정 가능하고 새로운 값을 나중에 할당할 수 없다. 다만 기존의 값에다 새로운 값을 추가하거나 제거하는 등 수정은 가할 수 있다.
Variable definition keywords and lexical environments
var, let, const 3가지 변수 정의 방법은 렉시컬 환경(다른 말로 스코프)과의 관계를 기준으로도 분류할 수 있다. 이 경우 var와 let, const로 분류한다.
Using the var keyword
var 키워드를 쓸 때 해당 변수는 가장 가까운 함수나 전역 렉시컬 환경에 정의된다.(블록은 무시된다는 점에 유의하자!) 아래 예제를 통해 이 같은 특징에 대해 자세히 알아보자.


위 예제에서 보듯 for 반복문의 내용 안에서 블록 변수(i와 forMessage), 함수 변수(functionActivity), 전역 변수(globalNinja)에 아무 제약 없이 접근 가능하다.
그러나 다른 언어를 쓰다 넘어온 수많은 개발자들을 헷갈리게 하는 점은 코드 블록에 정의된 변수를 해당 블록 밖에서도 접근 가능하다는 점이다.

이는 var로써 정의된 변수는 블록에 상관 없이 가장 가까운 함수나 전역 렉시컬 환경에 등록된다는 사실에서 비롯된다. 아래 예제는 reportActivity 함수 내에서 for 반복문이 두 바퀴를 돌고 난 다음의 상태를 나타낸다.

위 예제에 나오는 렉시컬 환경은 총 3가지이다.
- globalNinja 변수가 등록된 전역 환경
- reportActivity 함수 호출이 생성되는 reportActivity 환경. 변수 functionActivity, i, forMessage를 포함한다.
- for 블록 환경. var로 정의된 변수는 블록 환경을 무시하므로 아무 변수도 포함되어 있지 않다.
이와 같은 var 변수는 헷갈리기 쉽기 때문에 ES6 작업 환경에서는 let과 const를 사용한다.
Using let and const to specify block-scoped variables
가장 가까운 함수나 전역 렉시컬 환경에다 변수를 정의하는 var와 달리 let과 const는 보다 직관적이다. 이 둘은 변수를 블록 환경을 포함한 가장 가까운 렉시컬 환경에다 정의한다. let과 const를 사용하여 위 예제를 고쳐 써보자.

아래 예제는 reportActivity 함수 안의 for 반복문이 두 바퀴 돌고 난 이후의 상태를 나타낸 것이다. 위 예제 코드 역시 전역 환경, reportActivity 환경, 블록 환경 3가지 렉시컬 환경을 갖는다. 허나 앞선 예제와 달리 여기서는 let과 const를 사용했기 때문에 변수들은 각자 가장 가까운 렉시컬 환경에 정의된다. 따라서 변수 i와 forMessage는 for 블록 환경에 속하는 것이다.

Registering identifiers within lexical environments
JavaScript의 강력한 특징 중 하나는 바로 사용하기 쉽다는 점이다. 함수 반환값의 유형을 비롯하여, 함수 매개변수의 유형, 변수의 유형 등을 특정하지 않아도 된다. 그리고 앞서 살펴보았듯 JavaScript는 싱글 스레드 모델이라서 한 줄씩 실행된다는 점도 알 것이다. 이와 관련하여 아래 예제를 살펴보자.

언뜻 봤을 때는 별반 이상이 없어 보이는 코드이다. 허나 잘 살펴보면 check 함수를 정의하기 전에 해당 함수를 호출하고 있음을 볼 수 있다. 함수를 정의하기도 전에 호출했음에도 불구하고 위 코드는 정상적으로 작동한다. 왜 그럴까?
JavaScript는 함수를 정의하는 위치에 대해 까다롭지 않다. 따라서 각 호출 앞이든 뒤든 원하는 곳에 함수 선언을 할 수 있다.
The process of registering identifiers
함수 선언을 아무 데나 할 수 있다는 점은 차치하고, 그렇다면 JavaScript 엔진은 어떻게 'check'라는 함수가 존재한다는 사실을 아는 것일까? JavaScript는 약간의 속임수를 써서 JavaScript 코드를 두 단계로 나눠 실행한다.
- 새로운 렉시컬 환경이 생성될 때마다 활성화된다. 이 단계에서 코드는 아직 실행되지 않는다. 다만 JavaScript 엔진이 현재 렉시컬 환경 내에 정의된 모든 변수와 함수를 조회하여 등록한다.
- 첫 번째 단계가 성공적으로 이루어지고 나서 JavaScript 엔진이 실행된다. 정확한 동작은 변수의 유형과 환경의 유형에 따라 다르다.
자세한 절차는 아래와 같다.


- 만약 함수 환경을 생성한다면 모든 정식 매개변수와 인수의 값과 함께 내재적인 인수 식별자가 생성된다. 함수가 아닌 환경이라면 이 단계는 생략된다.
- 만약 전역 혹은 함수 환경을 생성한다면 현재 코드(다른 함수의 내용까지 가지 않고)는 함수 선언(함수 표현이나 화살표 함수는 아님)을 위해 탐색된다. 발견된 각각의 함수 선언을 위해 새로운 함수는 생성되고 함수의 이름과 함께 환경 내 식별자에 바운딩된다. 만약 식별자 이름이 이미 존재한다면 해당 식별자의 값은 덮어써진다. 블록 환경을 다룬다면 이 단계는 생략된다.
- 현재 코드에서 변수 선언을 탐색한다. 함수 및 전역 환경에서 var 키워드로 선언되고 다른 함수 외부에다 정의된 모든 변수(그러나 블록 내에 배치할 수 있음)를 찾고, 다른 함수 및 블록 외부에서 정의된 let 및 const 키워드로 선언된 모든 변수를 찾는다. 블록 환경에서 코드는 현재 블록에서 직접 let 및 const 키워드로 선언된 변수만을 탐색한다. 발견된 각 변수에 대해 식별자가 환경에 존재하지 않는 경우 식별자가 등록되고 그 값은 undefined으로 설정된다. 식별자가 존재하면 값이 그대로 유지된다.
Calling functions before their declarations
JavaScript를 사용하기 쉽게 하는 특징 중 하나는 함수 정의 순서를 상관하지 않는다는 점이다.

위 예제에서 fun 함수는 정의되기 전에 호출된다. fun 함수는 함수 선언으로 정의되었고 어떠한 JavaScript 코드도 실행되기 전에 현재 렉시컬 환경이 생성될 때 해당 함수의 식별자는 등록되었다. 그러므로 assert를 호출하기 전에 이미 fun 함수는 존재하는 것이다.
함수 표현(function expressions)와 화살표 함수(arrow funtionos)는 이 처리 과정에 포함되지 않으며 이 둘은 정의한 위치에 프로그램이 이르면 그때서야 실행된다.
Overriding functions
JavaScript에서 마주치는 또 하나의 헷갈리는 부분은 바로 함수 식별자가 오버라이딩되는 문제이다. 바로 예제를 살펴보자.

- 첫 번째 assert에서 식별자 fun은 함수를 참조하고, 두세 번째에서는 숫자를 참조한다. 이 같은 작업은 식별자가 등록될 때 일어나는 작업의 결과로서 일어난다.
- 위에 서술된 두 번째 작업 단계는 함수 선언으로 정의된 함수가 생성되고 어떤 코드든 평가되기 전에 그 식별자와 관련된다는 것이다.
- 세 번째 단계로 변수 선언이 처리되고 현재 환경에서 마주치지 않은 식별자와 undefined 값이 연관된다는 것이다.
위 예제 코드에서 식별자 fun은 함수 선언이 등록될 때 두 번째 단계에서 마주치기 때문에 undefined 값이 변수 fun에 할당되지 않는다. 따라서 fun이 함수인지 확인하는 첫 번째 assert가 통과되는 것이다. 그러고 나서 식별자 fun에 숫자 3을 할당한다. 이렇게 함으로써 함수에 대한 참조를 잃고 식별자 fun은 숫자를 참조하게 된다.
실제로 프로그램이 수행되는 동안 함수 선언은 생략되어 fun 함수의 정의는 fun 식별자 값에 아무런 영향도 끼치지 못한다.
변수 호이스팅(variable hoisting)
JavaScript를 공부하다 보면 변수와 함수 선언은 함수의 맨 위나 전역 스코프 위로 끌어올려진다는 개념을 자주 접한다. 변수와 함수 선언은 기술적으로는 어디에도 움직이지 않는다. 이들은 코드가 실행되기 전에 렉시컬 환경에서 조회되고 등록될 뿐이다.
Exploring how closures work
클로저는 함수로 하여금 함수가 생성될 때 스코프 안에 있는 모든 변수들에게 접근하게 만드는 작동 원리이다. 이때까지 클로저를 사용해서 프라이빗 객체 변수를 따라 만들고 콜백을 유려하게 다루는 방법도 살펴보았다.
클로저와 스코프는 뗄래야 뗄 수 없는 관계이다. 클로저는 말 그대로 JavaScript에서 스코프가 작동하는 법칙의 부수 효과이기 때문이다. 이번에는 실행 컨텍스트를 사용했을 때의 이점과 더불어 클로저가 어떻게 작동하는지 알게 해주는 렉시컬 환경에 대해 알아보겠다.
Revisiting mimicking private variables with closures
앞서 살펴봤듯이 클로저로 프라이빗 변수를 따라 만들 수 있다. 아래 예를 통해 자세히 살펴보자.


위 예제를 통해 첫 번째 Ninja 객체가 생성되고 나서 애플리케이션의 상태를 분석해보자. JavaScript 생성자는 new 키워드로써 호출되는 함수이다. 그러므로 생성자 함수를 호출할 때마다 생성자에 대한 변수 지역을 쫓는 새로운 렉시컬 환경을 만든다. 위 예제에서는 feints 변수를 추적하는 새로운 Ninja 환경이 만들어진다.
추가로 함수가 만들어질 때마다 생성된 렉시컬 환경을 유지한다.(내부 [[Environment]] 프로퍼티를 통해서) 위 예제의 경우, Ninja 생성자 함수 안에서 getFeints와 Feint라는 Ninja 환경에 대한 참조를 얻는 두 가지 함수를 만든다. Ninja 환경은 두 함수가 생성된 환경이다.
getFeints와 feint 함수는 새롭게 생성된 ninja 객체의 메소드로 할당된다.(this 키워드로 접근 가능하다) 따라서 getFeint와 feint는 Ninja 생성자 함수 밖에서도 접근 가능하고 이는 결국 feints 변수를 둘러싼 클로저를 효과적으로 생성했다는 뜻이다.

Ninja 생성자와 함께 만들어지는 모든 객체는 생성자가 호출될 때 정의되는 변수 주변을 닫는 자신만의 고유 메소드를 가진다.(ninja1.getFeints 메소드는 ninja2.getFeints 메소드와 다르다) 이러한 '프라이빗' 변수들은 직접적으로는 접근 불가능하고 생성자 안에서 만들어지는 객체 메소드를 통해서만 접근 가능하다.
이제 ninja2.getFeints()를 호출하면 어떤 일이 일어나는지 살펴보자.

ninja2.getFeints() 호출을 만들기 전에 JavaScript 엔진은 전역 코드를 실행한다. 이때 전역 실행 컨텍스트 안에서 실행되는 프로그램은 실행 스택에 들어 있는 유일한 컨텍스트이다. 동시에 단 하나 유효한 렉시컬 환경은 전역 환경이며 이는 전역 실행 컨텍스트와 연관이 있다.
ninja2.getFeints() 호출을 만들 때 ninja2 객체의 getFeints 메소드를 호출한다. 모든 함수 호출은 새로운 실행 컨텍스트를 만들고 새로운 getFeints 실행 컨텍스트는 만들어진 후 실행 스택에 들어간다. 이는 또한 보통 이 함수 내에 정의된 변수를 추적하는 렉시컬 환경의 생성으로 이어진다. 덧붙여 getFeints 렉시컬 환경은 외부 환경으로서 getFeints 함수가 생성된 환경을 획득하고, ninja2 객체가 만들어질 때 NInja 환경은 활성화된다.
그렇다면 feints 변수의 값을 얻으려고 할 때 어떤 일이 일어날까. 첫 번째로 현재 활성화된 getFeints 렉시컬 환경이 생성된다. 아직 어떠한 변수도 getFeints 함수 내에 정의되지 않았기 때문에 이 렉시컬 환경은 텅 비었고 우리의 목표인 feints 변수도 찾을 수 없다. 다음으로 현재 렉시컬 환경의 외부 환경 안에서 탐색이 이어진다. 예제의 경우 ninja2 객체가 만들어질 때 Ninja 환경이 활성화된다. 이번에는 Ninja 환경이 feints 변수에 대한 참조를 갖고 탐색은 끝난다.
Private variables caveat
프라이빗 변수는 객체의 프라이빗 프로퍼티가 아니라 생성자 내 생성된 객체 메소드에 의해 활성화가 유지되는 변수이다.


위 예제 코드는 ninja1.getFeints 메소드를 완전히 새로운 imposter 객체에 할당하는 소스 코드이다. getFeints 함수를 imposter 객체에서 호출할 때마다 ninja1이 인스턴스화될 때 만들어진 변수 feints의 값에 접근할 수 있는지 시험하고 전체 프라이빗 변수를 속이고 있음을 증명한다.
이 예제는 JavaScript 안에 어떠한 프라이빗 객체도 없음을 보여주지만, 객체 메소드에 의해 생성된 클로저를 사용하여 충분히 나은 대안을 가질 수 있다.
Revisiting the closures and callbacks example
앞에서 살펴보았던 간단한 콜백 타이머로 돌아가보자. 이번에는 두 객체에 기능을 부여할 것이다.


animateIt 함수를 호출할 때마다 해당 애니메이션의 중요한 변수 집합(elem, tick, timer)을 추적하는 새로운 함수 렉시컬 환경이 만들어진다. 클로저를 통해 해당 함수의 변수와 함께 적어도 함수 하나가 작동하고 있다면 해당 환경은 활성화 상태를 유지한다. 위 예제의 경우, clearInterval 함수가 호출될 때까지 브라우저는 setInterval 콜백의 활성화를 유지한다. 나중에 인터벌이 끝나고 나면 브라우저는 일치하는 콜백을 부름과 함께 클로저를 통해 콜백이 생성될 때 정의된 변수들이 따라온다. 이는 임의로 콜백을 맵핑하고 변수들을 활성화하는 문제를 회피하게 해주며 따라서 코드 작성을 매우 간결하게 해준다.
요점 정리
- 클로저는 함수가 정의될 때 스코프 안에 있는 모든 변수들에 접근할 수 있게 하는 함수이다. 클로저는 함수의 '안전한 버블'과 함수가 정의되는 시점의 스코프 안에 든 변수들을 만든다. 이 방식에서 함수는 해당 함수가 만들어진 스코프가 사라져도 이미 실행에 필요한 모든 조건을 갖추었다.
- 다음 고급 기능을 위해 클로저를 사용 가능하다.
- 프라이빗 객체 변수 따라 만들기. 메소드 클로저를 통해 생성자 변수를 감싸서 만든다.
- 콜백 다루기
- JavaScript 엔진은 실행 컨텍스트 스택(혹은 콜 스택)을 통해 함수 실행을 추적한다. 함수가 호출될 때마다 새로운 함수 실행 컨텍스트가 만들어지고 스택 위에 쌓인다. 함수 실행이 끝나면 해당 실행 컨텍스트는 스택에서 제거된다.
- JavaScript 엔진은 렉시컬 환경(스코프)로써 식별자를 추적한다.
- JavaScript에서 전역, 함수, 블록 스코프 변수를 정의할 수 있다.
- 변수를 정의하는 데 var, let, const 키워드를 쓴다.
- var 키워드는 가장 가까운 함수나 전역 스코프 안에 변수를 정의한다(블록은 무시)
- let과 const 키워드는 가장 가까운 스코프(블록 포함) 안에 변수를 정의한다
- const는 단 한 번만 값이 할당 가능한 변수를 생성하게 한다.
- 클로저는 JavaScript 스코핑 규칙의 부수 효과이다. 함수가 생성된 스코프가 사라진 지 오래 되더라도 함수를 호출 가능하다.
함께 보기
- 클로저(closures)
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
Closures - JavaScript | MDN
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closur
developer.mozilla.org
https://www.w3schools.com/js/js_function_closures.asp
JavaScript Function Closures
W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, Python, SQL, Java, and many, many more.
www.w3schools.com
- var
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var
var - JavaScript | MDN
The var statement declares a function-scoped or globally-scoped variable, optionally initializing it to a value.
developer.mozilla.org
- const
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
const - JavaScript | MDN
Constants are block-scoped, much like variables declared using the let keyword. The value of a constant can't be changed through reassignment (i.e. by using the assignment operator), and it can't be redeclared (i.e. through a variable declaration). However
developer.mozilla.org
- let
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
let - JavaScript | MDN
The let statement declares a block-scoped local variable, optionally initializing it to a value.
developer.mozilla.org
'👩💻 Programming > JavaScript' 카테고리의 다른 글
미래를 향한 함수_2. 프로미스(promises) from Secrets of the JavaScript Ninja (0) | 2022.04.13 |
---|---|
미래를 향한 함수_1. 제너레이터(generators) from Secrets of the JavaScript Ninja (0) | 2022.04.13 |
함수의 달인이 되자_1. 클로저(closure)와 스코프(scope) from Secrets of the JavaScript Ninja (0) | 2022.04.11 |
프로토타입(Prototype)을 알아보자! (0) | 2022.04.07 |
함수를 좀더 이해해보자! (0) | 2022.04.07 |
댓글