JavaScript에는 객체 지향 프로그래밍(OOP)을 위한 기능이 있어요. 객체 지향 프로그래밍에서는 특정 데이터와 기능을 담은 객체(인스턴스)를 "템플릿"(클래스)으로부터 만들어요. 객체 지향 맥락에서 데이터 속성은 필드, 함수 속성은 _메서드_라고 해요.
JavaScript는 2015년에 언어 명세에 클래스가 추가되기 전까지 클래스가 아예 없었지만, 프로토타입 기반 상속으로 객체 지향 프로그래밍을 할 수 있었어요.
그리고 오늘날에는 class 키워드를 쓸 수 있지만, JavaScript는 여전히 프로토타입 기반 언어예요.
프로토타입 기반 언어라는 것이 무슨 뜻인지, JavaScript가 실제로 어떻게 동작하는지 이해하기 위해 클래스가 없던 시절로 돌아가 볼까요?
JavaScript에서는 일반 함수로 템플릿(클래스)을 만들어요.
함수를 이런 템플릿으로 사용할 때는 _생성자 함수_라고 하고, 관례상 함수 이름을 대문자로 시작해요.
생성자 함수를 호출할 때 new 키워드를 사용하면 템플릿으로부터 인스턴스(객체)가 만들어져요.
function Car() {
// ...
}
const myCar = new Car();
const yourCar = new Car();
JavaScript에서는 인스턴스를 만든 뒤에도 인스턴스와 생성자 함수가 서로 관계를 유지한다는 점이 중요해요.
모든 인스턴스 객체에는 언어 명세에서 [[prototype]]이라고 부르는 숨겨진 내부 속성이 있어요.
이 속성은 생성자 함수의 prototype 키 값을 참조해요.
네, 맞아요. JavaScript 함수는 내부적으로 객체이기도 하기 때문에 키/값 쌍을 가질 수 있어요.
정리하면 다음과 같아요:
prototype 속성이 인스턴스의 _프로토타입_이 돼요.파생된 객체(인스턴스) 모두가 몇 가지 필드를 갖고, 객체를 만들 때 그 필드에 초깃값을 넘겨주고 싶을 때가 많아요.
이때 this 키워드를 사용할 수 있어요.
생성자 함수 안에서 this는 new로 만들어질 새 객체를 나타내요.
생성자 함수를 new와 함께 호출하면 this가 자동으로 반환돼요.
즉, 생성자 함수에서 this에 필드를 추가하면 새 인스턴스에 필드를 추가할 수 있어요.
function Car(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false
메서드는 생성자 함수의 prototype 속성을 통해 추가해요.
메서드 안에서는 this를 통해 인스턴스의 필드에 접근할 수 있어요.
이것이 가능한 이유는 다음과 같은 일반 규칙 때문이에요.
함수가 객체의 메서드로 호출되면, 그 함수의
this는 메서드가 호출된 객체로 설정돼요. 1
function Car() {
this.engineRunning = false;
// ...
}
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
Car.prototype.addGas = function (litre) {
// ...
};
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
위 예시의 myCar는 평범한 JavaScript 객체인데, 이 객체를 살펴보면(예를 들어 브라우저 콘솔에서) myCar 객체 안에서 직접 startEngine이라는 속성과 그 값인 함수를 찾을 수 없어요.
그럼 위 코드는 대체 어떻게 동작하는 걸까요?
그 비밀은 바로 _프로토타입 체인_이에요.
객체의 속성(필드나 메서드)에 접근하려고 하면, JavaScript는 먼저 그 키가 객체 자체에 직접 있는지 확인해요.
없으면 원래 객체의 [[prototype]] 속성이 참조하는 객체에서 키를 계속 찾아요.
앞서 말했듯이, 예시에서 [[prototype]]은 생성자 함수의 prototype 속성을 가리켜요.
우리가 거기에 startEngine 함수를 추가했기 때문에 JavaScript는 바로 그곳에서 이 함수를 찾아요.
function Car() {
// ...
}
Car.prototype.startEngine = function () {
// ...
};
그리고 체인은 거기서 끝나지 않아요.
Car.prototype의 [[prototype]] 속성(myCar.[[prototype]].[[prototype]])은 Object.prototype(Object 생성자 함수의 prototype 속성)을 참조해요.
여기에는 모든 JavaScript 객체에서 쓸 수 있는 일반 메서드, 예를 들어 toString()이 들어 있어요.
결국 myCar.toString()을 호출할 수 있는데, JavaScript가 프로토타입 체인 전체에서 그 메서드를 찾기 때문에 이 메서드가 존재하는 거예요.
프로토타입 체인은 값을 읽을 때만 따라간다는 점에 주의해요. 인스턴스 객체의 속성을 직접 설정하거나 삭제해도 그 특정 인스턴스에만 적용돼요. 클래스 기반 상속을 사용하는 언어에 익숙하다면 예상과 다를 수 있어요.
오늘날 JavaScript는 class 키워드로 클래스를 정의하는 것을 지원해요.
이 문법은 2015년에 언어 명세에 추가됐어요.
한편으로는 클래스를 더 읽고 쓰기 쉽게 만들어 주는 문법적 설탕을 제공해요.
새 문법은 C++나 Java 같은 언어에서 클래스를 작성하는 방식과 더 비슷해요.
그런 언어에서 넘어온 개발자는 더 쉽게 적응할 수 있어요.
다른 한편으로는, 클래스 문법이 프로토타입 문법에서는 쓸 수 없는 새로운 언어 기능의 길을 열어 줘요.
새 문법에서는 class 키워드 다음에 클래스 이름과 중괄호로 묶인 클래스 본문을 써서 클래스를 정의해요.
본문에는 생성자 함수의 정의, 즉 constructor라는 이름의 특별한 메서드가 들어 있어요.
이 함수는 프로토타입 문법의 생성자 함수와 똑같이 동작해요.
클래스 본문에는 모든 메서드 정의도 들어 있어요.
메서드 문법은 객체 안에 함수를 값으로 추가할 때 본 축약 표기법과 비슷해요. 자세한 내용은 객체 개념을 참고해요.
class Car {
constructor(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
startEngine() {
this.engineRunning = true;
}
addGas(litre) {
// ...
}
}
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
내부적으로 JavaScript는 여전히 프로토타입 기반 언어라는 점을 기억해요. 위의 "프로토타입 문법" 섹션에서 배운 모든 메커니즘이 그대로 적용돼요.
JavaScript에서 모든 인스턴스 필드는 기본적으로 외부에 공개돼요. 필드에 직접 접근하고 값을 할당할 수 있어요.
그런데 밑줄로 시작하는 필드와 메서드는 비공개로 취급하는 것이 확립된 관례예요. 이런 필드와 메서드는 클래스 외부에서 절대 직접 접근하면 안 돼요.
비공개 필드에는 게터와 세터가 함께 붙기도 해요.
get과 set 키워드를 사용하면, 함수와 같은 이름의 속성에 접근하거나 값을 할당할 때 실행되는 함수를 정의할 수 있어요.
class Car {
constructor() {
this._mileage = 0;
}
get mileage() {
return this._mileage;
}
set mileage(value) {
throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
// Just an example, usually you would not provide a setter in this case.
}
}
const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.
this 예시 - 객체 메서드로서, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (2021년 12월 03일 참조) ↩
이 연습 문제에서는 윈도우 기반 컴퓨터 시스템을 시뮬레이션해요. 이동하고 크기를 조절할 수 있는 윈도우를 몇 개 만들어 볼 거예요. 다음 그림은 아래에서 다룰 값들을 보여줘요.
<--------------------- screenSize.width --------------------->
^ ┌────────────────────────────────────────────────────────────┐
| │ │
| │ position.x, _ │
| │ position.y \ │
| │ \<----- size.width -----> │
| │ ^ *──────────────────────┐ │
| │ | │ title │ │
| │ | ├──────────────────────┤ │
screenSize.height │ | │ │ │
| │ size.height │ │ │
| │ | │ contents │ │
| │ | │ │ │
| │ | │ │ │
| │ v └──────────────────────┘ │
| │ │
| │ │
v └────────────────────────────────────────────────────────────┘
📣 다양한 JavaScript 실력을 연습해 보려면 1번과 2번 과제는 프로토타입 문법으로, 나머지 과제는 클래스 문법으로 풀어 봐요.
Size라는 클래스(생성자 함수)를 정의해요.
이 클래스에는 윈도우의 현재 크기를 저장하는 width와 height 필드가 있어야 해요.
생성자 함수는 이 필드들의 초깃값을 받아야 해요.
너비는 첫 번째 매개변수로, 높이는 두 번째 매개변수로 전달해요.
기본 너비와 높이는 각각 80과 60이어야 해요.
또한 새로운 너비와 높이를 매개변수로 받아 새 크기가 반영되도록 필드를 바꾸는 resize(newWidth, newHeight) 메서드를 정의해요.
const size = new Size(1080, 764);
size.width;
// => 1080
size.height;
// => 764
size.resize(1920, 1080);
size.width;
// => 1920
size.height;
// => 1080
Position이라는 클래스(생성자 함수)를 정의하고, 윈도우 왼쪽 위 모서리의 현재 가로 위치와 세로 위치를 각각 저장하는 x와 y 필드를 만들어요.
생성자 함수는 이 필드들의 초깃값을 받아야 해요.
x 값은 첫 번째 매개변수로, y 값은 두 번째 매개변수로 전달해요.
두 필드 모두 기본값은 0이어야 해요.
위치 (0, 0)은 화면의 왼쪽 위 모서리이고, 오른쪽으로 갈수록 x 값이 커지고 아래로 갈수록 y 값이 커져요.
또한 새로운 x와 y를 매개변수로 받아 새 위치가 반영되도록 속성을 바꾸는 move(newX, newY) 메서드도 정의해요.
const point = new Position();
point.x;
// => 0
point.y;
// => 0
point.move(100, 200);
point.x;
// => 100
point.y;
// => 200
다음 필드를 가진 ProgramWindow 클래스를 정의해요:
screenSize: width가 800이고 height가 600인 Size 타입의 고정된 값을 가져요size: Size 타입의 값을 가져요. 초깃값은 Size 인스턴스의 기본값이에요position: Position 타입의 값을 가져요. 초깃값은 Position 인스턴스의 기본값이에요윈도우는 열릴 때(생성될 때) 항상 기본 크기와 위치로 시작해요.
const programWindow = new ProgramWindow();
programWindow.screenSize.width;
// => 800
// Similar for the other fields.
참고: ProgramWindow라는 이름은 브라우저 환경에 내장된 Window 클래스와 구분하기 위해 Window 대신 사용해요.
ProgramWindow 클래스에는 resize 메서드를 넣어야 해요.
Size 타입의 매개변수를 입력으로 받아, 지정한 크기로 윈도우 크기를 조절하려고 시도해요.
하지만 새 크기는 특정 범위를 넘을 수 없어요.
x = 400, y = 300이고 height = 400, width = 300으로 크기 조절을 요청하면, 화면이 y 방향으로 요청을 모두 수용할 만큼 크지 않기 때문에 윈도우는 height = 300, width = 300으로 조절돼요.const programWindow = new ProgramWindow();
const newSize = new Size(600, 400);
programWindow.resize(newSize);
programWindow.size.width;
// => 600
programWindow.size.height;
// => 400
크기 조절 기능 외에도 ProgramWindow 클래스에는 move 메서드가 있어야 해요.
Position 타입의 매개변수를 입력으로 받아요.
move 메서드는 resize와 비슷하지만, 크기가 아니라 윈도우의 _위치_를 요청한 값으로 조정해요.
resize와 마찬가지로 새 위치도 특정 한계를 넘을 수 없어요.
x와 y 모두 최소 위치는 0이에요.x = 250, y = 100이고 x = 600, y = 200으로 이동을 요청하면, 화면이 x 방향으로 요청을 모두 수용할 만큼 크지 않기 때문에 윈도우는 x = 550, y = 200으로 이동해요.const programWindow = new ProgramWindow();
const newPosition = new Position(50, 100);
programWindow.move(newPosition);
programWindow.position.x;
// => 50
programWindow.position.y;
// => 100
ProgramWindow 인스턴스를 입력으로 받아 윈도우를 지정한 크기와 위치로 바꾸는 changeWindow 함수를 구현해요.
이 함수는 변경 사항을 적용한 뒤 전달받은 ProgramWindow 인스턴스를 반환해야 해요.
윈도우는 너비 400, 높이 300이 되고 x = 100, y = 150에 위치해야 해요.
const programWindow = new ProgramWindow();
changeWindow(programWindow);
programWindow.size.width;
// => 400
// Similar for the other fields.
Exercism에 가입하고 JavaScript 트랙을 개념 37개연습 문제 159개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.