[javascript] hoisting, scope, closure

javascript

이번 포스트는 호이스팅(hosting), 스코프(scope), 클로저(closure)에 대한 개념을 다루도록 하겠습니다.

  1. 호이스팅(hoisting)
  2. 스코프(scope)
  3. 클로저(closure)

위의 세 가지는 es5 spec에서 발생하는 이슈들이며, es6에서 이것을 어떻게 해결하였는지를 기준으로 다루도록 하겠습니다.


호이스팅(hoisting)

네이버 사전에 hoisting이라는 단어를 검색하면 다음과 같은 결과가 나옵니다.

Hoisting : 끌어올리기, 들어올려 나르기

"끌어올리다" 라는 게 호이스팅인데, 과연 무엇을 끌어올리는 걸까요? 다음 코드를 살펴봅시다.

console.log(a)
var a = 10
// 결과는 undefined가 나옵니다.

C나 Java의 경우, 이런 형태로 코드를 작성하면 error가 발생합니다. 하지만 js의 경우 단지 undefined 가 출력 될 뿐입니다.

호이스팅의 또다른 문제점은 다음과 같습니다.

var a = 1
var c = function () {
  var b = 2
  console.log(a) // result : undefined
  var a = 2
  console.log(a) // result : 2
}
c()

어째서 이런 결과가 발생할까요?

사실 위의 코드들은 runtime(처리하는 과정)에서 다음과 같이 해석됩니다.

// 첫번째 경우
var a
console.log(a)
a = 10
// 두번째 경우
var a = 1
var c = function () {
  var b = 2, a
  console.log(a) // result : undefined
  a = 2
  console.log(a) // result : 2
}
c()

무엇을 "끌어올렸는지" 알 것 같나요? 뒤에 선언 된 변수를 맨 위로 끌어올리는 현상을 "호이스팅"이라고 합니다. 어느 시점에 변수를 선언 하든, 이것을 처리하는 과정에서 최 상단으로 변수를 끌어올리는 현상입니다.

앞서 언급했지만, 이러한 호이스팅의 문제점은 error가 발생하지 않는 데 있습니다.

이러한 문제를 ECMAScript6+ 에서는 어떻게 처리할까요?

console.log(a)
const a = 10
// [error] Uncaught SyntaxError: Identifier 'a' has already been declared

ES6+에서는 변수를 선언할 때 두 가지 keyword를 사용합니다.

  • let : 변하는 값
  • const : 변하지 않는 값 (고정)

let과 const로 선언하면 hosting이 발생하지 않게 됩니다. 즉, 변수를 선언하지 않고 사용하면 정상적으로 error가 발생하도록 구성되었습니다.

그리고 var의 경우 window 객체에 할당되지만, let과 const의 경우 window 객체에 할당 되지 않습니다.

var a = 10
const b = 20
let c = 30
console.log(window.a, window.b, window.c) // 10 undefined undefined
console.log(a, b, c) // 10 20 30




스코프(scope)

scope라는 단어를 네이버 사전에서 검색하면 다음과 같은 결과가 나옵니다.

1. (무엇을 하거나 이룰 수 있는) 기회

2. (주제조직활동 등이 다루는) 범위

이 포스트에서 의미하는 scope는 "변수를 사용할 수 있는 범위" 입니다. 다음 코드를 살펴봅시다.

for (var i = 0; i < 5; i ++){}
console.log(i)
// result : 5

C나 java 같은 경우 block 단위 스코프를 사용합니다. 하지만 ES5의 경우, function 단위의 scope를 사용하고 있습니다. 따라서 for 안에 선언된 i는 for이 끝난 뒤에도 살아있는 변수로 남아있습니다.

var a = function () {
  var b = 10
  console.log(b) // result : 10
}
console.log(b) // result : undefined

function 내부에 선언 된 b는, function 바깥에서 사용할 수 없습니다. 이러한 현상을 scope 라고 합니다.

자 그럼 이게 어째서 문제가 되는 것일까요? 여러 개의 js 파일이 있다고 생각해봅시다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="a.js"></script>
  <script src="b.js"></script>
  <script src="c.js"></script>
</head>
<body>
</body>
</html>

편의상 js는 하나의 코드 블럭으로 살펴보겠습니다.

// a.js
var a = 10
console.log(a) // 10
function aa () { console.log(a) }

// b.js
var a = 20
console.log(a) // 20
function bb () { console.log(a) }

// c.js
var a = 30
console.log(a) // 30
function cc () { console.log(a) }
aa() // 30
bb() // 30
cc() // 30

위의 코드는 결과적으로 30이라는 값을 출력합니다. 이러한 문제점을 해결하기 위해서 es5에는 "즉시 실행 함수" 라는 것을 사용합니다.

// a.js
(function (){
  var a = 10
  console.log(a) // 10
  function aa () { console.log(a) }
  aa() // 10
}());

// b.js
(function (){
  var a = 20
  console.log(a) // 20
  function bb () { console.log(a) }
  bb() // 20
}());

