Skip to content

Commit 47ddb8c

Browse files
committed
[B-001] modify: 자바스크립트 엔진 실행 과정으로 이해하는 '호이스팅'과 '클로저'
1 parent a3386af commit 47ddb8c

2 files changed

Lines changed: 52 additions & 11 deletions

File tree

216 KB
Loading

blog/development/javascript-engine-and-hoisting-closure/index.mdx

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ date: 2019-02-22 02:33:46
77
updateDate:
88
deck: 자바스크립트를 처음 접하면, 굉장히 쉬운 사용성에 놀라곤한다. C 언어와 Java 를 접했을때는 매번 컴파일이 필요했는데, 자바스크립트는 지금 이 글을 보고있는 브라우저에 개발자 모드 콘솔창을 켜놓고 바로 코딩이 가능하다는것은 가벼움과 쉬운 사용성을 의미한다. 코드의 실행은 어떻게 자바스크립트 엔진(그 중 V8) 내에서 이뤄지는것인가? 이를 이해한다면 변수와 함수 관계에 해당하는 Hoisting(호이스팅), Closure(클로저) 개념을 단순이 암기가 아닌 원리로 이해할 수 있다.
99
abstract: 우리가 작성한 .js 파일의 실행을 담당하는 자바스크립트 엔진이란게 무엇인지 간단하게 살펴보고, 자바스크립트 엔진이 .js 파일을 어떻게 실행하는지를 이해한다. 그 다음 매번 외웠던 '호이스팅'과 '클로저' 개념을 다시 이해해본다.
10+
embeddedImagesLocal:
11+
- './functions-context-scope-and-lexical-scope.jpeg'
1012
---
1113

14+
import { getImage, GatsbyImage } from 'gatsby-plugin-image';
15+
1216
# 자바스크립트
1317

1418
자바스크립트는 웹 페이지의 세 요소중 하나에 해당한다.
@@ -77,7 +81,9 @@ ASTs, 바이트코드 캐싱에 대한 과정은 본 글의 취지와 맞지않
7781

7882
## Compilation Phase
7983

80-
Compilation Phase 를 이해가 쉽도록 말하자면 **Heap 에 적재하는 과정**이라 할 수 있겠다. Heap 은 앞서 설명하였듯이 함수 실행에 필요한 내부의 파라미터, 변수, 함수들을 (Stack 에 적재하는 언어와 달리) 자바스크립트 엔진이 적재하는 곳이다. 그렇기에 Heap 에는 함수 Context Scope 가 함수의 정의 개수 만큼 쌓인다고 설명하였다. 그리하여 Compilation Phase 는 함수의 정의 단위로 수행되며, 새로운 함수 호출이 발생하면 그 함수에 맞는 Local Execution Scope 영역을 생성하고, 같은 일을 반복한다.
84+
Compilation Phase 를 이해가 쉽도록 말하자면 **Heap 에 적재하는 과정**이라 할 수 있겠다. Heap 은 앞서 설명하였듯이 함수 실행에 필요한 내부의 파라미터, 변수, 함수들을 (Stack 에 적재하는 언어와 달리) 자바스크립트 엔진이 적재하는 곳이다. 자바스크립트 실행할때 얼마나 많은 함수들이 정의되고 호출되는데, **그 많은 함수의 파라미터랑 함수 내 변수가 "하나의 Heap" 에 저장**된다고? 그러면 어떻게 구별할까?
85+
86+
함수가 실행될때(뒤에 배울 Execution Phase) 해당 함수에 대한 Scope 이 생기고, 함수 파라미터 및 함수 내 변수들은 이 Scope 에 컴파일 단계(Compilation Phase)에서 정의 및 적재된다. 파라미터, 변수의 Scope 은 모두 컴파일 단계에 정의되므로 후에 배울 Lexical Scope 이라한다.
8187

8288
- **Compilation Phase** 에선 **변수 선언****함수 선언****Heap 에 적재**한다.
8389

@@ -187,7 +193,7 @@ f(1);
187193
7. `f(1)`**함수 호출**이므로 `f()` 선언 여부를 확인하여 수행
188194
- **Global Scope (window)** 영역에 함수 `f()` 존재
189195
- `f()` 함수 실행을 위해 또 Compilation Phase 및 Execution Phase 이 필요하기에
190-
- Heap 에 **새 Local Execution Scope** 영역을 생성한다.
196+
- Heap 에 `f()` 를 위한 **새 Local Execution Scope** 영역을 생성한다.
191197
- Scope Chain 을 위해 꼭 자신을 호출한 부모 함수 Scope 에 대한 포인터를 갖는다.
192198
- `(hidden) A pointer for previous scope`
193199

@@ -249,7 +255,9 @@ f(1);
249255

250256
## Lexical Scope
251257

