Hoisting

호이스팅(Hoisting)은 JavaScript 엔진이 코드를 실행하기 전에 변수, 함수, 클래스 또는 임포트의 선언부를 스코프의 최상단으로 끌어올리는 것처럼 동작하는 JavaScript의 메커니즘으로, 이름 그대로 ‘끌어올린다(hoist)‘는 의미를 가진다.
그러나 실제로 코드가 물리적으로 재배치되는 것은 아니다. 이는 JavaScript 엔진의 컴파일 과정에서 일어나는 일종의 추상적 개념이다.

호이스팅은 JavaScript의 핵심 메커니즘 중 하나로, 코드의 실행 방식에 영향을 미친다.
효과적인 JavaScript 개발자가 되기 위해서는 호이스팅의 개념과 다양한 선언 방식에 따른 차이점을 이해하는 것이 중요하다.

모던 JavaScript에서는 letconst를 사용하여 변수를 선언하고, 함수 표현식을 활용하는 것이 호이스팅으로 인한 예기치 않은 동작을 방지하는 데 도움이 된다. 또한, 모든 변수를 스코프의 최상단에서 선언하는 습관을 들이면 호이스팅으로 인한 혼란을 최소화할 수 있다.

호이스팅의 역사와 배경

호이스팅은 JavaScript가 처음 설계될 때부터 존재했던 특성이다.
이는 Brendan Eich가 10일 만에 JavaScript를 만들면서 다른 언어(특히 Scheme)에서 영향을 받았기 때문이다.
함수의 호이스팅은 프로그램 구조를 더 유연하게 만들고, 상호 재귀적인 함수를 쉽게 정의할 수 있게 한다.
그러나 var의 호이스팅 동작은 많은 혼란을 야기했고, ES6에서 letconst를 도입할 때 TDZ 개념을 함께 도입하여 더 예측 가능한 동작을 제공하게 되었다.

호이스팅이 작동하는 방식

JavaScript 코드가 실행되기 전에 JavaScript 엔진은 두 가지 단계를 거친다:

  1. 생성 단계(Creation Phase): 이 단계에서 엔진은 코드를 스캔하여 변수와 함수의 선언을 메모리에 저장한다.
  2. 실행 단계(Execution Phase): 이 단계에서 코드가 실제로 한 줄씩 실행된다.
    호이스팅은 생성 단계에서 발생하는 현상으로, 선언된 것들이 메모리에 저장되어 마치 코드의 최상단으로 ‘끌어올려진’ 것처럼 동작한다.

변수 호이스팅

변수 호이스팅은 변수 선언이 해당 스코프의 최상단으로 끌어올려지는 현상이다.
변수 호이스팅은 사용된 선언 키워드(var, let, const)에 따라 다르게 동작한다.

Var 변수의 호이스팅

  • var로 선언된 변수는 함수 스코프를 가진다.
  • 선언과 초기화가 동시에 이루어지며, 초기 값은 undefined이다.
  • 선언 이전에 변수를 참조하면 undefined를 반환한다.
1
2
console.log(varVariable); // undefined (에러가 아님)
var varVariable = "Hello, var!";

위 코드는 내부적으로 다음과 같이 해석된다:

1
2
3
var varVariable; // 선언이 최상단으로 호이스팅되고, undefined로 초기화됨
console.log(varVariable); // undefined
varVariable = "Hello, var!"; // 실제 할당은 원래 위치에서 실행

Let과 Const 변수의 호이스팅

  • letconst로 선언된 변수는 블록 스코프를 가진다.
  • 선언은 호이스팅되지만, 초기화는 실제 코드에 도달했을 때 이루어진다.
  • 선언 이전에 변수를 참조하면 ReferenceError가 발생한다.  (일시적 사각지대, Temporal Dead Zone, TDZ)
1
2
3
4
5
// console.log(letVariable); // ReferenceError: letVariable is not defined
let letVariable = "Hello, let!";

// console.log(constVariable); // ReferenceError: constVariable is not defined
const constVariable = "Hello, const!";

TDZ는 변수가 선언된 위치부터 해당 선언이 실행되기 전까지의 코드 영역을 의미한다.
이 영역에서 변수에 접근하려고 하면 ReferenceError가 발생한다.

