Backend/Node.js

자바스크립트는 왜 어려울까? 호이스팅, 클로저, this, 프로토타입 쉽게 이해하기

_밍지_ 2026. 6. 6. 00:58
728x90
반응형
SMALL

자바스크립트 기초 정리 - 변수, 호이스팅, 클로저, 객체, 배열, 함수, 프로토타입

변수, 호이스팅(Hoisting), 클로저(Closure)

자바스크립트는 동적(Dynamic) 언어이기 때문에 변수의 타입을 미리 선언할 필요가 없다. 데이터 타입은 프로그램이 실행되는 과정에서 자동으로 결정되며, 변수의 타입을 확인하려면 typeof 연산자를 사용한다.

var puppy = "cute";

ES6 이전에는 변수를 선언할 때 var를 사용했다.

const puppy = "cute";
let dog = "lovely";

하지만 ES6 이후에는 var 대신 const와 let 사용을 권장한다. 이는 var가 가진 변수 호이스팅(Hoisting)Function-Level Scope 로 인해 발생할 수 있는 문제를 방지하기 위함이다.

변수 호이스팅(Hoisting)

변수 호이스팅이란 변수 선언과 초기화가 동시에 이루어져, 아직 값이 없음에도 오류가 나지 않는 현상이다.

예를 들어 변수를 선언하기 전에 참조했음에도 오류가 발생하지 않고 undefined가 출력되는 경우가 있다.

이러한 현상을 변수 호이스팅이라고 한다.

참고로 Hoisting은 "끌어올리다"라는 뜻이다.

자바스크립트의 기본 데이터 타입에는 다음과 같은 것들이 있다.

  • Number(숫자)
  • String(문자열)
  • Boolean
  • Undefined
  • Null

여기서 null은 개발자가 직접 변수에 null을 할당한 상태를 의미한다.

반면 undefined는 변수가 존재하지만 아직 값이 할당되지 않은 상태를 의미한다.

즉, 호이스팅으로 인해 변수는 값이 없음에도 메모리 공간을 차지하고 있으며, 선언하지 않은 것처럼 보이는 변수도 참조할 수 있게 된다.

let, const를 사용하는 이유

let과 const는 같은 이름의 변수를 중복 선언할 수 없기 때문에 호이스팅으로 인한 실수를 줄일 수 있다.

즉, 내가 명확하게 값을 할당한 변수만 사용할 수 있으므로 예기치 못한 오류를 예방할 수 있다.

Scope(스코프)

스코프(Scope)는 "범위"라는 뜻으로, 변수에 접근할 수 있는 범위를 의미한다.

Function-Level Scope

var는 Function-Level Scope를 따른다.

즉, 함수 내부에서 선언한 변수는 함수 내부에서만 유효하고, 함수 외부에서 선언한 변수는 모두 전역 변수(Global Variable)가 된다.

이 때문에 의도하지 않게 전역 변수의 값을 덮어쓰는 문제가 발생할 수 있다.

Block-Level Scope

let과 const는 Block-Level Scope를 따른다.

즉, 블록({}) 내부에서 선언한 변수는 블록 외부에 영향을 주지 않는다.

따라서 아래와 같은 경우

let puppy = "cute";

{
  let puppy = "lovely";
}

위의 puppy와 블록 내부의 puppy는 이름만 같을 뿐 서로 다른 변수이다.

블록을 기준으로 위와 아래는 완전히 다른 영역이라고 생각하면 된다.

let과 const의 차이

  • let : 재할당 가능
  • const : 재할당 불가능

이 외의 동작은 거의 동일하다.


클로저(Closure)

function outer() {
  var a = 'A';
  var b = 'B';

  function inner() {
    var a = 'AA';
    console.log(b);
  }

  return inner;
}

var outerFunc = outer();

outerFunc(); // B

클로저(Closure)는 내부 함수가 외부 함수의 스코프(범위)에 접근할 수 있는 현상을 말한다.

자바스크립트에서는 스코프가 함수 단위로 생성된다.

위 예제에서 inner() 함수는 outer() 함수 내부에 생성되었기 때문에 outer()의 스코프를 참조할 수 있다.

특히 outer() 함수의 실행이 종료된 이후에도 inner() 함수가 outer() 함수의 변수인 b에 접근할 수 있는데, 이러한 현상을 클로저(Closure) 라고 한다.


객체(Object)와 배열(Array)

객체(Object)는 현실 세계의 하나의 카테고리 또는 덩어리라고 생각하면 이해하기 쉽다.

예를 들어 "나라"라는 객체가 있다면 이름, 크기, 인구 수 등의 특징을 가질 수 있다.