252-
프로그래밍 언어는 변수 Scope 가 언제 정의되냐에 따라 Scope 명칭이 다르다. 컴파일 시점이면 **"Static Scope"**, 이는 Lexer, Parser 과정에 정의되는것이기에 **"Lexical Scope"** 라고도 부르고, 런타임 시점이면 **"Dynamic Scope"** 로 부른다. 자바스크립트 실행은 **함수 단위로 컴파일(Compilation Phase) 시점에 변수의 Scope 를 먼저 정의한 뒤에 + 수행(Execution Phase) 절차가 재귀적**으로 이뤄진다. 아래의 예를 보면 함수 `b()`와 변수 `var num = 1;`는 하나의 동일한 `main()` 함수의 Scope 인 **Global Scope (window)**에 정의된다. 그렇기에 함수 `b()`는 언제든지 변수 `var num = 1;`에 접근할 수 있는것이다.
258+
프로그래밍 언어는 변수 Scope 가 언제 정의되냐에 따라 Scope 명칭이 다르다. 컴파일 시점이면 **"Static Scope"**, 런타임 시점이면 **"Dynamic Scope"** 로 부른다. 자바스크립트에선 **컴파일 단계(Execution Phase)**에서 파라미터, 변수 모두 **정의될 때** 그때 위치한 함수의 Scope 에 귀속되기 때문에 **"Lexical Scope"** 라고도 부른다. [Lexical 의미는 무엇인가 만드는 것, 만드는 시점을 뜻하며, 파라미터, 변수, 함수 모두 만들어질때 즉 정의될때 Scope 를 따른다는 점에서 Lexical Scope 이라 부르는것이다.](https://www.freecodecamp.org/news/javascript-lexical-scope-tutorial/) 그래서 Lexical Scope 을 Static Scope 으로 얘기할 수도 있다.
259+
260+
아래의 예를 보면 함수 `b()`와 변수 `var num = 1;`는 하나의 동일한 `main()` 함수의 Scope 인 **Global Scope (window)에서 정의된다**. 그렇기에 함수 `b()`는 언제든지 변수 `var num = 1;`에 접근할 수 있는것이다.
253261

254262
``` Javascript
255263
var num = 1;
@@ -278,7 +286,11 @@ function b() {
278286
a();
279287
```
280288

281-
`a()`의 실행 결과는 `10`이 아닌 `1`이다. 이유는 `function b()`**"어디에서 호출되었는지" = "런타임"**시점이 중요한것이 아니라 **"어디에서 정의되었는지" = "컴파일(렉시컬)"**시점이 중요함을 상기하면 Global Scope 에 정의된 함수 `b()`는 Global Scope 에 정의된 `var num = 1;`을 바라보게 된다. 일반적으로 보편적 프로그래밍 언어인 런타임을 생각하여, `10`이라고 착각하게 되는것이다.
289+
`a()`의 실행 결과는 `10`이 아닌 `1`이다. 이유는 `function b()`
290+
- **"어디에서 호출되었는지" = "런타임"**시점이 중요한것이 아니라
291+
- **"어디에서 정의되었는지" = "컴파일(렉시컬)"**시점이 중요함을 상기하면
292+
293+
Global Scope 에 정의된 함수 `b()`는 Global Scope 에 정의된 `var num = 1;`을 바라보게 된다. 일반적으로 보편적 프로그래밍 언어에선 런타임 시 Scope 을 생각하여, `10`이라고 착각하게 되는것이다.
282294

283295

284296
## Variable Shadowing
@@ -318,7 +330,7 @@ var functionExpression = function(x) { return x } // 함수 표현식
318330

319331
앞서 1) Lexical Scope 와 2) Hoisting 은 우리가 보편적으로 생각하는 방식과 다르기에 실수할것들이 많았다.
320332

