밀집 배열(dense array)

밀집 배열은 동일한 크기의 메모리 공간이 빈틈없이 연속적으로 나열된 자료구조이다.
배열의 요소는 하나의 데이터 타입으로 통일되어 있으며 서로 연속적으로 인접해 있다.
이러한 밀집 배열이 자료구조(data structure)에서 말하는 배열이다.

이러한 일반적인 의미의 배열은 각 요소가 동일한 데이터 크기를 갖고, 빈틈없이 연속적으로 이어져 있다.
그러기에 아래의 연산을 통해 단 한 번의 연산으로 임의의 요소에 접근할 수 있다.
검색 대상 요소의 메모리 주소 = 배열의 시작 메모리 주소 + 인덱스 * 요소의 바이트 수
이처럼 매유 효율적이고 고속으로 동작하는 방식을 임의 접근(random access)라고 부르며 시간 복잡도는 O(1)이다.

다만, 정렬이 되지 않은 배열에서 특정한 요소를 검색하는 경우는 선형 검색(inear search)를 통해 접근해야 한다.
또한, 배열의 요소를 삽입하거나 삭제하는 경우 배열의 요소를 연속적으로 유지하기 위해 요소를 이동시켜야 하는 단점도 있다.

참고) 선형 검색

모든 요소를 처음부터 특정 요소를 발견할 때 까지 차례대로 검색하는 방식이다. 시간 복잡도는 O(n)이다.

// 선형 검색을 통해 배열(array)에 특정 요소(target)가 존재하는지 확인한다.
// 배열에 특정 요소가 존재하면 특정 요소의 인덱스를 반환하고, 존재하지 않으면 -1을 반환한다.
function linearSearch(array, target) {
  const length = array.length;

  for (let i = 0; i < length; i++) {
    if (array[i] === target) return i;
  }

  return -1;
}

console.log(linearSearch([1, 2, 3, 4], 2)); // 1
console.log(linearSearch([1, 2, 3, 4], 5)); // -1

희소 배열(sparse array)

희소 배열은 앞에서 말한 일반적인 의미의 배열(밀집 배열)과 다르다.
배열의 요소를 위한 각각의 메모리 공간은 동일한 크기를 갖지 않아도 되며, 연속적으로 어어져 있지 않을 수도 있다.
자바스크립트의 배열은 희소 배열이다.
즉, 자바스크립트의 배열은 엄밀히 말해 일반적인 의미의 배열(밀집 배열)이 아닌 일반적인 배열의 동작을 흉내 낸 특수한 객체(희소 배열)인 것이다.

그러기 때문에 자바스크립트 배열의 타입은 Object인 것이다.

typeof [1, 2, 3]; // 'object'

자바스크립트의 배열

자바스크립트의 배열은 Object의 모든 특징을 포함하고 있으며, 배열만의 추가적인 특징들이 존재한다.

  • 인덱스 기반
  • 값의 순서
  • length 프로퍼티
    인덱스 기반의 배열은 인덱스와 요소로 이루어져 있으며 값에 대한 참조를 인덱스를 통해 한다.
    객체와는 다르게 값에 순서가 있다.
    length 프로퍼티(속성)을 지닌다는 것도 배열만의 추가적인 특징이다.

일반적인 배열과 자바스크립트 배열의 장단점

일반적인 배열은 인덱스로 요소에 빠르게 접근할 수 있다. 하지만 요소를 삽입 또는 삭제하는 경우에는 효율적이지 않다.
반면 자바스크립트 배열은 인덱스로 배열 요소에 접근하는 경우는 일반 배열보다 느리지만, 요소를 삭제, 삽입하는 경우에는 일반 배열보다 빠르다.

또한, 인덱스로 배열 요소에 접근할 때 일반적인 배열보다 느릴 수밖에 없는 구조적인 단점을 보완하기 위해 대부분의 모던 자바스크립트 엔진은 배열을 일반 객체와 구별하여 좀 더 배열처럼 동작하도록 최적화해 구현했다고 한다.
아래의 코드를 통해 확인할 수 있다.

const arr = [];

console.time("Array");

for (let i = 0; i < 1000000; i++) {
  arr[i] = i;
}
console.timeEnd("Array");
// 14ms


const obj = {};

console.time("Object");