자바스크립트에서 객체는 키(Key)와 값(Value)의 쌍으로 이루어진 프로퍼티(Property)의 집합이다.

const country = {
  name: "Korea",
  population: "5178579",

  get_name: function() {
    return this.name;
  }
};

객체가 가지고 있는 특징이나 정보를 프로퍼티(Property) 라고 한다.

위 예제에서는

  • name
  • population

이 프로퍼티에 해당한다.

또한 객체는 데이터뿐만 아니라 행위도 가질 수 있다.

객체 내부에 함수를 넣어 만들 수 있으며, 위 예제의 get_name()이 이에 해당한다.

객체 내부에 존재하는 함수를 메서드(Method) 라고 부른다.


배열(Array)

const coffee = [];

coffee.push({ name: 'Americano' });
coffee.push({ name: 'Latte' });

console.log(coffee);
// [{name:'Americano'}, {name:'Latte'}]

console.log(coffee[0]);
// {name:'Americano'}

console.log(coffee.length);
// 2

배열은 [요소1, 요소2, ...] 형태로 생성할 수 있다.

배열에는 숫자, 문자열, 객체 등 다양한 타입의 데이터를 저장할 수 있다.

위 예제는 객체를 요소로 사용하는 객체 배열이다.

배열에 요소를 추가할 때는 .push() 메서드를 사용한다.


구조 분해 할당(Destructuring Assignment)

const animal = ['dog', 'cat'];

let [first, second] = animal;

console.log(first);  // dog
console.log(second); // cat

자바스크립트에서는 객체와 배열을 많이 사용한다.

이때 객체나 배열의 값을 간편하게 분리하여 변수에 저장할 수 있는데, 이를 구조 분해 할당(Destructuring Assignment) 이라고 한다.


자주 사용하는 배열 내장 함수

  • forEach() : for문을 간단하게 사용
  • indexOf() : 원소의 인덱스 반환
  • findIndex() : 객체 또는 배열 요소의 인덱스 검색
  • shift() : 첫 번째 요소 제거 및 반환
  • unshift() : 배열 맨 앞에 요소 추가
  • join() : 배열 요소를 문자열로 합침
  • map() : 배열 요소를 변환하여 반환
  • find() : 조건에 맞는 첫 번째 값 반환
  • filter() : 조건에 맞는 배열 생성
  • splice() : 특정 요소 제거
  • slice() : 일부 요소를 잘라 새 배열 생성
  • pop() : 마지막 요소 제거 및 반환
  • concat() : 배열 합치기
  • reduce() : 누적 값 계산

함수(Function)

함수는 function 키워드를 이용해 선언한다.

() 안에는 파라미터(Parameter)를 작성하고, {} 안에는 실행할 로직을 작성한다.

또한 return을 통해 결과값을 반환할 수 있다.

화살표 함수(Arrow Function)

const add = (a, b) => {
  return a + b;
}

console.log(add(1, 4)); // 5

화살표 함수는 function 대신 =>를 사용하여 선언할 수 있다.

함수 내부에 return만 존재한다면 아래와 같이 축약 가능하다.

const add = (a, b) => a + b;

arguments

일반 함수는 생성될 때 자동으로 arguments 객체를 가진다.

const func = function() {
  console.log(arguments);
}

func(1, 2, 3, 4);

// [Arguments] {'0':1, '1':2, '2':3, '3':4}

화살표 함수와 ...args

화살표 함수는 arguments 객체를 자동 생성하지 않는다.

따라서 필요한 경우 전개 연산자(Spread Operator)를 사용한다.

const func = (...args) => {
  console.log(args);
}

func(1, 2, 3, 4);

// [1, 2, 3, 4]

...args는 전달되는 값을 배열 형태로 모아준다.

전개 연산자는 ES6에서 추가된 문법이다.


this

자바스크립트의 this는 다른 언어와 다르게 동작한다.

자바스크립트에서 this는 호출하는 방식에 따라 결정된다.

브라우저 콘솔에서

console.log(this);

를 실행하면 Window 객체가 출력된다.

브라우저 환경에서 전역 객체(Global Object)는 Window 객체이기 때문이다.


var people = {
  name: 'gildong',

  say: function() {
    console.log(this);
  }
}

people.say();

var sayPeople = people.say;

sayPeople();

people.say()는 people 객체가 호출했으므로 this는 people 객체를 가리킨다.

반면

var sayPeople = people.say;

sayPeople();

는 전역에서 호출되므로 this는 전역 객체(Window)를 가리킨다.


bind()

var people = {
  name: 'gildong',

  say: function() {
    console.log(this);
  }
}

people.say();

var sayPeople = people.say.bind(people);