내부적으로 이를 시각화하면:

1
2
3
4
5
// 호이스팅은 되지만 초기화되지 않음 (TDZ 시작)
// letVariable 선언은 인식되나 접근 불가
console.log(letVariable); // ReferenceError: letVariable is not defined
let letVariable; // TDZ 종료, undefined로 초기화
letVariable = "Hello, let!"; // 값 할당

블록 스코프 내의 letconst:

1
2
3
4
5
6
7
8
{
  // TDZ 시작
  // console.log(blockVar); // ReferenceError
  let blockVar = "블록 스코프 변수";
  console.log(blockVar); // "블록 스코프 변수"
} // 블록 스코프 종료

// console.log(blockVar); // ReferenceError: blockVar is not defined

함수 호이스팅

함수는 선언 방식에 따라 호이스팅 동작이 다르다.

함수 선언문(Function Declaration)의 호이스팅

함수 선언문은 전체가 호이스팅된다. 이는 함수를 선언하기 전에 호출할 수 있다는 것을 의미한다.

1
2
3
4
5
sayHello(); // "Hello, World!" (에러 없이 작동)

function sayHello() {
  console.log("Hello, World!");
}

함수 내부의 중첩 함수 선언문도 호이스팅된다:

1
2
3
4
5
6
7
function outer() {
  inner(); // "내부 함수입니다."
  
  function inner() {
    console.log("내부 함수입니다.");
  }
}

함수 표현식(Function Expression)의 호이스팅

함수 표현식은 변수의 호이스팅 규칙을 따른다.
var로 선언된 함수 표현식은 undefined로 초기화되지만, 함수 자체는 호이스팅되지 않는다.

1
2
3
4
5
6
console.log(varFunction); // undefined (함수가 아님)
varFunction(); // TypeError: varFunction is not a function

var varFunction = function() {
  console.log("I'm a function expression using var");
};

let이나 const로 선언된 함수 표현식은 TDZ의 영향을 받는다:

1
2
3
4
5
6
// console.log(letFunction); // ReferenceError: letFunction is not defined
// letFunction(); // ReferenceError: letFunction is not defined

let letFunction = function() {
  console.log("I'm a function expression using let");
};

화살표 함수(Arrow Function)의 호이스팅

화살표 함수도 함수 표현식과 마찬가지로 변수의 호이스팅 규칙을 따른다:

1
2
3
4
5
6
// console.log(arrowFunction); // 변수 선언 방식에 따라 다름
// arrowFunction(); // 변수 선언 방식에 따라 다름

const arrowFunction = () => {
  console.log("I'm an arrow function");
};

클래스 호이스팅

ES6에서 도입된 클래스도 호이스팅의 영향을 받지만, letconst처럼 TDZ가 적용된다:

클래스 선언문 호이스팅

1
2
3
4
5
6
7
8
9
// const instance = new MyClass(); // ReferenceError: Cannot access 'MyClass' before initialization

class MyClass {
  constructor() {
    this.property = "값";
  }
}

const instance = new MyClass(); // 정상 작동

클래스 표현식 호이스팅

클래스 표현식도 변수 호이스팅 규칙을 따른다:

1
2
3
4
5
6
7
8
9
// const instance = new MyClassExpression(); // ReferenceError: Cannot access 'MyClassExpression' before initialization

const MyClassExpression = class {
  constructor() {
    this.property = "값";
  }
};

const instance = new MyClassExpression(); // 정상 작동

임포트 호이스팅 (Import Hoisting)

ES6 모듈의 import 문도 호이스팅된다. 모듈 내에서는 import 선언이 코드 최상단으로 호이스팅된다.

1
2
3
4
// myFunction(); // 이 함수는 import된 함수로, 호이스팅된 import 덕분에 사용 가능

// import와 export는 모듈의 최상단으로 호이스팅됨
import { myFunction } from './myModule.js';

블록 레벨 함수 호이스팅 (Block-level Function Hoisting)

