이전 포스팅 https://ipjaworld.tistory.com/63 에서 이어지는 내용입니다.
포스팅은 3편에 나눠서 업로드 됩니다.
A. 자바스크립트 조망하기
1. 강제변환과 깊은비교, 일치비교와 동등비교,
2. js의 this 키워드와 프로토타입, 그리고 메모리주소
B. 추가 주제
1. es6 모듈에서 class를 사용하지 않는 이유
A. 자바스크립트 조망하기
2. js의 this 키워드와 프로토타입, 그리고 메모리주소
"You Don't Know JS Yet"을 읽기 전까지 자바스크립트의 this 키워드와 프로토타입 시스템은 나에게 미지의 영역과 같았다. 특히 다른 언어에서의 this와 다르게 동작하는 자바스크립트의 this는 많은 혼란을 가져왔다. 이번에는 this와 프로토타입, 그리고 자바스크립트의 메모리 주소 관리에 대해 내가 새롭게 이해한 내용을 정리해보려 한다.
this 키워드: 호출 시점에 결정되는 컨텍스트
자바스크립트의 this는 다른 언어와 달리 함수가 호출되는 방식에 따라 결정된다. 이것은 내가 기존에 알고 있던 객체 지향 언어(예: Java, C++)의 this와 완전히 다른 개념이다.
가장 헷갈렸던 코드 예시를 살펴보자:
const person = {
name: "Alice",
greet: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
person.greet(); // "Hello, my name is Alice"
const greetFunction = person.greet;
greetFunction(); // "Hello, my name is undefined"
왜 같은 함수를 호출했는데 다른 결과가 나올까? 그것은 this가 함수를 어떻게 호출했느냐에 따라 결정되기 때문이다.
this 바인딩 규칙
자바스크립트에서 this는 다음 규칙에 따라 결정된다(우선순위 순):
- 명시적 바인딩: call, apply, bind 메서드를 사용해 this를 명시적으로 지정할 수 있다.
- greetFunction.call(person); // "Hello, my name is Alice"
- new 바인딩: 생성자 함수를 new 키워드로 호출하면 this는 새로 생성된 객체를 가리킨다.
- function Person(name) { this.name = name; } const alice = new Person("Alice"); // this는 새로 생성된 객체를 가리킴
- 암시적 바인딩: 객체의 메서드로 호출되면 this는 그 객체를 가리킨다.
- person.greet(); // this는 person을 가리킴
- 기본 바인딩: 일반 함수 호출 시 this는 전역 객체(브라우저에서는 window, Node.js에서는 global)를 가리킨다. 엄격 모드('use strict')에서는 undefined가 된다.
- function showThis() { console.log(this); } showThis(); // window 또는 global (또는 엄격 모드에서는 undefined)
- 화살표 함수: 화살표 함수는 자신만의 this를 갖지 않고, 외부 스코프의 this를 그대로 사용한다.화살표 함수는 선언될 때의 외부 스코프 this를 유지하므로, 객체 메서드나 이벤트 핸들러에서 원래 객체의 컨텍스트를 유지하고 싶을 때 유용하다:
- const counter = { count: 0, increase: function() { // 화살표 함수는 counter의 this를 유지 setInterval(() => { console.log(++this.count); // counter.count를 증가 }, 1000); } }; counter.increase();
- const person = { name: "Alice" }; const normalFunction = function() { return this.name; }; const arrowFunction = () => this.name; console.log(normalFunction.call(person)); // "Alice" console.log(arrowFunction.call(person)); // undefined (또는 전역 객체의 name)
이 규칙들을 이해하기 전에는 this의 동작이 너무 혼란스러웠다. 특히 콜백 함수에서 this가 예상과 다르게 동작하는 경우가 많았는데, ES6의 화살표 함수는 이런 문제를 해결하는 데 큰 도움이 되었다.
프로토타입: 자바스크립트의 상속 메커니즘
자바스크립트의 프로토타입 시스템은 클래스 기반 언어와는 다른 방식으로 상속을 구현한다. 이를 "프로토타입 기반 상속"이라고 한다.
프로토타입 체인
모든 자바스크립트 객체는 내부 프로퍼티 [[Prototype]](또는 __proto__)를 갖고 있으며, 이는 다른 객체를 가리킨다. 객체의 프로퍼티를 참조할 때 그 객체에 해당 프로퍼티가 없으면, 자바스크립트 엔진은 프로토타입 체인을 따라 프로퍼티를 찾는다.
const animal = {
eat: function() {
console.log("Eating...");
}
};
const dog = Object.create(animal);
dog.bark = function() {
console.log("Woof!");
};
dog.eat(); // "Eating..." - dog 객체에 없지만 프로토타입 체인을 통해 animal에서 찾음
이 코드에서 dog의 프로토타입은 animal 객체다. dog.eat()을 호출하면, dog 객체에 eat 메서드가 없기 때문에 프로토타입 체인을 따라 animal 객체에서 eat 메서드를 찾는다.
__proto__와 prototype의 차이점
자바스크립트에서 __proto__와 prototype은 다른 개념이다:
- __proto__: 모든 객체가 가지는 내부 프로퍼티로, 해당 객체의 프로토타입을 가리킨다.
- prototype: 함수 객체만 가지는 프로퍼티로, 해당 함수를 생성자로 사용했을 때 만들어지는 객체의 프로토타입이 된다.
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() {
console.log(`${this.name} says woof!`);
};
const rex = new Dog("Rex");
console.log(rex.__proto__ === Dog.prototype); // true
console.log(Dog.__proto__ === Function.prototype); // true
생성자 함수와 prototype 속성
함수는 특별한 prototype 속성을 갖는데, 이는 해당 함수를 생성자로 사용해 만든 객체의 프로토타입이 된다.
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
const alice = new Person("Alice");
alice.greet(); // "Hello, my name is Alice"
이 코드에서 alice의 프로토타입은 Person.prototype이다. 따라서 alice 객체는 Person.prototype에 정의된 greet 메서드에 접근할 수 있다.
클래스 문법과 프로토타입
ES6에서 도입된 class 문법은 프로토타입 기반 상속을 더 쉽고 명확하게 사용할 수 있게 해준다. 하지만 내부적으로는 여전히 프로토타입을 사용한다.
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating.`);
}
}
class Dog extends Animal {
bark() {
console.log(`${this.name} says woof!`);
}
}
const rex = new Dog("Rex");
rex.eat(); // "Rex is eating."
rex.bark(); // "Rex says woof!"
이 코드에서 rex의 프로토타입 체인은 Dog.prototype -> Animal.prototype -> Object.prototype이다.
클로저와 프로토타입의 관계
클로저는 자바스크립트의 또 다른 강력한 개념이다. 클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합으로, 내부 함수가 외부 함수의 변수에 접근할 수 있게 해준다.
프로토타입과 클로저를 함께 사용하면 강력한 패턴을 만들 수 있다:
function createCounter() {
let count = 0; // 클로저를 통해 접근 가능한 private 변수
function Counter() {}
Counter.prototype.increment = function() {
count++;
return count;
};
Counter.prototype.decrement = function() {
count--;
return count;
};
return new Counter();
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
이 예제에서 count 변수는 클로저를 통해 Counter 프로토타입의 메서드들이 접근할 수 있지만, 외부에서는 직접 접근할 수 없는 private 변수처럼 동작한다.
메모리 주소와 가비지 컬렉션
자바스크립트에서 변수의 메모리 관리는 대부분 자동으로 이루어진다. 하지만 그 동작 원리를 이해하는 것은 효율적인 코드를 작성하는 데 큰 도움이 된다.
원시 타입과 참조 타입의 메모리 관리
- 원시 타입: 원시 타입(string, number, boolean, null, undefined, symbol, bigint)은 값 자체가 변수에 저장된다. 변수에 다른 원시 값을 할당하면 이전 값은 덮어써진다.
- 참조 타입: 참조 타입(object, array, function 등)은 실제 객체는 힙(heap) 메모리에 저장되고, 변수에는 그 객체의 메모리 주소가 저장된다.
let x = { name: "John" };
let y = x; // y는 x와 같은 객체를 참조
y.name = "Jane";
console.log(x.name); // "Jane" - x와 y는 같은 객체를 참조하기 때문
가비지 컬렉션
자바스크립트는 자동으로 더 이상 참조되지 않는 객체를 메모리에서 해제하는 가비지 컬렉션을 수행한다. 이를 "도달 가능성(reachability)" 알고리즘이라고 한다.
let obj = { data: "some data" };
// 여기서 obj는 객체를 참조
obj = null;
// obj가 더 이상 객체를 참조하지 않으므로, 그 객체는 가비지 컬렉션의 대상이 됨
클로저와 메모리 누수
클로저는 함수가 외부 환경의 변수를 참조할 수 있게 해주는데, 이로 인해 의도치 않은 메모리 누수가 발생할 수 있다.
function setupHandler() {
let element = document.getElementById("button");
let counter = 0;
element.addEventListener("click", function() {
console.log(++counter);
});
}
이 코드에서 클로저(이벤트 핸들러 함수)는 counter 변수를 참조하고 있다. 만약 이벤트 리스너를 제거하지 않으면, counter 변수는 가비지 컬렉션의 대상이 되지 않아 메모리 누수가 발생할 수 있다.
메모리 누수를 방지하려면 다음과 같이 이벤트 리스너를 제거해야 한다:
function setupHandler() {
let element = document.getElementById("button");
let counter = 0;
const clickHandler = function() {
console.log(++counter);
};
element.addEventListener("click", clickHandler);
// 나중에 이벤트 리스너 제거
return function cleanup() {
element.removeEventListener("click", clickHandler);
element = null; // DOM 요소에 대한 참조도 제거
};
}
const cleanup = setupHandler();
// 필요 없어지면 cleanup 함수 호출
// cleanup();
이터러블과 메모리 효율성
ES6에서 도입된 이터러블(iterable)과 제너레이터(generator)는 메모리 효율적인 데이터 처리를 가능하게 한다.
function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}
const iterator = infiniteSequence();
console.log(iterator.next().value); // 0
console.log(iterator.next().value); // 1
이 제너레이터 함수는 무한한 숫자 시퀀스를 생성하지만, 모든 값을 메모리에 저장하지 않고 필요할 때만 값을 생성한다. 이는 대용량 데이터를 처리할 때 매우 유용하다.
비동기 코드에서의 this 바인딩
비동기 코드에서 this 바인딩은 특히 주의해야 할 부분이다. 콜백 함수는 원래 컨텍스트와 다른 컨텍스트에서 실행될 수 있기 때문이다.
const user = {
name: "Alice",
greetLater: function() {
setTimeout(function() {
console.log(`Hello, my name is ${this.name}`);
}, 1000);
}
};
user.greetLater(); // "Hello, my name is undefined"
이 문제는 화살표 함수나 bind 메서드를 사용하여 해결할 수 있다:
// 화살표 함수 사용
const user1 = {
name: "Alice",
greetLater: function() {
setTimeout(() => {
console.log(`Hello, my name is ${this.name}`);
}, 1000);
}
};
// bind 메서드 사용
const user2 = {
name: "Bob",
greetLater: function() {
setTimeout(function() {
console.log(`Hello, my name is ${this.name}`);
}.bind(this), 1000);
}
};
user1.greetLater(); // "Hello, my name is Alice"
user2.greetLater(); // "Hello, my name is Bob"
결론: 자바스크립트의 유연한 객체 시스템
자바스크립트의 this, 프로토타입, 메모리 관리는 처음에는 복잡하게 느껴질 수 있지만, 그 원리를 이해하면 매우 강력하고 유연한 시스템임을 알 수 있다.
- this 키워드는 호출 시점에 결정되며, 함수가 어떻게 호출되는지에 따라 달라진다.
- 프로토타입은 객체 간 상속 관계를 구현하는 자바스크립트만의 방식이다.
- 메모리 관리는 대부분 자동으로 이루어지지만, 참조 타입과 클로저를 사용할 때는 메모리 누수에 주의해야 한다.
- 클로저는 함수와 그 함수가 선언된 환경의 조합으로, 강력한 캡슐화를 가능하게 한다.
- 이터러블과 제너레이터는 메모리 효율적인 데이터 처리를 가능하게 한다.
이러한 개념들은 서로 밀접하게 연결되어 있으며, 자바스크립트의 특성을 이해하는 데 핵심이다. 특히 this와 프로토타입은 자바스크립트만의 독특한 개념이기 때문에, 다른 언어에서 온 개발자들이 자주 혼란을 겪는 부분이기도 하다.
하지만 이제 이런 개념들의 동작 원리를 더 명확하게 이해했으니, 자바스크립트의 강력한 기능들을 더 효과적으로 활용할 수 있을 것이다.
'잡담' 카테고리의 다른 글
You Don't Know JS Yet을 읽고: ES6 모듈에서 class를 사용을 지양하는 이유 (1) | 2025.03.02 |
---|---|
You Don't Know JS Yet을 읽고: 자바스크립트 타입 비교의 오해와 진실 (0) | 2025.03.02 |
프로젝트 자가진단 체크리스트 (2) | 2023.02.03 |