이 게시물은 SW마에스트로 연수생 간 진행한 Modern JavaScript DeepDive 스터디 개인 정리 내용입니다.
되도록 책의 내용을 그대로 적기보단 개인적으로 인상깊었던 부분을 구어체로 풀어쓰려 노력했습니다.
Modern JavaScript Deep Dive
Chapter _ 19 Prototype
JavaScript 는 멀티 패러다임 프로그래밍 언어다
JS 는 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티 패러다임 언어입니다.
객체지향 프로그래밍의 근본인 클래스 기반 객체지향 프로그래밍 언어 C++ 이나 JAVA 와 비교하여 클래스, 상속, 캡슐화 키워드(private, public, protected)가 없어서 오해를 받지만, 아주 조금 결이 다른 프로토타입 기반 객체지향 프로그래밍 언어라는 점은 변함 없습니다.
클래스보다 더 강력하고 효율적이라고 주장하나,
이건 아직 잘 모르겠고 ES6 와 함께 클래스도 도입되어 나중에 25장에서 다룰 예정입니다.
JS 의 거의 모든 것은 객체입니다. 원시타입을 제외하면 말이죠.
객체지향 프로그래밍
객체지향 프로그래밍은 과거 절차지향적 관점에서 벗어나 객체의 집합으로 프로그램을 표현하는 프로그래밍 패러다임을 말합니다.
또한 객체지향 프로그래밍은 조금 더 인간의 관점에서 프로그래밍을 바라보며 시작되었습니다.
단순한 데이터에 인간이 실체를 인식하는 사고를 접목하여, 실체를 나타내는 속성(attribute, property)를 설정하고 이를 통해 인식&구별합니다.
추상화
객체지향에서 빼놓을 수 없는 키워드 추상화입니다.
앞서 실체를 나타내는 속성을 설정한다고 했는데 책의 예시를 인용하자면,
사람(데이터)은 { 이름, 주소, 성별, 나이, 신장, 체중, … } 등 다양한 속성(attribute, property)을 가집니다. 속성을 설명하기위해 예를 들자면 데이터를 설명할 수 있는 항목들이라고 할 수 있을 것 같습니다.
여기서 우리가 구현하려는 프로그램에서는 사람(데이터)의 이름과 주소에만 관심이 있다면, 앞선 여러가지 속성들 중에서 필요 속성만 간추려 내는 과정을 추상화 라고 표현합니다.
프로퍼티 & 메서드
앞선 예시처럼 속성들을 추상화해서 하나의 단위로 구성하는 자료구조를 객체라고 하며, 객체지향 프로그래밍은 이런 독립적인 객체 들을 사용해서 프로그램을 표현하는 패러다임인 것입니다.
여기서 단순히 값만 존재한다면 프로그램은 동작하지 않을 것입니다.
그렇기에 객체지향 프로그래밍은 객체를 상태와 상태를 조작하는 동작 으로 나누고 그 둘을 하나의 논리 단위로 묶어서 처리합니다.
따라서 정리하자면, ‘객체는 상태데이터와 동작을 하나의 논리적 단위로 묶은 복합적 자료구조다.’ 라고 정리할 수 있겠습니다.
여기서의 상태를 프로퍼티, 동작을 메서드 라고 부릅니다.
상속과 프로토타입
상속은 객체지향 프로그래밍의 핵심 개념으로, 어떤 객체의 프로퍼티나 메서드를 다른 객체가 내려받아 그대로 사용할 수 있는 것을 의미합니다.
내려받는다는 행위 자체는 동일한 동작을 기대할 수 있을 뿐만 아니라 불필요한 중복을 제거하는 효과 또한 기대할 수 있습니다.
1
2
3
4
5
6
7
8
9
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * this.radius ** 2;
}
}
const c1 = new Circle(1);
const c2 = new Circle(2);
위와같은 예시 코드가 존재할 때 Circle 함수는 radius 라는 프로퍼티를 갖고, getArea 라는 메서드를 갖습니다.
여기서 radius 프로퍼티는 매번 달라질 수 있지만, getArea 는 고정적이며, 각각의 인스턴스가 중복소유 합니다. 이로 인해 메모리는 불필요하게 낭비되겠죠?
이러한 불필요한 중복을 JS 에서는 프로토타입 기반 상속을 통해 효과적으로 제거할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.getArea = function () {
return Math.PI * this.radius ** 2;
}
const c1 = new Circle(1);
const c2 = new Circle(2);
프로토타입이요? 그게 뭔데요?
순서대로 읽고 계시다면, 모르는게 맞습니다. 가장 처음에 이름만 나왔지 뭔지는 전혀 설명하지 않았으니까요.
지금까지는 단순히 비슷한 계열끼리 상속해주는 무언가 라고만 이해하시면 될 것 같습니다.
Circle 생성자 함수가 생성하는 모든 인스턴스는 Circle 의 모든 프로퍼티와 프로토타입, 메서드를 상속받습니다.
여기서 getArea 는 앞선 방식과는 다르게 단 하나만 생성되어 Circle의 프로토타입 메서드로 할당되어있습니다. Circle 생성자의 인스턴스들은 해당 메서드를 상속받아 사용하기 때문에 코드 재사용 관점에서 좋습니다.
프로토타입 객체
프로토타입 객체는 JS 에서 객체 간 상속을 구현하기 위해 사용됩니다.
프로토타입은 어떤 객체의 부모 객체 역할을 하는 객체로서, 다른 객체에 공유 프로퍼티&메서드를 제공합니다.
즉, 프로토타입을 상속받은 자식 객체는 부모 객체의 프로토타입을 자신의 것 처럼 사용할 수 있습니다.
Chapter 16 - 프로퍼티 어트리뷰트에서 내부슬롯 이라는 개념에 대해 알아봤었습니다.
( 추가로, 내부 슬롯에는 직접 접근할 수 없지만, __proto__ 와 같은 접근자 프로퍼티를 통해 간접적으로 접근할 수 있다는 것도 알 수 있었습니다. )
모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가지며, 슬롯에 저장되는 프로토타입은 객체 생성 방식에 의해 결정됩니다.
만약 생성자 함수에 의해 생성된다면, 앞서 Circle 함수에서 다뤘듯이 생성자 함수는 모든 프로토타입을 상속합니다. 즉, 모든 프로토타입은 생성자 함수와 연결되어 있습니다.
또 다른 방법에 의해 생성되는 것은 이번 챕터 뒷부분에 알아보도록 하겠습니다.
__proto__접근자 프로퍼티
- 모든 객체는
__proto__접근자 프로퍼티를 통해 자신의 프로토타입 내부슬롯에 간접적으로 접근할 수 있습니다. __proto__접근자 프로퍼티는 객체가 소유하는 프로퍼티가 아니라 Object.prototype 의 프로퍼티입니다. 즉, 상속을 통해__proto__접근자 프로퍼티를 사용하는 것입니다.__proto__접근자 프로퍼티를 사용하는 이유는, 상호참조에 의한 체이닝 생성의 방지를 위해서 입니다.- 만약 부모가 자식의 프로토타입을 자식이 부모의 프로토타입을 참조한다면 체이닝이 발생하기 때문에 단방향 연결리스트로 구현되어야 합니다.
- 그래서
__proto__접근자 프로퍼티로 검증을 거쳐 접근하고 교체하도록 구현되어있습니다.
__proto__접근자 프로퍼티를 직접 사용하는 것은 권장되지 않습니다.- ES5 까지는
__proto__접근자 프로퍼티는 비표준이었습니다. 하지만 ES6 에서 표준이 되었고, 현재 대부분의 브라우저는__proto__접근자 프로퍼티를 지원합니다. - 하지만
__proto__접근자 프로퍼티를 직접 사용하는 것은 권장되지 않으며,Object.getPrototypeOf메서드와Object.setPrototypeOf메서드의 사용이 권장됩니다. 참고로 두 메서드 모두 ES5 에 도입된 메서드입니다. - 권장하지 않는 이유는, non-standard한 ‘느낌’ 이 있어서 라고 합니다. ES6 이전까지
__proto__접근자 프로퍼티는 비표준이었고 그러한 인식이 남아있어서 단순히 권장하진 않는다. 정도인 듯 싶습니다.
- ES5 까지는
함수 객체의 prototype 프로퍼티
함수 객체만이 소유하는prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킵니다.
즉, 생성자 함수로서 호출할 수 없는 함수인 화살표 함수와 메서드 축약표현 메서드는 prototype 프로퍼티를 소유하지 않으며 프로토타입도 생성하지 않습니다.
정보의 나열같은 느낌이 있지만, 마지막으로 모든 객체가 가지고있는 __proto__ 접근자 프로퍼티와 함수 객체만 가지고 있는 prototype 프로퍼티는 결국 같은 프로토타입을 가리킵니다.
하지만 이 둘의 차이점은 사용되는 주체가 다르고 사용되는 방법이 다르다는 점입니다.
__proto__ 접근자 프로퍼티는 모든 객체가 가지고 있어서 객체 자신이 사용하며 프로토타입 체이닝을 위해 사용됩니다. ( 체이닝은 추후 다시 나옵니다! )
prototype 프로퍼티는 생성된 인스턴스들이 사용하며 객체 생성 및 상속을 위해 사용됩니다.
프로토타입의 constructor 프로퍼티와 생성자 함수
모든 프로토타입은 constructor 프로퍼티를 갖습니다.
이 constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킵니다.
따라서 다음과 같은 플로우가 형성됩니다.
생성자 함수 ( prototype이 constructor프로퍼티를 참조하고 있음 ) →
생성자 함수가 새로운 인스턴스 생성 ( 이 인스턴스 프로토타입에도 constructor 프로퍼티가 필요함 ) →
__proto__ 접근자 프로퍼티 로 생성자 함수의 prototype 프로퍼티에 접근 →
생성자 함수의 prototype 프로퍼티에 있는 constructor 프로퍼티를 상속받아 사용
리터럴 표기법으로 생성된 객체의 생성자함수와 프로토타입
이번엔 생성자 함수로 생성된 것이 아닌 리터럴 표기법으로 생성된 객체의 생성자함수와 프로토타입에 대해서 알아보도록 하겠습니다.
리터럴 표기법에 의해 생성된 객체도 당연히 프로토타입은 존재합니다. 하지만 다른 점이라면 constructor 프로퍼티가 가리키는 생성자 함수가 반드시 객체를 생성한 생성자 함수라고는 단정할 수 없습니다.
ES11 문서에 따르면 Object 생성자 함수 호출이나 객체 리터럴은 추상연산을 호출해 객체를 생성하는 점은 동일하나, 프로퍼티를 추가하는 세부 내용이 다르다고 합니다.
따라서 완전히 같은가? 라는 질문에는 ‘아니다’ 라는 결론이 나온다고 합니다.
함수 객체의 경우엔 Function 생성자 함수가 아닌 함수 선언문과 표현식을 통해 함수 객체를 생성한 경우 렉시컬 스코프와 클로저를 가지므로, Function 생성자 함수로 생성한 것과는 명확한 차이가 있습니다.
하지만 constructor 프로퍼티를 확인해보면 Function 생성자 함수를 가리킵니다.
이것은 프로토타입과 생성자 함수는 언제나 쌍으로 존재해야하기 때문에 가상의 생성자 함수를 갖게 되었기 때문입니다.
이 외에도 배열이나 정규표현식에서도 작은 차이가 있습니다. 하지만 각각의 객체로서 거의 동일한 특성을 갖기때문에 생성자 함수로 생성한 객체와 같다고 지레짐작해도 무리는 없습니다.
하지만 유사한 것과 동일한 것은 명확히 다른 부분이기 때문에 짚고 넘어가는것이 좋습니다.
| 리터럴 표기법 | 생성자 함수 | 프로토타입 |
|---|---|---|
| 객체 리터럴 | Object | Object.prototype |
| 함수 리터럴 | Function | Function.prototype |
| 배열 리터럴 | Array | Array.prototype |
| 정규표현식 리터럴 | RegExp | RegExp.prototype |
( 리터럴 표기법으로 생성된 객체의 생성자 함수와 프로토타입 표 )
프로토타입의 생성 시점
프로토타입과 생성자 함수는 언제나 쌍으로 존재해야하기 때문에 프로토타입은 생성자 함수가 생성되는 시점에 생성됩니다.
여기서 생성자 함수는 크게 두 가지 사용자 정의 생성자 함수와 JS 빌트인 생성자 함수로 구분할 수 있습니다.
사용자 정의 생성자 함수와 프로토타입 생성 시점
화살표 함수나 메서드 축약표현으로 정의하지 않은 일반 함수는 new 연산자를 사용해서 생성자 함수로 호출할 수 있습니다.
생성자 함수로 호출할 수 있는 함수의 constructor 는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성됩니다.
함수 선언문은 런타임 이전에 JS 엔진에 의해 먼저 실행되고, 더불어 프로토타입이 생성되어 Person 생성자 함수의 prototype 프로퍼티에 바인딩 된다.
빌트인 생성자 함수와 프로토타입 생성 시점
모든 빌트인 생성자 함수는 전역객체(window)가 생성되는 시점에 생성됩니다.
즉, 객체가 생성되기도 전에 생성자 함수와 프로토타입은 객체화 되어 존재합니다.
이후 생성자함수 또는 리터럴 표기법으로 객체를 생성하면 프로토타입이 생성된 객체의 [[Prototype]] 내부슬롯에 할당됩니다.
객체 생성 방식과 프로토타입의 결정
객체 생성 방식은 여러가지가 있으나, 공통점은 추상연산 방식에 의해 생성된다는 점입니다.
추상연산은 필수적으로 자신이 생성할 객체의 프로토타입을 인수로 전달받습니다. 그리고 자신이 생성할 객체에 추가될 프로퍼티 목록을 옵션으로 전달 받습니다.
우선 추상연산은 빈 객체를 생성하고 프로퍼티 목록을 객체에 추가 한 이후 필수 인자로 전달받은 프로토타입을 [[Prototype]] 내부슬롯에 할당 한 이후 생성된 객체를 반환합니다.
즉, 프로토타입은 추상연산에 전달되는 인수에 의해 결정됩니다.
객체 리터럴에 의해 생성된 객체의 프로토타입
객체 리터럴은 평가되어 생성할 때 추상연산을 호출합니다. 이 경우 추상연산에 전달되는 프로토타입은 Object.prototype 이기 때문에 생성되는 객체의 프로토타입 또한 Object.prototype이 됩니다.
Object 생성자 함수에 의해 생성된 객체의 프로토타입
Object 생성자 함수를 인수 없이 호출하면 빈 객체가 호출되며 추상연산이 호출됩니다.
이 때, 객체리터럴과 마찬가지로 추상연산에 전달되는 프로토타입은 Object.prototype이기 때문에 생성되는 객체의 프로토타입도 Object.prototype이 됩니다.
이 경우는 사실상 객체 리터럴 방식과 동일한 구조를 갖습니다.
생성자 함수에 의해 생성된 객체의 프로토타입
new 연산자와 함께 생성자 함수를 호출하여 인스턴스를 생성하면 역시나 추상연산이 호출됩니다.
하지만 이 경우 전달되는 프로토타입이 생성자 함수의 prototype 프로퍼티에 바인딩되어있는 객체입니다.
프로토타입 체이닝
생성자 함수에 의해 생성된 객체는 생성자 함수의 프로토타입 메서드 뿐만아니라 Object.prototype 의 메서드까지 사용할 수 있습니다.
이것은 생성자 함수의 프로토타입 뿐만 아니라 Object.prototype 또한 상속받았음을 의미합니다.
하지만 getPrototypeOf 메서드를 통해 프로토타입을 체크해보면 생성자함수의 프로토타입이 반환됩니다.
어떻게 된걸까요?
JS 는 객체의 프로퍼티 혹은 메서드에 접근할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부슬롯의 참조를 따라 부모 역할을 하는 프로토타입의 프로퍼티를 마치 스코프를 순회하듯 순차적으로 검색합니다.
이것을 프로토타입 체이닝이라고 칭하며, 이것이 바로 JS 가 객체지향 프로그래밍의 상속과 프로퍼티 검색을 구현하는 매커니즘입니다.
여기서 프로토타입 체이닝의 최상위에 위치하는 객체는 언제나 Object.prototype 입니다. 따라서 Object.prototype 를 프로토타입 체인의 종점이라고 칭합니다.
프로토타입 체이닝은 프로퍼티가 없을 때 [[Prototype]] 내부슬롯의 참조를 따라간다고 했는데, 따라서 Object.prototype 의 [[Prototype]] 은 더이상 참조할 게 없는 null 이 설정되어있습니다.
참고할 부분은 체인의 종점에서도 프로퍼티가 검색되지 않는다면 에러가 아닌 undefined 를 반환한다는 점을 주의해야합니다.
오버라이딩과 프로퍼티 섀도잉
상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의 하여 사용하는 방식을 오버라이딩이라고 합니다.
위의 사진에서 Person 의 프로토타입 메서드 sayHello 가 존재하지만 me의 인스턴스 메서드 sayHello 가 오버라이딩 되며 프로토타입 메서드가 덮어씌워지는 현상이 발생했습니다. 이러한 클래스가 아닌 상속에 의해 덮어씌워지는 현상을 프로퍼티 섀도잉 이라고 칭합니다.
프로토타입의 교체
프로토타입은 객체의 특성과 마찬가지로 임의의 다른 객체로 변경할 수 있습니다. 이는 객체간의 상속 관계를 동적으로 변경할 수 있다는 뜻 입니다.
생성자 함수에 의한 프로토타입 교체
생성자 함수 객체를 프로토타입으로 교체하게 되면 생성자 함수에는 constructor 객체가 없기 때문에 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴되며, 해당 객체의 constructor 를 찾아보게 되면 Object.prototype 이 반환됩니다. 이 경우엔 교체된 생성자 함수 객체에 constructor 프로퍼티를 추가해서 연결을 되살려줘야 합니다.
인스턴스에 의한 프로토타입 교체
프로토타입을 교체하는 것 뿐만 아니라 __proto__ 접근자 프로퍼티를 통해 프로토타입에 접근할 수 있었습니다.
접근 뿐만 아니라 __proto__ 접근자 프로퍼티를 활용해서 프로토타입을 교체도 할 수 있습니다. 당연하겠지만, __proto__ 접근자 프로퍼티를 통해 프로토타입을 교체하는 것은 이미 생성된 객체의 프로토타입을 교체한다는 의미입니다.
이 또한 생성자 함수와 마찬가지로 constructor 프로퍼티와 생성자 함수 간 연결이 파괴되지만 차이점은 인스턴스에 의한 경우 생성자 함수의 prototype 프로퍼티가 교체된 프로토타입을 가리키지 않는다는 점 입니다.
이렇게 두 가지 연결이 파괴되는 경우는 그 연결을 되살리는 게 상당히 번거롭습니다. 따라서 직접 교체보단 추후에 나올 직접 상속을 사용하는 것이 정신건강에 이롭다고 합니다.
Instanceof 연산자
Instanceof 연산자는 이항 연산자로서 우변 생성자 함수의 prototype에 바인딩 된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true를, 아니라면 false로 평가됩니다.
1
2
3
4
5
6
7
8
function Person(name) {
this.name = name;
}
const me = new Person('CSKIM')
console.log(me instanceof Person) // true
console.log(me instanceof Object) // true
위의 예제를 보면 이해가 빠를 것 같습니다.
Person.prototype 이 me 의 프로토타입 체인상에 존재하기때문에 true 로 평가, Object.prototype 도 마찬가지이므로 true 로 평가됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
function Person(name) {
this.name = name;
}
const me = new Person('CSKIM')
const parent = {}
Object.setPrototypeOf(me, parent)
console.log(me instanceof Person)//false
console.log(me instanceof Object)//true
만약 의도적으로 프로토타입의 연결을 파괴한다면 분명히 me 는 Person에 의해 생성된 인스턴스이지만 false가 반환되는 것을 확인할 수 있습니다.
이 또한 이젠 더이상 Person.prototype이 me 의 프로토타입 체인에 존재하지 않기 때문입니다.
직접 상속
Object.create 를 사용한 직접 상속
Object.create는 다른 객체 생성 방식과 마찬가지로 추상연산을 호출합니다. 하지만 Object.create 메서드는 두 개의 매개변수를 받는데, 첫 째로는 생성할 객체의 프로토타입으로 지정할 객체를, 두 번째 인수로는 생성할 프로퍼티 키와 프로퍼티 디스크립터 객체로 이뤄진 객체를 전달합니다.
두 번째 인수는 다른 방식들과 마찬가지로 생략이 가능합니다.
Object.create 의 가장 큰 특징은 첫 번째 매개변수의 객체 프로토타입 체인에 속하는 객체를 생성해준다는 뜻 입니다.
이는 new 연산자 없이도 객체를 생성하고 프로토타입을 지정해서 상속받을 수 있으며, 심지어 객체 리터럴도 상속받을 수 있습니다.
객체 리터럴 내부에서의 proto 에 의한 직접 상속
앞선 Object.create 는 두 번째 인자로 프로퍼티를 정의해서 전달하는게 상당히 번거로운데, __proto__ 접근자를 객체 리터럴 내부에서 사용해서 직접상속도 가능합니다.
1
2
3
4
5
6
7
8
9
const myProto = {x:10}
const obj = {
y:20,
__proto__:myProto // 접근자를 통한 객체 직접 상속
}
console.log(obj.x , obj.y) // 10, 20
console.log(Object.getPrototypeOf(obj) === myProto) // true
정적 프로퍼티 & 메서드
정적 프로퍼티와 메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조와 호출을 할 수 있는 프로퍼티와 메서드를 뜻합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hi MY NAME IS ${this.name}`)
}
Person.staticProp = "static prop"
Person.staticMethod = function () {
console.log('staticMethod')
}
const me = new Person('Lee')
Person.staticMethod();
me.statiMethod()
위 예제에서 생성자 함수는 객체이므로, 프로퍼티와 메서드를 소유할 수 있습니다.
Person 생성자 함수 객체가 소유한 프로퍼티와 메서드를 정적 프로퍼티/메서드 라고 합니다.
정적 프로퍼티, 메서드는 생성자 함수가 생성한 인스턴스로 참조 호출할 수 없습니다.
당연한 얘기겠지만 생성자 함수가 생성한 인스턴스는 자신의 프로토타입 체인에 속한 객체에 접근할 수 있는것이지, 정적 메서드와 프로퍼티에 접근할 수 있는게 아니기 때문입니다.
프로퍼티 존재 확인
in 연산자
in 연산자는 객체 내에 특정 프로퍼티 존재 여부를 확인합니다.
1
2
3
4
5
/**
* key : 프로퍼티 키를 나타내는 문자열
* object: 객체로 평가되는 표현식
*/
key in object
주의 할 점은 in 연산자의 경우 확인 대상 객체의 프로퍼티 뿐만 아니라 확인 대상 객체가 상속받는 모든 프로토타입의 프로퍼티를 확인합니다.
object 객체에는 toString이라는 프로퍼티가 없지만 Object.prototype 에 위치하므로 true 를 반환합니다.
Object.prototype.hasOwnProperty 메서드
Object.prototype.hasOwnProperty 메서드를 사용해도 마찬가지로 프로퍼티의 존재 여부를 알 수 있습니다.
다만 차이점은, 인수로 전달 받은 프로퍼티 키가 객체 고유 프로퍼티 키일 경우에만 true를 반환하고 그 외엔 false 를 반환합니다.
프로퍼티 열거
for … in 문
순회문에서는 앞선 Chapter 16 에서 다뤘던 [[Enumerable]] 프로퍼티 어트리뷰트를 우선 확인하고 넘어가야 합니다.
[[Enumerable]] 프로퍼티 어트리뷰트는 프로퍼티가 열거 가능한 요소인가를 나타내는 어트리뷰트로서, true 인 경우에만 for 문에서 순회문에 포함됩니다.