ES6 이전에는 함수 선언문이 블록 내에 있을 때의 동작이 브라우저마다 달랐다. ES6부터는 엄격 모드(‘use strict’)에서 블록 레벨 함수가 블록 스코프 내로 호이스팅된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
'use strict';

{
  // 블록 내에서만 사용 가능
  function blockFunc() {
    return "블록 내 함수";
  }
  
  console.log(blockFunc()); // "블록 내 함수"
}

// console.log(blockFunc()); // ReferenceError: blockFunc is not defined

비엄격 모드에서는 블록 레벨 함수가 함수 스코프나 전역 스코프로 호이스팅되는데, 이 동작은 명확히 정의되어 있지 않아 권장되지 않는다.

호이스팅의 실제 사례와 문제점

호이스팅은 때때로 예상치 못한 동작과 버그를 유발할 수 있다. 몇 가지 실제 사례를 살펴보자:

함수와 변수 이름 충돌

같은 이름의 변수와 함수가 선언될 때 호이스팅이 어떻게 작동하는지 살펴보자:

1
2
3
4
5
6
7
8
console.log(typeof myFunction); // "function" (함수 선언이 우선함)
var myFunction = "I am a string";

function myFunction() {
  return "I am a function";
}

console.log(typeof myFunction); // "string" (변수 선언이 함수 선언을 덮어씀)

이 경우 변수 선언이 함수 선언을 덮어써서, myFunction은 함수가 아닌 문자열이 된다.

조건부 함수 선언

조건부 함수 선언은 호이스팅에 예측하기 어려운 영향을 미친다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if (true) {
  function conditionalFunc() {
    return "조건 A";
  }
} else {
  function conditionalFunc() {
    return "조건 B";
  }
}

// 결과는 브라우저 및 JavaScript 엔진에 따라 다를 수 있음
console.log(conditionalFunc()); // 일반적으로 "조건 A"이지만 명확한 표준이 없음

이러한 코드는 브라우저마다 다른 결과를 보일 수 있으며, 명확한 표준이 없어 피해야 한다.
대신 함수 표현식을 사용하는 것이 좋다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let conditionalFunc;

if (true) {
  conditionalFunc = function() {
    return "조건 A";
  };
} else {
  conditionalFunc = function() {
    return "조건 B";
  };
}

변수 가려짐(Variable Shadowing)

1
2
3
4
5
6
7
8
9
var x = 10;

function test() {
  console.log(x); // undefined (지역 변수 x가 호이스팅됨)
  var x = 20;
  console.log(x); // 20
}

test();

이 코드에서 함수 내부의 var x는 호이스팅되어, 전역 변수 x를 가리키는 것이 아니라 함수 스코프 내의 undefined 값을 출력한다.

호이스팅의 내부 메커니즘

JavaScript 엔진이 코드를 어떻게 실행하는지 이해하려면 실행 컨텍스트(Execution Context)와 렉시컬 환경(Lexical Environment)이라는 두 가지 개념을 이해해야 한다.
이 두 메커니즘은 JavaScript의 스코프, 클로저, 호이스팅과 같은 중요한 특성의 기반이 된다.

실행 컨텍스트의 생성 단계에서 환경 레코드에 변수와 함수 선언이 등록되기 때문에 변수와 함수 선언이 코드의 최상단으로 “끌어올려진” 것처럼 동작하도록 한다.

1
2
3
4
5
6
7
console.log(a); // undefined (에러가 아님)
var a = 5;

sayHello(); // "Hello!" (정상 작동)
function sayHello() {
  console.log("Hello!");
}

위 코드에서 aundefined로 출력되지만 에러가 발생하지 않는다. 이는 var 선언이 호이스팅되어 환경 레코드에 등록되고 undefined로 초기화되기 때문이다.
마찬가지로 sayHello 함수도 호이스팅되어 선언 전에 호출할 수 있습니다.

letconst의 경우는 다르다:

1
2
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 5;

letconst도 호이스팅되지만, 초기화되기 전에는 “일시적 사각지대(Temporal Dead Zone)“에 있어서 접근할 수 없다. 이는 변수가 렉시컬 환경에 등록되지만 아직 초기화되지 않았음을 의미한다.

실행 컨텍스트(Execution Context)