321-
1. Lexical Scope + Scope Chain = Function-level Scope = 함수 외부에서 선언한 변수 모두에 접근 가능하다.
333+
1. Function-level Scope(Lexical Scope + Scope Chain) = 함수 외부에서 선언한 변수 모두에 접근 가능하다.
322334
``` javascript
323335
var a = 1
324336

@@ -351,12 +363,16 @@ const, let 의 등장으로 if, for 문과 같은 block-level({}) 단위 변수
351363

352364
## Garbage Collection
353365

354-
자바스크립트에서 매 새 함수를 호출할때마다 Heap 에 함수 단위의 Context Scope 생성됨이 이젠 머릿속에 박혀있을것이다. Context Scope 는 함수 호출에만 유효하기에, 해당 함수의 호출이 끝난다면 해당 함수의 Scope 는 Heap 에서 제거된다. 이를 메모리 청소의 의미로 Garbage Collection 이라 부른다. 자바스크립트 파일 실행이 모두 끝나면 가장 처음에 호출됐던 `main()` 함수도 끝이나고, 이에 Global Scope(Window) 도 사라지게 된다. 이렇게 **자바스크립트는 단순히 함수(포인터)의 Reachability 를 기반으로 Garbage Collection 를 수행**한다. Swift 라던가 Java 는 Reference Count 전략을 통한 Garbage Collection 를 수행하는것과 달리 단순한 전략을 취함만 알면 된다.
366+
<GatsbyImage
367+
alt="Function's Context Scope and Lexical Scope for variables"
368+
image={getImage(props.localImages[0])}/>
369+
370+
자바스크립트에서 매 새 함수를 호출할때마다 Heap 에 함수 단위의 Context Scope 생성됨이 이젠 머릿속에 박혀있을것이다. Context Scope 는 함수 호출에만 유효하기에, 해당 함수의 호출이 끝난다면 해당 함수의 Scope 는 Heap 에서 제거된다. 이를 메모리 청소의 의미로 Garbage Collection 이라 부른다. 자바스크립트 파일 실행이 모두 끝나면 가장 처음에 호출됐던 `main()` 함수도 끝이나고, 이에 Global Scope(Window) 도 사라지게 된다. 이렇게 **자바스크립트는 단순히 함수(포인터)의 Reachability 를 기반으로 Garbage Collection 를 수행**한다. Swift 라던가 Java 는 Reference Count 전략을 통한 Garbage Collection 를 수행하는것과 달리 Mark And Sweep 이라는 단순한 전략을 취함만 알면 된다.
355371

356372

357373
## Closure
358374

359-
Java 의 언어에서는 Class 를 통해 변수를 private 으로 선언하여 Encapsulation 을 이뤄낸다. 외부에서 클래스 내 변수에 접근을 금하며, 변수의 변경은 모두 public 으로 노출된 함수를 통해서만 가능하게 한다. 객체지향 프로그래밍(OOP)뿐만 아니라 행동 주도 개발(DDD)의 필수 개념이 Encapsulation 인데, 애석하게도 Javascript 의 Class 는 Java 의 Class 와 달리 Encapsulation 을 지원하지 않는다. 물론 `_` Prefix 가 붙은 변수를 암묵적으로 private 변수로 판단하는 컨벤션이 있었지만, 어쨌든 Object 의 Prototype 을 콘솔로 찍으면 다 보이게되어서 의미가 없다. 또한 최근 자바스크립트에서 클래스 변수 앞에 `#` Prefix 를 붙이면 유사 Private 처럼 동작함을 알았는데, [`#변수명` 으로 변수가 정의되는것](http://www.gisdeveloper.co.kr/?p=11697)이기에, 근본적인 해결책은 아니다.
375+
Java 의 언어에서는 Class 를 통해 변수를 private 으로 선언하여 Encapsulation 을 이뤄낸다. 외부에서 클래스 내 변수에 접근을 금하며, 변수의 변경은 모두 public 으로 노출된 함수를 통해서만 가능하게 한다. 객체지향 프로그래밍(OOP)뿐만 아니라 행동 주도 개발(DDD)의 필수 개념이 Encapsulation 인데, 애석하게도 Javascript 의 Class 는 Java 의 Class 와 달리 Encapsulation 을 지원하지 않는다. 물론 `_` Prefix 가 붙은 변수를 암묵적으로 private 변수로 판단하는 컨벤션이 있었지만, 어쨌든 Object 콘솔로 찍으면 다 보이게되어서 의미가 없다. 또한 최근 자바스크립트에서 클래스 변수 앞에 `#` Prefix 를 붙이면 유사 Private 처럼 동작함을 알았는데, [`#변수명` 으로 변수가 정의되는것](http://www.gisdeveloper.co.kr/?p=11697)이기에, 이 또한 Object.getOwnPropertySymbols() 를 통해 다 보여서 근본적인 해결책은 아니다.
360376

361377
이때 자바스크립트에서는 Closure 를 사용하면 된다. 함수가 정의되는 Scope 에 따라 해당 함수가 참조할 수 있는 변수가 결정되는 Lexical Scope 를 활용하여, "함수의 정의" 자체를 반환하면 된다. 우선 Encapsulation 을 설명하기 전에 Closure 를 어떻게 정의하는지에 대해 알아보자.
362378

@@ -381,9 +397,9 @@ var closureTest = function() {
381397
};
382398
```
383399

384-
이렇게 정의하는 방식을 Closure 라고 부른다.
400+
이렇게 함수의 정의를 반환하는 방식을 Closure 라고 부른다.
385401

386-
Closure 를 사용하는 방식이었던, Encapsulation 은 여기서 우리가 private 로 가두고 싶은 변수를 정의만하면 된다.
402+
함수가 정의되는 Lexical Scope 에 정의된 변수는 해당 정의된 함수에서 접근이 가능하다.는 점을 이용하여 우리가 private 로 가두고 싶은 변수를 Closure 함수 정의 내부에 정의하면된다. 이를 통해 Encapsulation 을 이뤄낼 수 있다.
387403

388404
```javascript
389405
var closureTest = function() {
@@ -399,7 +415,7 @@ closure(); // output: This is innerFunction
399415
console.log(closure.cannotBeAccessedFromOuter); // output: undefined
400416
```
401417

402-
private 변수는 미공개 변수라고도 표현할 수 있다. 위의 예시는 미공개 변수는 고정된 값이었지만, 이를 상태로 정의할 수도 있다. Java 의 객체지향 프로그래밍에 충실한 방식의 코드 작성 기법이다. 또한 추가로 함수를 **하나**가 아닌 **여러개**를 정의해보자. Closure 설명에 정말 수도없이 인용되는 counter 함수를 정의해보자.
418+
private 변수는 '미공개 변수'라고도 불린다. 위의 예시는 미공개 변수는 고정된 값이었지만, 이를 변경가능한 상태로 정의할 수도 있다. Java 의 객체지향 프로그래밍에 충실한 방식의 코드 작성 기법이다. 또한 추가로 함수를 **하나**가 아닌 **여러개**를 정의해보자. Closure 설명에 정말 수도없이 인용되는 counter 함수를 정의해보자. Closure 는 항상 함수를 반환하는것으로 생각할 수도 있는데, 다음과 같이 객체를 반환할 수 도 있다.
403419

404420
```javascript
405421
var counter = function() {
@@ -427,7 +443,32 @@ counterClosure.show(); // output: 0
427443

428444
`counterClosure` 안의 `count` private 변수를 변경할 수 있는 방법은 함수를 호출하는 방법밖에 없고, 보기 위해서도 함수를 통해서만 볼 수 있다. `count` 변수는 `increase()`, `decrease()`, `show()` 함수를 통한다면 어디서든지 접근가능하다는 뜻이기도하다. 이 말은 즉슨, `count` 변수의 **Reachability**는 언제든 열려있단 뜻이며, 자바스크립트 엔진이 **Garbage Collection** 를 언제 수행해야할지 전혀 알 수 없다는 뜻이기도하다.
429445

430-
**Closure** 는 이와 같이 **Garbage Collection**되지 않는다는 치명적인 단점을 가지고있다. [**Garbage Collection**되게 하기 위해서는 `countCloure` 변수를 `null` 값을 넣어서 변수와 함수 모두에 대한 Refer 를 제거(Reachability 를 삭제)](https://dkje.github.io/2020/09/18/Closure/)하면 된다.
446+
따라서 `counterClosure` 객체는 수행이 모두 끝나더라도, 메모리에서 삭제되지 않는다. `count` 변수는 언제든지 사용될 준비를 하고있다는것의 의미는 `count` 변수는 `counter` 함수 내 존재하지만 global 에 참조되어있다는 의미이기도 하다. 이에 `counter` 함수가 정의된 global scope 가 닫히기 전까지는 계속해서 존재하며, 메모리 누수가 발생하게된다.
447+
448+
**Closure** 는 이와 같이 **Garbage Collection**되지 않는다는 치명적인 단점을 가지고있지만, 해결을 위해선 [**Garbage Collection**되게 하기 위해서는 `countCloure` 변수를 `null` 값을 넣어서 변수와 함수 모두에 대한 Refer 를 제거(Reachability 를 삭제)](https://dkje.github.io/2020/09/18/Closure/)하면 된다.
449+
450+
```javascript
451+
counterClosure = null;
452+
```
453+
454+
다른 방식으로는 재사용성이 없는 함수의 경우엔 IIFE(Immedietely Invoked Function Expression) 를 사용하면 된다.
455+
456+
```javascript
457+
(function hello() { ... })();
458+
```
459+
460+
```javascript
461+
(function makePrivateFunc() {
462+
const message = "private data";
463+
const privateFunc = function () {
464+
console.log(`${message} can also implemented through the IIFE`);
465+
};
466+
privateFunc();
467+
})();
468+
```
469+
---
470+
471+
다음과 같이 자바스크립트 엔진의 동작은 '컴파일 - 실행' 2개 과정으로 이뤄지는것과 함께, 함수의 실행(Context Scope)와 변수의 정의(Lexical Scope)에 대해 간략하게 알아보았다. 더 디테일하게 들어가면 수도없이 복잡해지는데, 웹 어플리케이션 개발자로는 이러한 개념만 알아도 실개발과 면접에 큰 이슈는 없을것이라 생각한다.
431472

432473
---
433474

0 commit comments

Comments
 (0)