for (let i = 0; i < 1000000; i++) {
  obj[i] = i;
}
console.timeEnd("Object");
// 26ms

'JavaScript > JavaScript' 카테고리의 다른 글

옵셔널 체이닝(Optional Chaining)  (1) 2024.02.25
JavaScript에서 접근자 프로퍼티와 캡슐화  (0) 2023.10.11
JS 문자열 비교 연산자  (1) 2023.10.06
호이스팅(Hoisting)  (0) 2023.09.27
sort 메소드  (0) 2023.09.06

객체의 프로퍼티(속성)는 점표기법을 통해 접근한다.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};

const catName = adventurer.cat.name;
console.log(catName); // Dinah

다만, 이렇게 중첩된 객체를 다룰 때 조심해야 할 부분이 있다.

const adventurer = {
  name: 'Jake',
};

const catName = adventurer.cat.name;
console.log(catName);

이렇게 cat 프로퍼티를 가지고 있지 않은 adventurer은 cat 프로퍼티가 undefined이므로 adventurer.cat.name에 접근하면 에러가 발생한다.

그래서 catName과 같이 중첩된 객체의 프로퍼티를 다룰 때는 adventurer.cat.name에 접근하기 전에 adventurer.cat이 null 또는 undefined가 아님을 확인하고 접근해야 에러를 방지할 수 있다.

이를 해결하기 위해서는 두개의 방식이 사용된다.

  1. if문 또는 AND 연산자
    // if 문
    if (adventurer.cat) {
    console.log(adventurer.cat.name);
    }
    

// AND 연산자
console.log(adventurer.cat && adventurer.cat.name)

AND 연산자를 이렇게 활용할 수 있는 이유는 `1-12section.js`에 있는 SCE 때문이다.