실행 컨텍스트는 JavaScript 코드가 평가되고 실행되는 환경을 추상화한 개념이다. 쉽게 말해, JavaScript 엔진이 코드를 실행하기 위해 필요한 모든 정보를 담고 있는 환경이라고 생각할 수 있다.

  • 코드를 실행하는 데 필요한 모든 정보를 담고 있는 객체이다.
  • 스코프, 호이스팅, this, 함수, 클로저 등의 동작 원리를 포함한다.
  • 콜 스택에 쌓이며, 가장 위에 있는 컨텍스트와 관련된 코드가 실행된다.
  • 함수가 실행될 때마다 새로운 실행 컨텍스트가 생성된다.

JavaScript 엔진은 코드를 실행하기 위해 다음과 같은 세 가지 유형의 실행 컨텍스트를 생성한다:

  1. 전역 실행 컨텍스트(Global Execution Context): 코드가 처음 실행될 때 생성되는 기본 컨텍스트이다. 브라우저에서는 window 객체가, Node.js에서는 global 객체가 이에 해당한다.
  2. 함수 실행 컨텍스트(Function Execution Context): 함수가 호출될 때마다 해당 함수에 대한 새로운 실행 컨텍스트가 생성된다.
  3. Eval 실행 컨텍스트(Eval Execution Context): eval() 함수 내에서 실행되는 코드에 대한 컨텍스트이다. 현대 JavaScript에서는 보안상의 이유로 잘 사용되지 않는다.
실행 컨텍스트의 구성 요소

실행 컨텍스트는 다음과 같은 주요 구성 요소를 가진다:

  1. 렉시컬 환경(Lexical Environment): 변수와 함수 선언을 저장하는 곳이다.
  2. 변수 환경(Variable Environment): ES6부터 도입된 개념으로, 기본적으로 렉시컬 환경과 같지만 var 선언만 저장한다.
  3. this 바인딩(This Binding): 현재 컨텍스트에서 this 키워드가 참조하는 값을 결정한다.
  4. 외부 환경 참조(Outer Environment Reference): 외부 렉시컬 환경에 대한 참조이다.
실행 컨텍스트 스택(Execution Context Stack)

JavaScript 엔진은 실행 컨텍스트 스택(호출 스택이라고도 함)을 사용하여 코드 실행 순서를 관리한다. 이 스택은 LIFO(Last In, First Out) 원칙에 따라 작동한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function first() {
  console.log('첫 번째 함수');
  second();
}

function second() {
  console.log('두 번째 함수');
  third();
}

function third() {
  console.log('세 번째 함수');
}

first();

위 코드가 실행될 때 실행 컨텍스트 스택은 다음과 같이 변화한다:

  1. 전역 실행 컨텍스트가 스택에 추가된다.
  2. first() 함수가 호출되면 first 실행 컨텍스트가 스택에 추가된다.
  3. first 내부에서 second()가 호출되면 second 실행 컨텍스트가 스택에 추가된다.
  4. second 내부에서 third()가 호출되면 third 실행 컨텍스트가 스택에 추가된다.
  5. third 함수가 완료되면 해당 컨텍스트가 스택에서 제거된다.
  6. second 함수가 완료되면 해당 컨텍스트가 스택에서 제거된다.
  7. first 함수가 완료되면 해당 컨텍스트가 스택에서 제거된다.
  8. 마지막으로 전역 실행 컨텍스트가 남는다(프로그램이 종료될 때까지).
실행 컨텍스트의 생성 과정

실행 컨텍스트는 두 단계를 거쳐 생성된다:

  1. 생성 단계(Creation Phase):
    • 렉시컬 환경 컴포넌트 생성
    • 변수 환경 컴포넌트 생성
    • this 바인딩 결정
  2. 실행 단계(Execution Phase):
    • 코드를 한 줄씩 실행
    • 변수에 값 할당

렉시컬 환경(Lexical Environment)

렉시컬 환경은 JavaScript 코드에서 변수와 함수가 어디에서 어떻게 접근 가능한지를 정의하는 구조이다.
이는 식별자(변수명, 함수명 등)와 해당 식별자가 참조하는 값 사이의 연관 관계를 저장한다.