// c.js
(function (){
  var a = 30
  console.log(a) // 30
  function cc () { console.log(a) }
  cc() // 30
}());

aa() // [error] aa is not function
bb() // [error] bb is not function
cc() // [error] cc is not function

즉시 실행 함수를 통하여 scope 영역을 제한하고, 다른 파일로 변수나 함수가 전파 되지 않도록 관리하는 것입니다. 이렇게 하면 알 수 없는 변수와 함수로 인해 발생하는 오류가 없어지게 됩니다.

여태까지 es5 spec에서의 scope에 대해 살펴봤습니다. 이제 es6+의 scope에 대해 살펴보도록 하겠습니다. es6+의 경우 scope를 block 단위로 제한합니다. es6+의 scope를 사용하는 방법은 let과 const로 변수를 선언하는 것입니다.

for (let i = 0; i < 5; i ++) {}
console.log(i) // [error] Uncaught ReferenceError: i is not defined

다음과 같이 사용할 수도 있습니다.

{
  let a = 10
  console.log(a) // 10
}
console.log(a) // [error] Uncaught ReferenceError: a is not defined

블록 단위 스코프의 장점은 모듈화에 있습니다.

// a.js
{
  const a = 10
  console.log(a) // 10
  function aa () { console.log(a) }
  aa() // 10
}

// b.js
{
  const a = 20
  console.log(a) // 20
  function bb () { console.log(a) }
  bb() // 20
}

// c.js
{
  const a = 30
  console.log(a) // 30
  function cc () { console.log(a) }
  cc() // 30
}

aa() // [error] aa is not function
bb() // [error] bb is not function
cc() // [error] cc is not function

es5에서 사용하는 scope보다 훨씬 명확하고 간단합니다.

이제 scope를 이용하여 모듈화를 해보겠습니다.
javascript는 namespace를 이용하여 모듈화할 수 있습니다.

// a.js
const module = {}
{
  const a = 10
  const b = 20
  const c = function () {}
  module.a = {a: a, b: b, c: c}
}
module.a = module.a || {}

// b.js
{
  const a = 10
  const b = 20
  const c = function () {}
  module.b = {a: a, b: b, c: c}
}
module.b = module.b || {}

// c.js
{
  const a = 10
  const b = 20
  const c = function () {}
  module.c = {a: a, b: b, c: c}
}
module.c = module.c || {}
module.d = module.d || {}
console.log(module)
/* result
  a: {a: 10, b: 20, c: ƒ}
  b: {a: 10, b: 20, c: ƒ}
  c: {a: 10, b: 20, c: ƒ}
  d: {}
*/

[tip]

const a = a || b 연산의 경우, a가 false면 b를 할당하고, 아니면 a를 할당한다.


위와 같은 방법으로 js 모듈화를 할 수 있습니다. 최근 스펙에는 export와 import가 추가되었으나, 아직 보편적으로 사용하기에는 지원하는 브라우저가 없기 때문에 당분간은 이러한 방식으로 사용하거나 webpack을 사용하여 모듈화를 할 수 있습니다. webpack에 대한 내용도 나중에 다루도록 하겠습니다.


클로저(closure)

클로저는 뭐라고 정의하기 어려운 개념입니다. 그래서 코드로 살펴보겠습니다.

클로저에 들어가기 이전에 !

javascript는 객체를 1급 시민으로 취급한다. 뭔지 잘 모를 것이다. 쉽게 말해, 변수에다가 함수를 할당할 수 있으며, 함수가 함수를 반환할 수 있다. 그리고, 함수의 인자로 함수를 받아 사용할 수 있으며, 이것이 나중에 callbakc 함수로 이용되기도 한다.

참고 : 1급시민, 1급객체, 1급함수

function a () {
  var b = 0
  return function () {
    console.log(++b)
  }
}
var c = a()
c() // result : 1
c() // result : 2
c() // result : 3
c() // result : 4

a 내부에 선언된 변수 b가 죽지 않고 계속 살아 있습니다.
클로저는 이와 같이 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수를 의미합니다.

클로저의 장점

  1. 함수를 호출할 때마다 기존에 생성했던 값을 유지할 수 있다. 즉, 변수를 재활용할 수 있다.
  2. 외부에 해당 변수(참조하고 있는 변수)를 노출시키지 않는다. 전역적으로 노출 되고 있진 않지만, 전역적인 방식으로 사용될 수 있다는 것이다. 쉽게 말해 코드의 안정성을 보장한다.

클로저의 단점

  1. 클로저에 할당 된 변수는, 프로그램이 종료될 때 까지 메모리에 남아있다.

즉, 클로저를 통하여 변수를 생성하면 재활용을 할 수 있으나, 클로저를 지나치게 많이 사용하면 메모리가 낭비될 수 있다는 것이다.

클로저에 대한 개념을 정확하게 이해하기 위해서는

  1. 변수의 스코프
  2. 변수의 생명주기
  3. 가비지 컬렉터(쓰레기 변수 수집기)
  4. 1급시민

등의 개념을 알고 있어야 한다.