sayPeople();

실행 결과

{name: 'gildong', say: [Function: say]}
{name: 'gildong', say: [Function: say]}

this를 특정 객체로 고정하고 싶다면

bind(객체)

를 사용한다.


화살표 함수와 this

화살표 함수에는 자체적인 this가 존재하지 않는다.

따라서 bind()를 사용해도 this를 새롭게 주입할 수 없다.

또한 생성자(new)로 사용할 수도 없다.

화살표 함수 내부에서 this를 사용하면 일반 변수처럼 동작하며, 내부에 this가 존재하지 않으므로 상위 스코프의 this 또는 전역 객체의 this를 참조하게 된다.


프로토타입(Prototype)과 상속(Inheritance)

프로토타입(Prototype)은 "원형"이라는 뜻이다.

자바스크립트는 프로토타입을 이용해 객체 지향 프로그래밍을 구현한다.

자바스크립트는 전통적인 클래스 기반 언어가 아니라 프로토타입 기반 언어(Prototype-Based Language) 라고 부른다.


prototype 객체

function func() {};

console.log(func.prototype);

func.prototype.name = 'gildong';

console.log(func.prototype);

자바스크립트에서는 기본 데이터 타입을 제외한 대부분의 것이 객체이다.

객체의 원형이 되는 프로토타입을 이용하여 새로운 객체를 생성할 수 있으며, 이렇게 생성된 객체는 또 다른 객체의 원형이 될 수 있다.

prototype은 객체의 프로퍼티 중 특별한 의미를 가진 프로퍼티이며, 이 역시 객체이다.


브라우저 콘솔에서

function func() {};

를 입력한 뒤

func.prototype

을 확인해보자.

출력된 결과 내부의 __proto__ 객체를 살펴보면 여러 가지 기본 프로퍼티가 존재한다.

예를 들어

func.hasOwnProperty()

와 같은 메서드는 직접 선언하지 않았지만 프로토타입 객체(__proto__)에 기본적으로 존재하기 때문에 사용할 수 있다.

객체 내부에는 __proto__라는 프로퍼티가 존재한다.

그리고 이 __proto__는 객체를 생성한 원형 객체(Prototype Object)를 참조하는 숨겨진 링크 역할을 한다.


프로토타입 상속

const animal = {
  leg: 4,
  tail: 1,

  say() {
    console.log('I have 4 legs 1 tail');
  }
}

const dog = {
  sound: 'wang'
}

const cat = {
  sound: 'yaong'
}

dog.__proto__ = animal;
cat.__proto__ = animal;

console.log(dog.leg); // 4

프로토타입이 중요한 이유는 상속(Inheritance)을 가능하게 하기 때문이다.

위 예제에서 dog 객체에는 leg 프로퍼티가 없지만, 프로토타입인 animal에서 값을 찾아 사용할 수 있다.


Prototype Chaining

const animal = {
  leg: 4,
  tail: 1,

  say() {
    console.log('I have 4 legs 1 tail');
  }
}

const dog = {
  sound: 'wang',
  happy: true
}

dog.__proto__ = animal;

const cat = {
  sound: 'yaong'
}

cat.__proto__ = dog;

console.log(cat.happy); // true
console.log(cat.leg);   // 4

이처럼 프로토타입을 여러 단계로 연결하는 것을 Prototype Chaining 이라고 한다.

  • cat에 happy가 없으므로 dog에서 찾는다.
  • cat과 dog 모두 leg가 없으므로 animal에서 찾는다.

이러한 방식으로 상속 관계가 연결된다.


생성자 함수와 Prototype

function Animal() {}

Animal.prototype.legs = 4;
Animal.prototype.tail = 1;

const dog = new Animal();
const cat = new Animal();

자바스크립트는 프로토타입을 이용하여 클래스 없이도 객체 지향 프로그래밍을 구현할 수 있다.

function과 new를 사용해 클래스처럼 동작하는 객체를 생성할 수 있으며,

객체.prototype.속성 = 값

형태로 여러 객체가 공통 속성을 공유하도록 만들 수 있다.

위 예제에서 dog와 cat은

  • Animal.prototype.legs
  • Animal.prototype.tail

을 함께 사용한다.

즉, 두 객체가 동일한 프로토타입 객체를 공유하기 때문에 메모리 공간은 2개만 사용된다.

만약

this.legs = 4;
this.tail = 1;

처럼 각 객체마다 개별적으로 저장했다면 총 4개의 공간이 필요하게 된다.

따라서 공통 속성이나 메서드는 Prototype에 저장하여 공유하는 것이 메모리 효율 측면에서 유리하다.

728x90
반응형
LIST