렉시컬 환경의 구성 요소

렉시컬 환경은 다음 두 가지 주요 구성 요소로 이루어져 있다:

  1. 환경 레코드(Environment Record): 현재 스코프 내의 변수, 함수 선언 등을 저장하는 저장소이다. 두 가지 유형이 있다:

    • 선언적 환경 레코드(Declarative Environment Record): 변수, 함수 선언, 매개변수 등을 저장한다.
    • 객체 환경 레코드(Object Environment Record): 전역 코드의 경우 전역 객체(window, global)의 프로퍼티를 저장한다.
  2. 외부 렉시컬 환경 참조(Outer Lexical Environment Reference): 외부 스코프(상위 스코프)의 렉시컬 환경을 참조한다. 이를 통해 스코프 체인이 형성된다.

렉시컬 스코핑(Lexical Scoping)

JavaScript는 렉시컬 스코핑(정적 스코핑)을 사용한다. 이는 변수의 스코프가 코드를 작성하는 시점(즉, 어디에 작성되었는지)에 결정된다는 의미이다. 이것이 바로 ‘렉시컬(lexical)‘이라는 용어가 사용되는 이유이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const globalVariable = "전역 변수";

function outerFunction() {
  const outerVariable = "외부 함수 변수";
  
  function innerFunction() {
    const innerVariable = "내부 함수 변수";
    console.log(innerVariable);    // "내부 함수 변수"
    console.log(outerVariable);    // "외부 함수 변수"
    console.log(globalVariable);   // "전역 변수"
  }
  
  innerFunction();
}

outerFunction();

위 코드에서 innerFunctionouterVariableglobalVariable에 접근할 수 있는 이유는 렉시컬 스코핑 때문이다. innerFunction의 렉시컬 환경은 외부 렉시컬 환경 참조를 통해 outerFunction의 렉시컬 환경에 접근할 수 있고, 이어서 전역 렉시컬 환경에도 접근할 수 있다.

실행 컨텍스트와 렉시컬 환경의 상호작용

실행 컨텍스트와 렉시컬 환경이 어떻게 상호작용하는지 더 자세히 살펴보자.

예제를 통한 이해
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let a = 10;
function fn1() {
  let b = 20;
  function fn2() {
    let c = 30;
    console.log(a + b + c);
  }
  fn2();
}
fn1();

이 코드가 실행될 때 발생하는 일을 단계별로 살펴보면:

  1. 전역 실행 컨텍스트 생성:

    • 전역 렉시컬 환경 생성
    • 환경 레코드에 변수 a와 함수 fn1 등록
    • a에 10 할당
  2. fn1 호출:

    • fn1의 실행 컨텍스트 생성
    • fn1의 렉시컬 환경 생성
    • 환경 레코드에 변수 b와 함수 fn2 등록
    • 외부 렉시컬 환경 참조는 전역 렉시컬 환경을 가리킴
    • b에 20 할당
  3. fn2 호출:

    • fn2의 실행 컨텍스트 생성
    • fn2의 렉시컬 환경 생성
    • 환경 레코드에 변수 c 등록
    • 외부 렉시컬 환경 참조는 fn1의 렉시컬 환경을 가리킴
    • c에 30 할당
  4. console.log(a + b + c) 실행:

    • c는 현재 렉시컬 환경에서 찾음
    • b는 외부 렉시컬 환경(fn1)에서 찾음
    • a는 더 외부의 렉시컬 환경(전역)에서 찾음
    • 60 출력 (10 + 20 + 30)
  5. fn2 실행 완료:

    • fn2의 실행 컨텍스트가 스택에서 제거됨
  6. fn1 실행 완료:

    • fn1의 실행 컨텍스트가 스택에서 제거됨

이 과정에서 볼 수 있듯이, 각 함수가 호출될 때마다 새로운 실행 컨텍스트와 렉시컬 환경이 생성된다. 그리고 변수를 찾을 때는 현재 렉시컬 환경에서 시작하여 외부 렉시컬 환경 참조를 따라 상위 환경을 차례로 탐색한다.

클로저(Closure)와 렉시컬 환경