2. Optional Chaining
```javascript
console.log(adventurer.cat?.name);

?.이게 옵셔널 체이닝 연산자이다.
이 연산자는 왼쪽 프로퍼티 값이 undefined 또는 null이 아니라면 그다음 프로퍼티 값을 리턴하고,
undefined 또는 null이라면 undefined를 반환한다.

이걸 코드를 통해 동작 원리를 살펴보면 아래와 같다.

console.log((adventurer.cat === null || adventurer.cat === undefined) ? undefined : adventurer.cat.name)

이 옵셔널 체이닝 연산자는 리액트에서 매우 자주 사용된다.

API 호출 결과 처리

const MyComponent = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://api.example.com/users')
      .then((response) => response.json())
      .then((data) => setData(data));
  }, []);

  // data가 null인 경우 컴포넌트 렌더링 오류 발생
  const name = data.user.name;

  // Optional Chaining 사용
  const name = data?.user?.name;

  return (
    <div>
      <h1>{name}</h1>
    </div>
  );
};

조건부 렌더링

const MyComponent = ({ user }) => {
  return (
    <div>
      {user?.name && <h1>{user.name}</h1>}
      {!user?.name && <p>사용자 정보가 없습니다.</p>}
    </div>
  );
};

'JavaScript > JavaScript' 카테고리의 다른 글

밀집 배열과 희소 배열  (1) 2024.02.26
JavaScript에서 접근자 프로퍼티와 캡슐화  (0) 2023.10.11
JS 문자열 비교 연산자  (1) 2023.10.06
호이스팅(Hoisting)  (0) 2023.09.27
sort 메소드  (0) 2023.09.06

리액트 개발을 하다 보면 for 문이나 while 문보다는
Array 객체의 메서드(map, reduce, filter)를 더 자주 사용한다.

이러한 방식을 선언형 프로그래밍(Declarative Programming)이라고 하며
리액트 개발에서는 선언형 프로그래밍 방식이 더 선호된다.
그리고 이와 대비되는 개념은 명령형 프로그래밍(Imperative Programming)이라 한다.

반복문에서의 명령형 프로그래밍과 선언형 프로그래밍

명령형 프로그래밍

명령형 프로그래밍(Imperative Programming)은 기존에 일반적으로 사용되는 프로그래밍 방식이다.

const numbers = [1, 2, 3, 4, 5];
let sum = 0;

// numbers 배열의 짝수 합
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    sum += numbers[i];
  }
}

이처럼 합계를 구하는 코드가 어떻게 동작하는지를 작성하는 방식을 명령형 프로그래밍이라고 한다.
이것만 보고는 명령형 프로그래밍에 대해 감이 잡히지 않을 수 있다.
왜냐하면 명령형 프로그래밍 방식은 우리가 일반적으로 코딩하는 방식이기 때문이다.
아래의 선언형 프로그래밍을 보면 확실하게 이해가 갈 것이다.

선언형 프로그래밍

선언형 프로그래밍(Declarative Programming)은 원하는 결과를 묘사하는 방식으로 코드를 작성한다.
아래의 예제는 위에 명령형 배열의 예에서 사용했던 짝수의 합을 구하는 예제이다.

const numbers = [1, 2, 3, 4, 5];

// 1. filter 함수로 짝수만 추출
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
const evenNumbers = numbers.filter(number => number % 2 === 0);

// 2. reduce 함수로 합산
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
const sum = evenNumbers.reduce((acc, number) => acc + number, 0);

위에 예제는 filter() 함수와 recue() 함수를 사용해 배열의 짝수인 합을 구한다.
여기에서 중요한 것은 어떻게 필터링하고, 어떻게 합을 구하는지가 아니라 결과를 얻는 것에 초점이 맞춰져 있다.
이렇게 원하는 결과가 무엇인지에 초점을 맞추는 방식을 선언현 프로그래밍이라고 한다.

UI에서의 명령형 프로그래밍과 선언형 프로그래밍

명령형 프로그래밍

<div id="root"></div>
const root = document.getElementById("root");
const $inputFiled = document.createElement("input");
const $submitBtn = document.createElement("button");

$submitBtn.textContent = "Send";
$submitBtn.disabled = true;

root.appendChild($inputFiled);
root.appendChild($submitBtn);

$inputFiled.addEventListener("input", (event) => {
  const inputValue = event.target.value;
  $submitBtn.disabled = inputValue.trim().length === 0;
});

$submitBtn.addEventListener("click", () => {
  $inputFiled.value = "";
  $submitBtn.disabled = true;
});

이처럼 명령형 프로그래밍은
DOM 트리의 요소 생성, 속성 설정, 이벤트 추가 등의 단계를
순차적인 명령으로 수행한다.

그리고 상태(state)는 직접 명령($submitBtn.disabled = true;)을 통해 변경한다.
만약 입력한 값을 지우려면,
즉, 상태를 변경하려면 해당 명령을 직접 해야 한다.
이처럼 상태 변경을 매우 직관적으로 수행한다.

선언형 프로그래밍

function Form() {
  const [inputValue, setInputValue] = useState("");

  const handleChange = ((e) => {
    setInputValue(e.target.value);
  });

  const handleClick = (() => {
    setInputValue("");
  })

  const isButtonDisabled = inputValue.trim().length === 0;

  return (
    <div>
      <input type="text" value={inputValue} onChange={handleChange} />
      <button disabled={isButtonDisabled} onClick={handleClick} >
        Send
      </button>
    </div>
  )
}

이처럼 선언형 프로그래밍은
DOM 트리에 태그를 직접 명령하여 생성하는 방식이 아닌
선언만 하면(return ()) 나머지는 리액트가 알아서 처리하는 방식이다.

또한, text = 123처럼 명령형으로 값을 직접 변경할 수 없다.
setText() 함수를 호출해 내부에서 처리한 후
리액트가 컴포넌트에 다시 렌더링 하는 방식이다.

이러한 선언형 프로그래밍은 코드는 간결하며 가독성을 높여준다.
그리고 선언현 프로그래밍은 추상화를 가능하게 하며 이 때문에 재사용성이 높은 특징을 갖게 된다.

 


참고) 추상화란

추상화란 복잡한 것을 간단하게 보여주는 것을 말한다.
예를 들어 '자동차의 엑셀을 밞으면 자동차가 앞으로 간다'처럼,
사실 내부에서는 엔진 동작이나, 기어 등 복잡한 로직들이 구현되어 있지만, 엑셀만 밟으면 앞으로 간다로 필요한 부분만 추출해서 간단하게 만드는 것을 추상화라고 한다.

이런 추상화는 코드의 재사용성을 높여주는데,
만약 '엑셀을 밞으면 앞으로 간다.'를 함수로 정의했다고 하면
버스의 '엑셀을 밞으면 앞으로 간다.'
승용차의 '엑셀을 밟으면 앞으로 간다.'
처럼 필요할 때 '엑셀을 밞으면 앞으로 간다.' 함수만 가져와서 사용하면 되기에
하나의 함수를 다시 사용할 수 있다.
이를 재사용성을 높여 준다고 한다.

JavaScript에서 접근자 프로퍼티와 캡슐화

접근자 프로퍼티: Getter와 Setter

JavaScript에서 객체의 프로퍼티에는 크게 두 가지 유형이 있습니다. 데이터 프로퍼티와 접근자 프로퍼티입니다.

데이터 프로퍼티

기본적으로 변수에 값을 할당하는 것처럼 객체의 프로퍼티에 값을 저장하는 것을 '데이터 프로퍼티'라고 부릅니다.

const car = {
  model: "Sedan",
  year: 2022
};

접근자 프로퍼티

반면에, 접근자 프로퍼티는 실제 값을 갖지 않습니다. 대신, 다른 프로퍼티의 값을 읽거나 저장할 때 작동하는 getset 메서드를 정의합니다.

const student = {
  _score: 90,
  get score() {
    return this._score;
  },
  set score(value) {
    if (value < 0 || value > 100) {
      throw new Error("점수는 0~100 사이여야 합니다.");
    }
    this._score = value;
  }
};

클래스에서의 접근자 프로퍼티

getset 메서드는 클래스에서도 사용 가능합니다.

class Movie {
  constructor(title, year) {
    this.title = title;
    this.year = year;
  }
  get info() {
    return `${this.title} (${this.year})`;
  }
  set yearReleased(y) {
    if (y < 1800) return;
    this.year = y;
  }
}

일반적으로는 데이터 프로퍼티와 동일한 접근자 프로퍼티 명을 짓습니다.

class User {
  constructor(name, age) {
    this.name = name;
    this._age = age;  // 내부에서만 사용할 프로퍼티는 보통 '_'로 시작합니다.
  }

  get age() {
    return this._age;
  }

  set age(value) {
    if (value < 0) {
      console.error("나이는 음수가 될 수 없습니다.");
      return;
    }
    this._age = value;
  }
}

const user = new User("John", 30);
console.log(user.age); // 30

user.age = -1;  // "나이는 음수가 될 수 없습니다."
console.log(user.age); // 30

다만, _age라고 변수 명을 수정해야 합니다. 그렇지 않고 age를 사용하면 데이터 프로퍼티에 할당하는 과정(this.age = age)에 age를 호출할 경우 set메서드를 호출하는 과정을 반복할 수도 있습니다.

프로퍼티 이름 앞에 언더스코어(_)를 붙이면 이 프로퍼티는 클래스 내부에서만 사용되고, 외부에서는 접근하지 않는 것으로 알려주긴 하나, 실제로는 외부에서도 접근 가능합니다.

console.log(user._age); // 30

이렇게 접근할 수 있으므로 완전한 캡슐화를 보장하지는 않습니다. 그래서 아래처럼 # 를 사용해 캡슐화를 구현할 수 있습니다.

은닉과 캡슐화

JavaScript에서는 # 기호를 사용하여 private 필드를 정의할 수 있습니다. 이러한 private 필드는 클래스 외부에서 접근할 수 없어 캡슐화를 구현합니다.

class BankAccount {
  #balance = 0;

  get balance() {
    return this.#balance;
  }

  deposit(amount) {
    if (amount < 0) return;
    this.#balance += amount;
  }
}

이 방법을 사용하면, 외부에서 #balance 필드에 직접 접근할 수 없습니다.

const myAccount = new BankAccount();
console.log(myAccount.#balance);  // SyntaxError

'JavaScript > JavaScript' 카테고리의 다른 글

밀집 배열과 희소 배열  (1) 2024.02.26
옵셔널 체이닝(Optional Chaining)  (1) 2024.02.25
JS 문자열 비교 연산자  (1) 2023.10.06
호이스팅(Hoisting)  (0) 2023.09.27
sort 메소드  (0) 2023.09.06

문자열 비교 연산자

javaScript에서 문자열을 비교할 때는 ASCII 코드의 순서로 비교됩니다.

알파벳 비교

Copy code
console.log('apple' < 'banana'); // true
console.log('apple' > 'banana'); // false

대소문자

대문자 ASCII 코드 값이 소문자보다 작습니다.

Copy code
console.log('Apple' < 'apple'); // true
console.log('Apple' > 'apple'); // false

숫자형 문자열

숫자로 구성된 문자열도 사전식으로 비교됩니다.

Copy code
console.log('2' > '12'); // true, 문자열로서 '2'는 '12'보다 사전식으로 뒤에 옴
console.log('12' < '2'); // true, 문자열로서 '12'는 '2'보다 사전식으로 앞에 옴

특수문자

특수문자도 ASCII 코드 값을 기준으로 합니다.

Copy code
console.log('a' > '!'); // true, 'a'의 ASCII 코드 값이 '!'보다 큼

주의점

console.log(
  100 > 12,       //숫자는 그 자체로 비교
  '100' > '12',   //문자는 사전순으로 비교
  '100' > 12,     // 문자와 숫자를 비교하면 문자를 숫자로 변환
)

호이스팅(Hoisting)의 정의

호이스팅은 JavaScript에서 변수와 함수의 선언을 현재 스코프의 최상단으로 끌어올리는 특성을 말합니다. 이는 코드 실행 전에 JavaScript 엔진이 변수와 함수의 선언을 메모리에 할당하기 때문에 발생합니다.

호이스팅의 작동 원리

JavaScript의 실행 컨텍스트는 크게 세 단계로 나뉩니다

  1. 생성 단계 (Creation Phase)
    • 변수는 undefined로 초기화됩니다.
    • 함수 선언은 메모리에 저장되며, 해당 스코프의 최상단으로 끌어올려집니다.
  2. 실행 단계 (Execution Phase)
    • 코드가 줄 단위로 실행됩니다.
    • 변수에 값이 할당되고 함수가 호출됩니다.
  3. 종료 단계 (Termination Phase)

호이스팅은 생성 단계에서 발생합니다.

예)

console.log(a); // undefined
var a = 5;
console.log(a); // 5

foo(); // "Hello from foo!"
function foo() {
    console.log("Hello from foo!");
}

이 코드에서는 호이스팅 때문에 첫 번째 console.log(a);에서 에러가 발생하지 않고 undefined가 출력됩니다. 또한, 함수 foo가 선언되기 전에 호출되었지만, 호이스팅 덕분에 문제없이 실행됩니다.

변수 선언과 호이스팅

var로 선언된 변수의 호이스팅

  • var로 선언된 변수는 호이스팅 될 때 초기값이 undefined로 설정됩니다.
  • 변수 선언 및 초기화 이전에 변수를 참조하면 undefined 값을 반환합니다.
console.log(varVariable); // undefined
var varVariable = 5;
console.log(varVariable); // 5

let로 선언된 변수의 호이스팅

  • let로 선언된 변수도 호이스팅되지만, var와는 다르게 초기화 단계에서 undefined로 설정되지 않습니다.
  • 변수 선언 이전에 변수를 참조하려고 시도하면 "ReferenceError"가 발생합니다. 이 시기를 Temporal Dead Zone (TDZ)라고 부릅니다.
    console.log(letVariable); 
    // ReferenceError: Cannot access 'letVariable' before initialization
    let letVariable = 10;
    console.log(letVariable); // 10

const로 선언된 변수의 호이스팅

  • const로 선언된 변수도 호이스팅 되며, let과 마찬가지로 초기화 단계에서 undefined로 설정되지 않습니다.
  • 변수 선언 및 초기화 이전에 변수를 참조하려고 시도하면 "ReferenceError"가 발생합니다. 마찬가지로 이 시기는 Temporal Dead Zone (TDZ)에 속합니다.
  • const는 선언과 동시에 초기화가 반드시 이루어져야 합니다.
console.log(constVariable); 
// ReferenceError: Cannot access 'constVariable' before initialization
const constVariable = 20;
console.log(constVariable); // 20

정리

  • var: 변수가 호이스팅 되며 undefined로 초기화됩니다.
  • let: 변수가 호이스팅되나 초기값이 없으며 TDZ에 속하게 됩니다.
  • const: 변수가 호이스팅되며 초기값이 필요하고, TDZ에 속하게 됩니다.

함수 선언과 호이스팅

함수 선언식(Function Declarations)

  • 함수 선언식은 호이스팅 될 때 해당 스코프의 최상단으로 완전히 끌어올려집니다.
  • 이는 함수의 이름과 함께 함수의 본문도 함께 끌어올려진다는 것을 의미합니다.
  • 그 결과, 함수 선언 전에 함수를 호출할 수 있습니다.
// 호이스팅으로 인해 함수 선언 전에도 호출 가능
helloFunction();
// 안녕하세요!

function helloFunction() {
    console.log("안녕하세요!");
}

함수 표현식 (Function Expressions)

  • 함수 표현식은 변수에 함수를 할당하는 방식으로 정의됩니다.
  • 함수 표현식에 사용된 변수는 호이스팅 될 때 undefined로 초기화됩니다.
  • 그러나 함수 본문은 호이스팅되지 않습니다.
  • 따라서 함수 표현식을 통해 정의된 함수는 변수가 초기화된 후에만 호출할 수 있습니다.
// 호이스팅 때문에 변수는 이미 존재하지만, 함수의 본문은 아직 할당되지 않았으므로 에러 발생
helloExpression(); 
// TypeError: helloExpression is not a function

var helloExpression = function() {
    console.log("안녕하세요!");
};

helloExpression(); 
//안녕하세요!

알겠습니다. 화살표 함수에 대한 호이스팅의 동작을 자세히 알아보겠습니다.

화살표 함수 (Arrow Functions)

  • 화살표 함수는 함수 표현식의 간결한 버전으로 생각할 수 있습니다.
  • 화살표 함수는 변수에 할당되므로, 변수의 호이스팅 규칙이 적용됩니다.
  • 따라서 화살표 함수 자체는 호이스팅되지 않습니다. 하지만, 해당 화살표 함수가 할당된 변수는 호이스팅 될 때 undefined로 초기화됩니다.
  • 그 결과, 변수 할당 이전에 화살표 함수를 호출하려고 하면 오류가 발생합니다.
// 호이스팅 때문에 변수는 이미 존재하지만, 함수의 본문은 아직 할당되지 않았으므로 에러 발생
helloArrow(); 
// TypeError: helloArrow is not a function

var helloArrow = () => {
    console.log("안녕하세요!");
};

helloArrow(); 
// "안녕하세요!

참고: let 또는 const와 함께 선언된 화살표 함수의 경우, 호이스팅이 발생하지만 Temporal Dead Zone (TDZ) 에 있어 호출 시 "ReferenceError"가 발생합니다.

helloArrowWithLet(); 
// ReferenceError: Cannot access 'helloArrowWithLet' before initialization

let helloArrowWithLet = () => {
    console.log("let으로 선언된 화살표 함수입니다.");
};

helloArrowWithLet(); 
// let으로 선언된 화살표 함수입니다.

정리

  • 함수 선언식: 함수의 전체 본문이 호이스팅 되므로 선언 전에 호출할 수 있습니다.
  • 함수 표현식: 변수는 호이스팅되나 함수 본문은 호이스팅 되지 않으므로, 본문 할당 전에는 함수로서 호출할 수 없습니다.
  • 화살표 함수: 변수의 호이스팅 규칙이 적용되므로, 화살표 함수 본문은 호이스팅 되지 않습니다. 변수는 호이스팅 되나 초기화 전에 함수로서 호출하려면 오류가 발생합니다.

호이스팅의 범위

전역 범위 (Global Scope)에서의 호이스팅

  • 전역 범위에서 선언된 변수나 함수는 전역 객체 (브라우저에서는 window 객체)에 바인딩됩니다.
  • 호이스팅이 발생하면, 이러한 변수나 함수 선언은 스크립트의 최상단으로 끌어올려집니다.
  • 이는 앞에서 작성한 일반적인 호이스팅입니다.
console.log(globalVar); // undefined
var globalVar = "전역 변수";
console.log(globalVar); // "전역 변수"

globalFunction(); // "전역 함수 호출됨"

function globalFunction() {
    console.log("전역 함수 호출됨");
}

함수 범위 (Function Scope)에서의 호이스팅

  • 함수 내부에서 선언된 변수나 함수는 해당 함수 범위 내에서만 접근 가능합니다.
  • 함수 범위에서의 호이스팅은 해당 함수의 최상단으로 변수나 함수 선언을 끌어올립니다.
function exampleFunction() {
    console.log(localVar); // undefined
    var localVar = "함수 내부의 변수";
    console.log(localVar); // "함수 내부의 변수"

    localFunction(); // "함수 내부의 함수 호출됨"

    function localFunction() {
        console.log("함수 내부의 함수 호출됨");
    }
}

exampleFunction();
  • 또한, 함수 내부에서 var로 선언된 변수는 해당 함수의 전체 범위에서 사용할 수 있습니다. 이는 함수 범위의 호이스팅 때문입니다.
function hoistingExample() {
    console.log(functionScopedVar); // undefined
    if (true) {
        var functionScopedVar = "변수";
    }
    console.log(functionScopedVar); // "변수"
}

hoistingExample();

위에 예제에서 var functionScopedVar = "변수"if 블록 내부에서 선언되었지만, 함수의 어디에서나 접근할 수 있습니다. 이는 var가 함수 범위를 가지고, 호이스팅이 적용되기 때문입니다.

만약 조건절이 false인 경우는 var로 선언된 변수의 초기화 코드는 실행되지 않습니다. 하지만 변수 자체는 여전히 호이스팅되어 함수의 최상단에 선언되므로, undefined 값으로 초기화 됩니다.

function hoistingExample() {
    console.log(functionScopedVar); // undefined
    if (false) {
        var functionScopedVar = "변수";
    }
    console.log(functionScopedVar); // undefined
}

hoistingExample();

이러한 특성 때문에 var는 함수 내에서 예측하기 어려운 코드가 될 수 있습니다.
반면, letconstvar와 다르게 블록 범위(block scope)를 가집니다.

블록 범위(Block Scope)와 let, const

  • var는 함수 범위를 가지며, letconst는 블록 범위를 가집니다.
  • letconst로 선언된 변수는 호이스팅 되나, 초기화 전에 접근하려고 하면 Temporal Dead Zone (TDZ)에서 오류가 발생합니다.
function blockScopeExample() {
    if (true) {
        console.log(blockScopedVar); 
        // ReferenceError: Cannot access 'blockScopedVar' before initialization
        let blockScopedVar = "블록 범위의 변수";
        console.log(blockScopedVar); // "블록 범위의 변수"
    }
    console.log(blockScopedVar); 
    // ReferenceError: blockScopedVar is not defined
}

blockScopeExample();

TDZ(Temporal Dead Zone)

letconst로 선언된 변수도 호이스팅은 발생합니다. 다만, 이 변수들은 Temporal Dead Zone (TDZ) 시기에 접근할 수 없습니다. 왜냐하면, letconst가 블록 범위(block scope)를 가지며, 선언과 초기화가 분리되어 이루어지기 때문입니다.

Temporal Dead Zone (TDZ)

  • Temporal Dead Zone (TDZ)는 변수가 코드에서 물리적으로 선언된 위치와 초기화(할당)된 위치 사이의 코드 영역을 지칭합니다.
  • 이 시간 동안 해당 변수에 접근하려고 하면 JavaScript는 "ReferenceError"를 던집니다.
// 호이스팅
// TDZ 시작
console.log(myLetVariable); 
// ReferenceError: Cannot access 'myLetVariable' before initialization
// TDZ 끝, 초기화 시작
let myLetVariable = 10;

console.log(myLetVariable); // 10

letconst 선언도 호이스팅 되지만, 호이스팅 되는 시점과 초기화되는 시점 사이에 TDZ가 존재합니다.

  1. 호이스팅: 변수 선언이 스코프의 최상단으로 끌어올려집니다.
  2. TDZ 시작: 변수 선언문을 코드 실행이 만나면 TDZ가 시작됩니다.
  3. TDZ 끝 & 초기화: 변수 초기화(할당)가 실행되면 TDZ가 끝나고 변수가 사용 가능해집니다.

비교: var, let, const

  • var: 함수 범위를 가지며 호이스팅 됩니다. 호이스팅 된 변수는 undefined로 초기화됩니다.
  • let: 블록 범위를 가지며 호이스팅 되나, TDZ에 있을 때에는 접근할 수 없습니다.
  • const: 블록 범위를 가지며 호이스팅되나, TDZ에 있을 때에는 접근할 수 없습니다. 초기화 없이 선언이 불가능합니다.