클로저는 렉시컬 환경과 깊은 관련이 있다. 클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function createCounter() {
  let count = 0;
  
  return function increment() {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

이 예제에서 increment 함수는 createCounter의 렉시컬 환경에 있는 count 변수에 계속 접근할 수 있다. 이는 increment 함수가 생성될 때 외부 렉시컬 환경 참조를 통해 createCounter의 렉시컬 환경을 참조하기 때문이다. 이 참조는 createCounter 함수의 실행이 완료된 후에도 유지된다.

블록 스코프와 렉시컬 환경

ES6부터 도입된 letconst는 블록 스코프를 가진다. 이는 중괄호({}) 내에서 선언된 변수가 해당 블록 내에서만 유효하다는 의미이다.

1
2
3
4
5
6
7
{
  let blockVar = "블록 내부";
  var functionVar = "함수 스코프";
}

console.log(functionVar); // "함수 스코프"
console.log(blockVar);    // ReferenceError: blockVar is not defined

JavaScript 엔진은 블록 스코프를 구현하기 위해 블록마다 별도의 렉시컬 환경을 생성한다.
블록이 실행될 때 새로운 렉시컬 환경이 생성되고, 블록이 종료되면 해당 환경은 참조가 없어져 가비지 컬렉션의 대상이 된다.

This 바인딩과 실행 컨텍스트

this 키워드의 값은 함수가 어떻게 호출되는지에 따라 결정된다.
이는 실행 컨텍스트가 생성될 때 this 바인딩 컴포넌트에 저장된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const person = {
  name: "홍길동",
  sayHello: function() {
    console.log(`안녕하세요, ${this.name}입니다.`);
  }
};

person.sayHello(); // "안녕하세요, 홍길동입니다."

const greet = person.sayHello;
greet(); // "안녕하세요, undefined입니다." (this가 전역 객체를 가리킴)

여기서 person.sayHello()를 호출할 때 thisperson 객체를 가리키지만, greet()를 호출할 때는 this가 전역 객체(브라우저에서는 window)를 가리킨다. 이는 함수를 어떻게 호출하는지에 따라 실행 컨텍스트의 this 바인딩이 다르게 설정되기 때문이다.

함수의 종류에 따른 실행 컨텍스트와 렉시컬 환경의 차이

JavaScript에는 여러 종류의 함수가 있으며, 각각 실행 컨텍스트와 렉시컬 환경에 영향을 미치는 방식이 다르다.

일반 함수(Function Declaration & Expression)

일반 함수는 자신만의 this, arguments, 그리고 새로운 실행 컨텍스트를 가진다.

1
2
3
function regularFunction() {
  console.log(this); // 호출 방식에 따라 달라짐
}
화살표 함수(Arrow Function)

화살표 함수는 자신의 thisarguments를 갖지 않으며, 상위 스코프의 것을 사용한다.

1
2
3
const arrowFunction = () => {
  console.log(this); // 항상 상위 스코프의 this를 사용
};

이는 화살표 함수가 생성될 때 렉시컬 환경의 외부 환경 참조를 통해 상위 스코프의 this를 캡처하기 때문이다.

실행 컨텍스트와 렉시컬 환경의 응용

이 개념들을 이해하면 JavaScript의 많은 고급 패턴과 기법을 더 잘 이해할 수 있다.

모듈 패턴(Module Pattern)

모듈 패턴은 클로저를 이용하여 private 변수와 메서드를 구현하는 방법.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const counter = (function() {
  let count = 0; // private 변수
  
  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
    getValue: function() {
      return count;
    }
  };
})();

console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getValue());  // 2
console.log(counter.count);       // undefined (private 변수에 직접 접근 불가)

이 패턴은 즉시 실행 함수 표현식(IIFE)의 렉시컬 환경에 count 변수를 캡슐화하여, 외부에서 직접 접근할 수 없게 한다.

커링(Currying)

커링은 여러 인자를 받는 함수를 인자 하나씩 받는 함수들의 체인으로 변환하는 기법.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

이 패턴은, 각 단계에서 새로운 실행 컨텍스트와 렉시컬 환경이 생성되고, 클로저를 통해 이전 단계의 인자가 유지되는 방식으로 작동한다.

실제 개발에서의 호이스팅 활용 및 주의사항

호이스팅을 활용한 코드 구성

함수 호이스팅을 활용하면 코드의 가독성을 높일 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 메인 함수 먼저 배치
function main() {
  helper1();
  helper2();
}

// 보조 함수는 아래에 배치
function helper1() {
  console.log("보조 함수 1");
}

function helper2() {
  console.log("보조 함수 2");
}

main(); // 정상 작동

이렇게 하면 중요한 코드를 먼저 보여주고 세부 구현은 나중에 배치할 수 있다.

호이스팅 관련 모범 사례

호이스팅으로 인한 문제를 방지하기 위한 몇 가지 모범 사례:

명확한 초기화 패턴 사용하기
1
2
3
4
5
6
7
8
let config = null; // 명시적으로 초기값 설정

function initConfig() {
  config = {
    theme: 'dark',
    language: 'ko'
  };
}
변수는 항상 스코프의 최상단에서 선언하기
1
2
3
4
5
6
7
8
function goodExample() {
  // 모든 변수를 함수 시작 부분에 선언
  let a = 1;
  let b = 2;
  
  // 이후 로직 작성
  return a + b;
}
Let과 Const 사용하기

var 대신 letconst를 사용하면 TDZ 덕분에 호이스팅 관련 버그를 더 일찍 발견할 수 있다.

1
2
3
4
5
6
7
function betterExample() {
  // 이 줄에서 에러가 발생하므로 버그를 일찍 찾을 수 있음
  // console.log(x); // ReferenceError: x is not defined
  
  const x = 10;
  return x;
}
함수 표현식 사용하기

함수 선언문 대신 함수 표현식을 사용하면 호이스팅을 제한할 수 있다.

1
2
3
4
5
6
7
8
9
// 함수 선언문 (전체가 호이스팅됨)
function declaredFunction() {
  return "I am hoisted entirely";
}

// 함수 표현식 (변수 호이스팅 규칙을 따름)
const expressionFunction = function() {
  return "I follow variable hoisting rules";
};

다양한 유형의 호이스팅 비교

선언 유형호이스팅초기화TDZ예시
var 변수undefined아니오var x = 5;
let 변수아니오let x = 5;
const 변수아니오const x = 5;
함수 선언문전체 함수아니오function x() {}
함수 표현식(var)변수만undefined아니오var x = function() {};
함수 표현식(let/const)변수만아니오const x = function() {};
클래스 선언아니오class X {}
클래스 표현식변수만아니오const X = class {};

호이스팅의 주의사항

  1. 코드의 가독성과 유지보수성을 위해 변수와 함수는 사용하기 전에 선언하는 것이 좋다.
  2. let과 const를 사용하여 예측 가능한 스코프 동작을 만들어내는 것이 권장된다.
  3. 함수 표현식보다는 함수 선언문을 사용하면 호이스팅으로 인한 혼란을 줄일 수 있다.

다양한 JavaScript 환경에서의 호이스팅

브라우저 환경에서의 호이스팅

브라우저에서는 전역 스코프가 window 객체와 연결된다:

1
2
3
4
5
6
var globalVar = "전역 변수";
console.log(window.globalVar); // "전역 변수"

// let과 const는 window 객체에 추가되지 않음
let globalLet = "전역 let 변수";
console.log(window.globalLet); // undefined

Node.js 환경에서의 호이스팅

Node.js에서는 모듈 패턴으로 인해 전역 스코프가 다르게 동작한다:

1
2
3
4
var moduleVar = "모듈 변수";
console.log(global.moduleVar); // undefined (Node.js에서는 var가 global에 자동 추가되지 않음)

// CommonJS 모듈에서는 각 파일이 자체 스코프를 가짐

ES 모듈에서의 호이스팅

ES 모듈은 자체적인 스코프를 가지며, importexport가 호이스팅된다:

1
2
3
4
5
6
7
8
9
// 이 import는 실행 전에 처리됨
import { helper } from './helper.js';

// 사용 전에 export됨
helper();

export function myFunction() {
  // …
}

참고 및 출처