JavaScript

[웹코딩 가이드] 홈

이제 웹프로그래밍 언어의 최고봉, 자바스크립트를 살펴봅니다 diagram-arrow-down

html은 웹문서의 내용을 구성하고 의미를 부여하는 데 사용하는 마크업 언어 이며, Css는 그 html 문서에 원하는 모양을 입혀주기 위한 스타일 규격 이다. 한편, javaScript는 웹문서에서 동적으로 추가, 제거되고 변경되는 컨텐츠를 만들고 조작할 수 있도록 해주는 스크립트 언어 이다


자바스크립트는 많이 어렵습니다. 순서대로만 설명할 수도 없고, 그렇게 배워나갈 수도 없습니다. 지나갔다, 다시 돌아오고,, 다시 나아가고,,, 하면서 수없는 반복을 통해 익혀나가야 합니다 ㅡㅡ; 러시아 혁명가 레닌이 쓴 유명한 팜플렛 [1보 전진, 2보 후퇴](= 한걸음 더 나아가기 위한 2보 후퇴)가 생각나는군요 혹시나 이 말에 의문을 품는 분이 계실까 해서 덧붙이는데.. [1보 후퇴 for 2보 전진]이 아닙니다 ^^


객체의 프로토타입

은 각 객체가 시스템의 특정 측면을 나타내는 객체 모음으로 시스템을 모델링하는 것으로서, 객체는 사용하려는 다른 코드에 대한 공용 인터페이스를 제공하지만 자체로는 비공개 내부 상태를 유지한다 따라서, 시스템의 다른 부분은 객체 내부에서 일어나는 일에 대해 신경쓸 필요가 없다!

객체의 프로토타입 체인
스크립트에서 모든 객체 인스턴스는 자신을 생성한 생성자함수의 prototype 이 가리키는 프로토타입 객체를 자신의 부모 객체로 설정하는 __proto__ 링크를 통해 연결되는데, 이러한 체인을 통해 그 최종 단계에 위치한 Object 객체의 메서드는 모든 내장 객체로 전파되며, 모든 인스턴스에서 사용할 수 있게 된다!
객체 생성자함수 또한 __proto__ 링크를 통해 모든 객체의 최상위 객체인 Object 객체로 연결되고(이는 연결을 통한 접근 이지 복사 가 아니다!), 이러한 프로토타입 체인을 통해 그 최종 단계에 위치한 Object 객체의 메서드와 프로퍼티는 모든 하위 객체로 전파되며, 모든 객체 인스턴스에서 사용할 수 있게 된다
[ 객체 생성자함수 정의 ]
                                        
                                            function Test(a, b, c, d) { // 생성자함수 Test 정의
                                                // 속성들 정의..
                                            } Test.prototype.x= function() { // 생성자함수 Test의 x() 메서드 정의
                                                // ..
                                            }
                                        
                                    

객체 인스턴스__proto__constructor.prototype__proto__Object.prototype 여기서 constructor 는 인스턴스 객체의 생성자함수를 가리킨다


* cf) 객체의 데이터 프로퍼티는 프로토타입 체인이 아니라 인스턴스에 정의해야 한다. 따라서, 항시 Object.hasOwn(인스턴스객체, 속성);(인스턴스객체 가 특정 속성을 자체적으로 가지고 있는가?)으로 체크해주는 것이 좋다!

                                    
                                        const ex= { Kjc: "jc", Kjh: "jh" }

                                        for (const name in ex) {
                                            if (Object.hasOwn(ex, name)) {
                                                console.log(name) // Kjc Kjh
                                            }
                                        }

                                        for (const name of Object.keys(ex)) {
                                            console.log(name) // Kjc Kjh
                                        }
                                    
                                

객체의 프로퍼티 나열 시, 위와 같이 Object.keys를 사용하면; 프로토타입 체인상에만 정의된(해당 객체 자체적으로는 보유하지 않은, 상속된) 프로퍼티를 건너뛸 수 있다!

new (와 객체 생성자함수)로 인스턴스 변수를 정의하면; 스크립트는 내부적으로, 우선 빈 객체를 생성하고(let 인스턴스= {}), constructor.prototype 을 인스턴스의 프로토타입으로 설정한다. 다음으로, 인스턴스를 초기화하는데, 이때 this 는 인스턴스에 바인딩되고 인수는 new 와 함께 사용한 인수를 그대로 사용하여(생성자함수.apply(인스턴스, argunents)) 완성된 인스턴스 객체를 반환한다(return 인스턴스객체;)
[ 객체의 프로토타입 체인 ]
                                        
                                            function Circle(center, radius) { // 객체 생성자함수 Circle()
                                                this.radius= radius // 이 this는 ins 인스턴스의 this다!
                                                this.area= function() {
                                                    return Math.PI * this.radius * this.radius;
                                                }
                                            }

                                            let ins= new Circle({x: 0, y: 0}, 2) // 객체 생성자함수 Circle의 인스턴스 변수 ins
                                            console.log(ins.area()) // 12.566370614359172
                                        
                                    

이렇게 인스턴스로부터 constructor.prototype 을 거쳐서 (모든 함수의 프로토타입은 Object 이므로)Object 객체로 이어지는 프로토타입 체인이 만들어지며, 이를 통해 인스턴스가 Object 객체의 프로퍼티를 사용할 수 있게 된다!

객체 리터럴에서의 프로토타입 체인
객체 리터럴 방식으로 만들어지는 인스턴스 객체{}__proto__Object.prototype 링크를 따라 Object.prototype이 자신의 프로토타입 객체가 된다. 따라서 모든 객체 인스턴스는 Object 객체가 지닌 스크립트 기본 내장 메서드와 프로퍼티를 이용할 수 있게 된다 단, Object 객체의 모든 메서드와 속성들을 상속받는 것은 아니고, Object.prototype 객체에 포함된 것들만이다 - 곧, prototype 속성은 상속시키려는 멤버들만 정의해둔 객체이다!
Object 객체는 모든 객체의 최상위 객체이므로 이 객체의 프로토타입에 속성이나 메서드를 추가하면 스크립트 내 모든 곳에서 사용할 수 있게 된다. 스크립트 기본 내장 객체인 Number, String, Array 등 또한 프로토타입 체인을 통해 모든 객체의 최종 프로토타입인 Object.prototype으로 연결되어 있다: []__proto__Array.prototype__proto__Object.prototype

* cf) 자바스크립트 표준 스펙에서 [[prototype]]으로 표현되는 프로토타입 객체에 대한 링크는 다수의 최신 브라우저들이 __proto__ 속성을 통해 특정 객체의 프로토타입에 접근할 수 있도록 구현하였는데, ES 6) 문법에서는 Object.getPrototypeOf(obj); 메서드를 통해 객체의 프로토타입에 바로 접근할 수 있다!

                                    
                                        let proto= {}

                                        let obj= Object.create(proto) // obj는 proto를 상속받는다
                                        Object.getPrototypeOf(obj) === proto // true
                                    
                                
객체 생성자함수와 this
객체 생성자함수는 객체를 생성하고 초기화하는데, 객체 생성자함수가 new로 호출되면; 빈 객체를 생성하여 프로토타입을 설정하고, 객체 초기화를 수행한 다음 자신을 호출한 인스턴스에게 돌려주게 된다. 곧, new로 특정 함수를 호출하게 되면; 해당 함수는 객체 생성자함수로 작동하게 되는데, 먼저 빈 객체를 생성한 다음 코드 내부에서 this 를 사용하여 동적으로 프로퍼티나 메서드를 생성하는 것이다 따라서, 생성자함수 내부에서 사용되는 this 는 이 빈 객체에 묶이게 된다!
                                    
                                        const Fnc= function(name) { // 이 함수는 new로 호출되면 객체 생성자함수가 된다!
                                            this.name= name,
                                            this.inFnc= function() {
                                                console.log(this.name)
                                            }
                                        } // 여기서 this는 자신을 호출한 인스턴스에 바인딩된다 ← 내부에서 정의한 메서드 안에서 사용되는 this 또한 마찬가지이다!

                                        const Kim= new Fnc('Kjc') // 인스턴스 변수객체 Kim
                                        Kim.inFnc() // Kjc
                                    
                                

함수는 반드시 리턴값 을 반환하는데.. return 문 없는 일반 함수에서는 undefined 를 반환하며, 객체 생성자함수에서는 별도의 return 문이 없으면 this 로 바운딩된 새로 생성된 객체가 반환된다 때문에 생성자함수에서는 일반적으로 리턴값을 지정하지 않는다!


* cf) 일반 함수 호출의 경우 thisWindow 전역객체에 바인딩되는 반면, 객체 생성자함수 호출의 경우에는 새로 생성되는 빈 객체에 바인딩된다는 점에서 함수 호출 시 new 키워드의 잘못된 사용으로 인해 문제를 일으킬 위험성이 생겨난다!

                                    
                                        function Car(maker, color) {
                                            this.carMaker= maker
                                            this.carColor= color
                                        }

                                        function myCar(maker, color) { // 변수 객체 myNewCar의 생성자함수
                                            Car.call(this, maker, color) // 이 this는 Car() 함수에 묶인다!
                                            this.age= 5 // 이 this는 myNewCar 인스턴스에 묶인다!
                                        }

                                        const myNewCar= new myCar('제네시스', '검정색') // new를 써서 myCar를 자신의 생성자함수로 삼는다
                                        console.log(myNewCar.carMaker + ": " + myNewCar.carColor + ", " + myNewCar.age) // 제네시스: 검정색, 5
                                    
                                

상속과 믹스인

자바스크립트에서는 객체의 인스턴스와 그 생성자간에 프로토타입 연결이 이루어지며, 이 연결을 통해 프로토타입 체인을 타고 올라가며 속성과 메서드를 탐색하게 된다 - 곧, 객체의 인스턴스로 상속되는 속성과 메서드들은 해당 객체가 아니라 그 객체 생성자의 prototype 속성에 정의되어 있는 것이다!

프로토타입 체이닝과 동적 디스패치
프로토타입 체이닝은 객체의 특정 프로퍼티를 읽으려고 할 때 발생한다 - 곧, (그 프로퍼티가 해당 객체에 없다면)체인을 거슬러 올라가면서 검색하는 것이다
                                    
                                        let objA= {
                                            name: 'Kim',
                                            sayHello: function() {
                                                console.log(this.name)
                                            }
                                        }, objB= { name: 'Lee' }

                                        objB.__proto__= objA // objB의 프로토타입 체인상에 objA를 연결한다

                                        let objC= {}
                                        objC.__proto__= objB
                                        objC.sayHello() // Lee ← 가까운 곳부터 참조한다!

                                        delete objB.name // objB의 name 속성 제거
                                        objC.sayHello() // Kim ← 가까운 곳에서 못 찾으면; 프로토타입 체인을 거슬러 위로 올라가며 찾는다!
                                    
                                
값을 쓰려고 할 때라면; 값을 읽을 때와는 달리, 동적으로 프로퍼티가 추가되고 값이 들어간다. 따라서 (상속받은 같은 이름의 프로퍼티가 있다면)체인의 윗 단계에 존재하는 같은 이름의 프로퍼티를 가리게된다('동적 디스패치!')
                                    
                                        let objA= {
                                            name: 'Kim',
                                            sayHello: function() {
                                                console.log(this.name)
                                            }
                                        }, objB= { name: 'Lee' }

                                        objB.__proto__= objA

                                        let objC= { name: 'none' }
                                        objC.__proto__= objB // 체인상에 있는 같은 이름의 프로퍼티를 가리게 된다!
                                        objC.sayHello() // none ← objB, objC의 name 프로퍼티는 가려졌다!
                                    
                                

* cf) A instanceof BA 의 프로토타입 체인상에 B 생성자함수 클래스가 있는지 여부를 확인한다

                                    
                                        function Car(marker, model) {
                                            this.make= marker
                                            this.model= model
                                        }

                                        let mycar= new Car("현대", "제네시스")
                                        console.log(mycar instanceof Car) // true
                                        console.log(mycar instanceof Object) // true
                                    
                                
프로토타입 기반 상속
Obj2= Object.create(Obj);는 명시적으로 Obj 의 프로토타입을 지정하여 객체 Obj2 를 생성하는데, 이를 활용하면 가장 간단하게 상속을 구현할 수 있다
                                    
                                        let per= {
                                            name: 'Kim',
                                            say: function() { console.log('Hi! ' + this.name) }
                                        }
                                        per.say() // Hi! Kim

                                        let per2= Object.create(per) // per2는 per을 상속받는다
                                        per2.name= 'Lee' // 상속받은 name 프로퍼티에 새 값을 할당한다 ← 상속받은 per.name 값을 '가린다!'
                                        per2.say() // Hi! Lee
                                    
                                

per 를 상속받은 per2__proto__ 링크를 통해 부모 객체인 per 의 프로퍼티에 접근할 수 있고, 자신만의 프로퍼티를 생성할 수도 있게 된다 이렇게 프로토타입의 특성을 활용하여 상속을 구현하는 것이 바로 프로토타입 기반 상속이다!

믹스인과 Object.assign()
1. 상속을 사용하지 않는 대신에 특정 객체의 프로퍼티를 동적으로 다른 객체에 뒤섞는 믹스인 방식도 사용할 수 있는데, 이를 위해서는 먼저 객체의 프로퍼티를 복사하는 믹스인 함수를 만들어야 한다
                                    
                                        function mixin(target, src) { // 참조를 통한 복사를 위한 믹스인 함수
                                            for (let p in src) { // src 객체의 키로 루프를 돈다
                                                if (src.hasOwnProperty(p)) // p가 src의 멤버라면;
                                                    target[p]= src[p] // 복사 ← 같은 키는 덮어쓴다!
                                            }

                                            return target;
                                        } // Object.assign(); 메서드와 같은데, 양자 모두 덮어씌워진다!

                                        let obj1= {a: 1, b: 2}
                                        let obj2= {b: 3, c: 4}
                                        let obj3= mixin(obj1, obj2) // mixin(); 함수를 통한 복사
                                        console.log(obj3) // {a: 1, b: 3, c: 4}
                                    
                                
2. 깊은 복사를 하는 Object.assign(); 메서드를 쓰면 좀 더 간단하다. Object.assign(대상객체, 소스객체[, 소스객체2, ..]); 메서드는 소스객체 의 열거 가능한(심볼도 포함하여) 자체 프로퍼티를 대상객체 로 (값으로)복사하는데, 소스객체 의 프로퍼티는 대상 객체 에 있는 같은 이름의 프로퍼티를 덮게 된다 대상객체 를 보존하면서 소스객체 에 넣어둔 기본값을 복사해서 쓰고자 한다면; 빈 객체를 만들어 사용하면 된다: Object.assign({}, 대상객체, 소스객체);
                                    
                                        let target= { x: 1, y: 3 }, src= { y: 2, z: 3 }

                                        let obj= {}
                                        Object.assign(obj, target, src) // target, src 순으로 obj로 복사되면서 들어간다!
                                        console.log(obj) // {x: 1, y: 2, z: 3}
                                        console.log(target) // { x: 1, y: 3 } ← 대상객체 target은 보존된다!

                                        Object.assign(target, src)
                                        console.log(target) // {x: 1, y: 2, z: 3} ← 대상객체 target 자체가 변경된다!
                                    
                                

클래스란?

는 해당 유형의 구체적인 객체를 만들기 위한 일종의 템플릿이며, 각각의 구체적인 객체들은 클래스의 인스턴스 가 된다. 이는 새로이 클래스라는 것을 만든다기보다는, 기본적으로 함수를 만드는 것 이며, 여전히 프로토타입 체인을 기반으로 동작한다는 점에서는 본질상 차이가 없다!

클래스 선언 및 정의
클래스class MyClass { .. }와 같이 정의하고, 인스턴스= new MyClass();로 클래스의 인스턴스를 생성하는데(또는, 인스턴스= class MyClass{ .. }와 같이 한번에 작성해줄 수도 있다), 인스턴스를 만들 때는 클래스의 constructor(); 생성자함수가 실행되며, 그 생성자는 각각의(인스턴스로부터 받은 인자값으로) 인스턴스를 초기화하게 된다
[ 클래스 사용법 기본 ]
                                        
                                            class Coupon { // 클래스 선언 ← 클래스는 일반 함수와 달리 호이스팅되지 않는다!
                                                // 클래스 생성자 constructor() 함수
                                                constructor(price, expire) { // 인수와 함께 클래스 멤버 정의 ← constructor() 생성자도 함수이므로 인수에 price= 5 식으로 초기값을 넣어줄 수도 있다!
                                                    this.price= price // 클래스 필드(= 클래스 변수 또는 멤버 변수)
                                                    this.expire= expire || '1주일' // 인수 expire가 있으면; 이것을 쓰고, 아니라면; '1주일'을 사용한다
                                                }

                                                // 클래스의 메서드
                                                getExpire() {
                                                    return `이 쿠폰은 ${this.expire} 후에 만료됩니다!`; // 클래스 내 this는 자신을 호출할 인스턴스를 가리킨다!
                                                }
                                            }

                                            const c= new Coupon(10) // 클래스의 인스턴스 객체 c 생성
                                            console.log(c.price + "%, " + c['expire'] + " 쿠폰: " + c.getExpire()) // 10%, 1주일 쿠폰: 이 쿠폰은 1주일 후에 만료됩니다!

                                            const d= new Coupon(30, '24시간') // 클래스의 인스턴스 객체 d 생성
                                            console.log(d.price + "%, " + d['expire'] + " 쿠폰: " + d.getExpire()) // 30%, 24시간 쿠폰: 이 쿠폰은 24시간 후에 만료됩니다!

                                            console.log(Coupon.toString()) // Coupon 클래스 코드 출력
                                        
                                    

클래스 선언표현식은 일반 함수 선언과는 달리 호이스팅되지 않으며, 클래스 내부 바디는 모두 암묵적으로 스트릭트 모드로 작동한다! 클래스의 멤버 변수constructor() 내부에서 this.변수명으로 사용하는데, 이 this 는 자신을 호출할 인스턴스를 가리킨다 따라서, 여기서 letconst 를 사용해서는 안된다!


* cf) Object 객체toString(); 메서드를 이용하면 쉽게 클래스 멤버들에 대한 정보를 받아 객체의 내용을 한 눈에 파악할 수 있다:

                                    
                                        class Car {
                                            // ..
                                        
                                            toString() {
                                                return `{this.price} ${this.expire}`;
                                            }
                                        }
                                    
                                
클래스의 상속 및 재정의, 캡슐화
extendssuper();를 사용하면; 다른 생성자의 프로토타입을 상속받고, 새로운 필드 및 메서드를 추가해서 확장하거나, 같은 이름의 메서드에 대해 서로 다르게 구현할 수도 있다
                                    
                                        class Coupon { // 수퍼 클래스
                                            constructor(price, expire) { // 클래스의 멤버 생성자 정의
                                                this.price= price
                                                this.expire= expire || '1주일'
                                            }

                                            getExpire() { // 클래스의 메서드 정의
                                                return `이 쿠폰은 ${this.expire} 후에 만료됩니다!`;
                                            }
                                        }

                                        class FlashCoupon extends Coupon { // FlashCoupon 클래스는 수퍼 클래스인 Coupon 클래스를 상속받는다
                                            constructor(price, expire) {
                                                super(price) // 수퍼 클래스의 멤버 price를 호출하여 사용한다
                                                this.expire= expire || '24시간'; // 수퍼 클래스의 멤버 expire를 수정한다
                                            }

                                            getExpire() { // 수퍼클래스의 메서드 재정의 ← 수퍼 클래스의 메서드 getExpire()의 메시지 내용을 덮는다(가린다)!
                                                return `이 ${this.price}% 쿠폰은 깜짝 쿠폰으로서.. ${this.expire} 후에 만료됩니다!`;
                                            }
                                        }

                                        const c= new Coupon(10)
                                        console.log(c.price + "% 쿠폰: " + c.getExpire()) // 10% 쿠폰: 이 쿠폰은 1주일 후에 만료됩니다!

                                        const flash= new FlashCoupon(30)
                                        console.log(flash.price + "% 쿠폰: " + flash.getExpire()) // 30% 쿠폰: 이 30% 쿠폰은 깜짝 쿠폰으로서.. 24시간 후에 만료됩니다!
                                    
                                

클래스의 상속 시 메서드가 호출될 때마다 스크립트는 먼저 현재 클래스에 있는지 확인하고, 없다면; 수퍼 클래스로 올라가서 확인하게 된다 - 곧, 자식 클래스에 같은 이름의 메서드를 새로 작성하면; 부모 클래스의 메서드는 가려진다! 객체가 클래스의 인스턴스인지를 확인할 때는 instanceof 연산자를 사용하면 된다: 변수객체 instanceof 클래스

                                    
                                        class Tours {
                                            constructor(city, days) {
                                                this.city= city
                                                this.days= days
                                            }

                                            notice() {
                                                console.log(`${this.city} 등반은 ${this.days}일 걸립니다`)
                                            }
                                        }

                                        class ToursEtc extends Tours {
                                            constructor(city, days, plus, user){
                                                super(city, days) // city, days 멤버는 수퍼클래스로부터 가져온다
                                                this.plus= plus // 새로운 멤버 생성
                                                this.user= user // 새로운 멤버 생성
                                            }

                                            noticeAdd() { // 수퍼클래스의 메서드와 결합한 새로운 메서드 생성
                                                super.notice() // 수퍼클래스의 notice() 메서드를 가져온다
                                                this.isFirst(this.user) // 내부 메서드 호출에도 this는 필요하다!
                                            }

                                            // 캡슐화
                                            isFirst(user) {
                                                if(user === 'leader') {
                                                    return console.log(`개인 소지품 외에.. 등반대 ${this.plus.join(", ")}도 잊지 마십시오!!!`)
                                                } else {
                                                    return console.log("개인 소지품 챙기는걸 잊지 마십시오!")
                                                }
                                            }
                                        }

                                        const trip1= new ToursEtc("남산", 3, ['깃발', '플랜카드'])
                                        trip1.noticeAdd() // 남산 등반은 3일 걸립니다. 개인 소지품 챙기는걸 잊지 마십시오!

                                        const trip2= new ToursEtc("남산", 3, ['깃발', '플랜카드'], 'leader')
                                        trip2.noticeAdd() // 남산 등반은 3일 걸립니다. 개인 소지품 외에.. 등반대 깃발, 플랜카드도 잊지 마십시오!!
                                    
                                
클래스의 정적 메서드
클래스의 정적 메서드는 클래스에 관련되지만, 인스턴스와는 관련이 없는 범용적인 작업에 사용된다. 클래스의 인스턴스화 없이 바로 호출하는 정적 메서드는 메서드 이름 앞에 static 키워드를 붙여서 정의하는데, 다양한 용도로 사용할 범용 메서드가 필요할 때 사용한다
                                    
                                        class Person {
                                            constructor(name, age) {
                                                this.name= name
                                                this.age= age
                                            }

                                            greet() { // 클래스의 메서드
                                                console.log(`My name is ${this.name}, ${this.age}살`)
                                            }

                                            static hellow() { // 클래스의 정적 메서드
                                                console.log("안녕?")
                                            }
                                        }

                                        const kjc= new Person("Kjc", 29)
                                        Person.hellow() // 안녕? ← 정적 메서드는 클래스의 인스턴스가 아니라 클래스 자체(클래스명)으로 접근해야 한다!
                                        kjc.greet() // My name is Kjc, 29살
                                    
                                

클래스의 정적 메서드는 클래스의 프로토타입 객체의 프로퍼티가 아니라 클래스 생성자의 프로퍼티이다. 따라서 this 는 자신을 호출한 인스턴스가 아니라 클래스 자체에 묶인다 - 정적 메서드는 인스턴스가 아니라 클래스 이름 을 사용하여 호출하며, 따라서 this 를 사용할 일은 없다!

➥ 프로토타입 기반 상속 대 클래스 기반 상속

클래스 기반 객체지향 프로그래밍 OOP 에서 클래스와 객체는 두 개의 별도 구성이며, 객체는 항상 클래스의 인스턴스로 생성된다. 또한 클래스를 정의하는 데 사용되는 기능(클래스 구문 자체)과 객체를 인스턴스화하는 데 사용되는 기능 사이에는 차이가 있다. JavaScript에서는 함수나 객체 리터럴을 사용하여 별도의 클래스 정의 없이 객체를 생성할 수 있고, 이를 통해 기존 OOP보다 객체 작업을 훨씬 더 가볍게 만들 수 있다

프로토타입 체인은 상속 계층구조처럼 보이고 어떤 면에서는 비슷하게 작동하지만, 다른 면에서는 다르다. 하위 클래스가 인스턴스화되면; 하위 클래스에 정의된 속성과 계층구조에서 추가로 정의된 속성을 결합하는 단일 객체가 생성된다. 프로토타입을 사용하면 계층구조의 각 수준이 별도의 객체로 나타나 __proto__ 체인을 통해 함께 연결되며, 이는 상속 보다는 위임 에 가깝다 - 여러 면에서 위임은 상속보다 객체를 결합하는 더 유연한 방법이다!


* cf) 즉, 생성자와 프로토타입을 사용하여 JavaScript에서 클래스 기반 OOP 패턴을 구현할 수 있다. 그러나 상속과 같은 기능을 구현하기 위해 직접 사용하는 것은 매우 까다롭기에, JavaScript는 클래스 기반 OOP의 개념에 보다 직접적으로 매핑되는 프로토타입 모델 위에 추가 기능을 제공하는 것이다!

게터와 세터

객체의 프로퍼티에는 데이터 프로퍼티(value: '값')와 접근자 프로퍼티가 있는데, 접근자 프로퍼티는 메서드와 비슷하되, 게터세터 두 가지 함수로 구성되어 동적으로 움직인다는 점에서 차이가 있다!

접근자 프로퍼티: 게터와 세터
접근자 프로퍼티getset 으로 객체가 가진 프로퍼티 값을 객체 바깥에서 읽거나 쓸 수 있도록 제공하는데, delete를 써서 삭제할 수도 있다: delete person.name
1. 게터세터는 보통 프로퍼티의 값을 갱신할 때 유효성을 검증하거나 조건에 따라 다른 값을 반환하고자 하는 경우에 쓰이는데, 프로토타입의 상속 또한 가능하다. 접근자 프로퍼티getset 은 같은 이름으로 연결되어 있는데, 프로퍼티에 값을 할당할 때는 자동으로 세터가 호출되어 할당하는 값이 첫번째 매개변수로 전달되며, 읽을 때는 자동으로 게터가 호출된다
[ 게터와 세터 ]
                                        
                                            let obj= {
                                                // 데이터 프로퍼티
                                                _data: "Kjc",
                                            
                                                // 접근자 프로퍼티: 접근자 프로퍼티 내부에서 쓰이는 this는 객체 자신을 가리킨다!
                                                get acc_p() { // 프로퍼티에 접근 시; 게터가 호출된다
                                                    return this._data;
                                                }, set acc_p(value) { // 프로퍼티 값을 설정하려 할 때; (인자와 함께)세터가 호출된다
                                                    this._data= value;
                                                }
                                            }
                                            
                                            obj.acc_p= "jc"; // 세터 호출 ← 값 수정
                                            console.log(obj.acc_p); // jc ← 값 읽어오기
                                        
                                    

객체의 프로퍼티에 게터와 세터 모두 있으면; 읽기와 쓰기가 가능한 프로퍼티이며, 게터만 있으면; 읽기 전용, 세터만 있으면; 쓰기 전용 프로퍼티이다. 게터나 세터 호출 시는, 괄호는 붙이지 않고 객체의 프로퍼티에 접근할 때와 마찬가지로 . 표기법으로 접근한다 접근자 프로퍼티 게터와 세터는 메서드를 객체의 프로퍼티인 것처럼 위장한다!

2. 클래스에서 게터와 세터는 하나로 묶여 있다 - 프로퍼티에 값을 할당할 때는 (할당한 값을 매개변수로 전달하면서)세터가 호출되고, 프로퍼티의 값을 읽어올 때는 게터가 호출된다
                                    
                                        class MyClass {
                                            constructor(value) { // 필드 정의
                                                this._customField= value ? value : "음~";
                                            } // value에 값이 들어오면; 세터가 호출된다!

                                            get customField() { // 전달되는 값 없이 호출된 경우; 현재 설정된 값을 리턴한다
                                                return this._customField;
                                            } set customField(value) { // 받은 값으로 해당 필드 값을 변경한다
                                                this._customField= value
                                            }
                                        }

                                        const me= new MyClass()
                                        me.customField // 게터 호출
                                        console.log(me.customField) // 음~

                                        me.customField= 5 // 세터 호출
                                        console.log(me.customField) // 5
                                    
                                
                                    
                                        class User {
                                            constructor(firstName, lastName) {
                                                this.fName= firstName
                                                this.lName= lastName

                                                this._nickName= "" // 관습적으로, 맨 앞에 _을 붙여 게터와 세터로 이어짐을 암시한다!
                                            }

                                            set nickNames(str) { // 클래스의 세터
                                                this._nickName= str
                                            } get nickNames() { // 클래스의 게터
                                                console.log(`${this.fName} ${this.lName}의 별명은 ${this._nickName}`)
                                            }
                                        }

                                        const kjc= new User('Kim', 'jc')
                                        kjc.nickNames= "만화광" // 세터 호출
                                        kjc.nickNames // Kim jc의 별명은 만화광 ← 게터 호출
                                    
                                
➥ 객체 보호: 객체의 확장 및 잠금

객체의 확장은 객체에 새로운 프로퍼티를 추가할 수 있는지를 말하는데, 사용자 정의 객체와 내장 객체는 기본적으로 true 값이 설정되어 있다: Object.prventExtensions(객체)는 객체의 프로퍼티 추가를 금지하며, Object.seal(객체)는 객체의 프로퍼티 추가 및 삭제를 금지한다(오직 값의 읽기 쓰기만 가능하다). Object.freeze(객체)는 객체의 프로퍼티 추가 및 삭제, 값 쓰기까지 금지한다

                                        
                                            const obj= { id: 10, name: '호랑이' }
                                            Object.freeze(obj)

                                            obj.id= 12 // Uncaught TypeError! Cannot add property 3, object is not extensible
                                            obj.address= '경주' // Uncaught TypeError!
                                        
                                    

단, 객체에 접근자 프로퍼티가 정의되어 있다면; 게터와 세터 모두 호출할 수 있다!

이터레이터

이터레이터는 반복 처리가 가능한 객체를 말하며, 이터레이션은 데이터 안의 요소를 연속적으로 꺼내는 반복 처리를 의미하는데, 이를 통해 반복 처리를 단계적으로 제어할 수 있게 된다!

이터러블 객체의 단계적 순회
이터러블 객체의 순회 시, 단계적으로 제어하고자 한다면; 먼저 이터레이터 메서드를 호출해 이터레이터 객체를 얻어야 한다: const it= book.values(); 이제, 이터레이터 객체의 next(); 메서드를 호출하여 값을 가져오는데, 반환값의 done 속성이 true 가 될 때까지 반복적으로 값을 가져올 수 있다
                                    
                                        const book=[
                                            "1. let 변수와 const 상수",
                                            "2. 식별자(변수나 상수, 함수의 이름)",
                                            "3. 리터럴(문자열, 숫자, 불린 등)",
                                            "4. 값 대 참조의 개념"
                                        ]

                                        const it= book.values() // 배열 book의 각 값들을 가져온다!
                                        it.next() // 리턴값: { value: "1. let 변수와 const 상수", done: false }
                                        it.next() // 리턴값: { value: "2. 식별자(변수나 상수, 함수의 이름)", done: false }
                                        // .. ← 중간에서 무언가의 작업을 수행할 수도 있다!
                                        it.next() // 리턴값: { value: "3. 리터럴(문자열, 숫자, 불린 등)", done: false }
                                        it.next() // 리턴값: { value: "4. 값 대 참조의 개념", done: false}
                                        it.next() // 리턴값: { value: undefined, done: true }
                                        it.next() // 다 돌았으므로 앞으로 돌아갈 수는 없다 ← 곧, it로는 더 이상 값을 가져올 수 없다!

                                        const it2= book.values() // 같은 배열 book의 각 값들을 다른 변수에 담아 가져올 수 있다!

                                        let j= it2.next()
                                        while(! j.done) { // while 문으로 구현해본 for .. of 루프
                                            console.log(j.value)
                                            j= it2.next()
                                        }
                                    
                                

이터레이터는 반복 가능한 모든 요소를 다 불러온 뒤에도 계속 진행할 수 있지만, 역행할 수는 없다. 그리고, 하나의 대상에 대해 다수의 이터레이터 변수를 만들어 각자 독립적으로 움직이면서 작업을 조합할 수도 있다!


* cf) 이터러블 객체는 이터레이터를 반환하는 Symbol.iterator를 내장하고 있는데, next(); 메서드가 호출할 때마다 그 결과를 담은 iterator result 객체 (value: 현재 꺼내온 값, done: 모든 열거가 끝났는지 여부)를 반환한다


스크립트에 내장된 이터러블 데이터 타입의 이터레이터 객체는 그 자체가 이터러블이다 - 곧, 자기 자신을 반환하는 Symbol.iterator(); 메서드를 갖는다. 배열에 흔히 사용되는 내장 함수와 생성자 상당수는 임의의 이터레이터를 받도록 작성되었는데, Set(); 생성자도 그렇다: new Set("abc"); 문자열은 이터러블이므로, 이는 new Set(["a", "b", "c"]);와 같다!

이터레이션 프로토콜
1. 이터레이션 프로토콜은 모든 객체를 이터러블 iterable 객체로 바꿀 수 있도록 한다. 예컨대, 클래스에 심볼 메서드 [Symbol.iterator]();가 있고, 이 메서드가 이터레이터처럼 동작하는 객체를 반환한다면; 그 클래스의 인스턴스는 이터러블 객체라는 것을 의미한다
                                    
                                        class Log {
                                            constructor() {
                                                this.msg= [] // 이터러블 배열 객체
                                            }

                                            add(msg) {
                                                this.msg.push({msg, para: "경주"})
                                            }

                                            // 이터레이션 프로토콜: 이제 이 클래스의 인스턴스는 이터러블 객체가 된다!
                                            [Symbol.iterator]() {
                                                return this.msg.values();
                                            }
                                        }

                                        const log= new Log()
                                        log.add("남산")
                                        log.add("토함산")
                                        // ..

                                        for(let e of log) { // 이제 log를 배열처럼 순회할 수 있다!
                                            console.log(`${e.msg}@${e.para} `) // 남산@경주 토함산@경주
                                        }
                                    
                                
2. 이터러블 객체와 이터레이터의 핵심 특징 중 하나는, 이들이 본질적으로 느슨하다는 것이다 - 가령, 다음 값을 얻기 위해 계산이 필요할 시; 그 값이 실제로 필요해질 때까지 계산을 늦출 수 있다: 예컨대, 아주 긴 문자열이 있고, 이 문자열을 공백으로 구분된 단어로 토큰화한다고 할 때; 단순히 split(); 메서드를 쓸 경우 첫 단어 하나만 사용하면 되는 경우에도 문자열 전체를 처리할 때까지 기다리게 되며, 나아가 반환된 배열과 배열 내 문자열에 많은 메모리를 할당해야 한다!
                                    
                                        function words(s) {
                                            let r= /\s+/g // 하나 이상의 공백
                                            r.lastIndex= s.match(/[^ ]/).index // 공백이 아닌 첫번째 위치에서 검색을 시작한다

                                            return { // 이터러블인 이터레이터 객체를 반환한다
                                                [Symbol.iterator]() { // 이터러블이 된다
                                                    return this
                                                }, next() { // 이터레이터가 된다
                                                    let start= r.lastIndex; // 마지막으로 일치한 지점에서 재개한다

                                                    if(start < s.length) { // 아직 끝나지 않았다면;
                                                        let match= r.exec(s) // 다음 경계 위치를 찾아서,

                                                        if(match) { // 단어 경계를 찾으면; 그 단어를 반환한다
                                                            return { value: s.substring(start, match.index) }
                                                        }
                                                    }

                                                    return { done: true } // 그렇지 않다면; 끝낸다
                                                }
                                            }
                                        }

                                        console.log([...words(" abc def  ghi! ")]) // ['abc', 'def', 'ghi!']
                                    
                                
3. 이터레이터가 항상 끝까지 실행되지는 않는다. 이터레이터를 해체 할당과 함께 사용할 때 next(); 메서드는 지정된 변수 각각의 값을 얻을 수 있을 만큼만 호출된다. 그럼에도 필요 시 이터레이터의 순회를 멈출 수 있는 방법이 있어야 하며, 이를 위해 next();와 함께 return(); 메서드를 사용할 수 있는데, 이 메서드는 반드시 순회 결과 객체를 반환하여야 한다!

빨주노초파남보

                                    
                                        
                                    
                                
                                    
                                        function getNextColorIter() {
                                            const colors= [ 'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet' ]

                                            let color_idx= -1
                                            return {
                                                next() {
                                                    if(++color_idx >= colors.length) color_idx= 0;

                                                    return {
                                                        value: colors[color_idx], done: false
                                                    }
                                                }
                                            }
                                        }

                                        const color_it= getNextColorIter()
                                        setInterval(function() {
                                            document.querySelector('.rainbows').style['color']= color_it.next().value
                                        }, 3000);
                                    
                                

이 예제는 이터레이터를 이용하여 재작성해본 순수 함수인데, setInterval();로 호출 시에도 매번 next(); 값이 달라지지만; 함수의 메서드는 자신이 속한 객체의 컨텍스트 안에서 동작하므로 프로그램의 다른 부분에서 getNextColorIter(); 함수를 호출하더라도 각기 다른 이터레이터가 생성되어 서로간에 간섭은 생기지 않는다!

제너레이터

제너레이터이터레이터를 사용해 자신의 실행을 제어하는 함수로서, 그 실행을 개별적 단계로 나누어 제어하면서 언제든 호출자에게 제어권을 넘길 수 있고, 따라서 실행 중인 함수와 통신하는 것이 가능해진다!

제너레이터 함수
제너레이터 함수는 호출한 즉시 실행되지는 않고, 대신 이터레이터를 반환한다. 그리고 yield 표현식은 이터레이터로부터 next();로 호출되면; 먼저 제어권을 다시 이터레이터에게 돌려주고, 다음번 next(); 호출이 있어야 코드를 실행하여 값을 전달하게 된다
                                    
                                        function* rainbow() { // 제너레이터 함수 rainbow 생성
                                            yield '빨강' // next()로 호출되면 넘겨줄 값들..
                                            yield '주황'
                                            yield '노랑'
                                            yield '초록'
                                            yield '파랑'
                                            yield '남색'
                                            yield '보라'
                                        }

                                        for(let color of rainbow()) { // for .. of 문 ← 제너레이터 함수를 호출하면; 이터레이터를 얻는다!
                                            console.log(color) // 빨강 주황 노랑 초록 파랑 남색 보라
                                        }

                                        const it= rainbow() // 이터레이터 인스턴스
                                        it.next() // { value: "빨강", done: false } ← 제너레이터 함수 호출
                                        it.next()
                                        it.next()
                                        it.next()
                                        it.next()
                                        it.next()
                                        it.next() // { value: "보라", done: false }
                                        it.next() // { value: undefined, done: true }

                                        const it2= rainbow() // 또 다른 이터레이터 인스턴스
                                        console.log([... it2]) // [빨강 주황 노랑 초록 파랑 남색 보라]
                                    
                                

yield 에 지정한 값은 next(); 메서드의 리턴값이 되어 바깥으로 산출되며, 이 자체를 변수에 대입할 수도 있다: let a= yield 2 // a 값은 2가 됨

제너레이터로 생성한 이터레이터의 next(); 메서드에 값을 대입하면 제너레이터에 값을 넘길 수 있는데, 이 값은 제너레이터가 일시적으로 정지하기 직전의 yield 표현식의 값으로 사용된다 - 이를 활용하면 제너레이터의 내부 상태를 외부에서 변경할 수 있게 된다
                                    
                                        // 이터레이터가 만들어지면; 제너레이터는 이터레이터를 반환하고 일시 정지한 상태로 시작한다:
                                        function* genFnc() {
                                            const name= yield "이름은?" // it 변수로부터 next()가 호출되면; yield 표현식의 값("이름은?")을 매개변수로 하여 제어권을 호출한 이터레이터로 넘기고 일시 정지한다!
                                            const color= yield "좋아하는 색은?" // it 변수로부터 전달받은 매개변수 값('Kjc')를 name에 넣고, 두번째 yield 표현식의 값("좋아하는 색은?")을 제너레이터로 넘기고 일시 정지한다!

                                            return `${name} : ${color}` // it 변수로부터 전달받은 값을 적용하여 돌려준다
                                        }

                                        const it= genFnc() // 제너레이터 함수를 호출하면; 이터레이터를 얻는다!

                                        // 아직은 제너레이터의 어떤 부분도 실행되지 않는다 ← next()로 호출되어야 첫번째 yield 표현식이 실행된다!
                                        console.log(it.next()) // {value: '이름은?', done: false} ← 첫 next() 호출로 첫번째 yield를 전달받고("이름은?"), 이제 제어권은 호출자에게 넘어가고, 이 라인은 다음 호출이 있어야 완료된다!
                                        console.log(it.next('Kjc')) // {value: '좋아하는 색은?', done: false} ← 제너레이터로 'Kjc'를 넘기고, 다음 yield 표현식 값("좋아하는 색은?")을 전달받는다!
                                        console.log(it.next('Orange')) // {value: 'Kjc : Orange임', done: true} ← 제너레이터로 'Orange'를 넘기고, 제너레이터로부터 return문을 전달받는다
                                    
                                

* cf) 제너레이터 안에서 return 문을 사용하면; 그 위치와 상관없이 donetrue 가 되고, valuereturn 문이 반환하는 값이 된다!

                                    
                                        function* abc() {
                                            yield 'a'
                                            yield 'b'

                                            return 'c'; // done: true
                                        }

                                        for(let i of abc()) {
                                            console.log(i) // a b ← 여기서 제너레이터의 리턴값 c는 관심 밖이 된다!
                                        }
                                    
                                

제너레이터에서 중요한 값을 return 문으로 반환하도록 해서는 안된다. 반환되는 값을 받아 쓰려는 경우는 yield를 사용하고 return 문은 제너레이터를 중간에 종료하는 목적으로만 사용해야 한다!

프락시

Proxy는 객체에 대한 작업을 가로채고, 필요하다면 작업 자체를 수정할 수 있도록 한다 - 곧, 객체의 기본 작업(속성 조회, 할당, 열거, 함수 호출 등)에 대해 사용자 지정 동작을 추가하는 것이다!

프락시 생성자와 get, set 핸들러
1. 프락시 생성자에 넘기는 첫번째 매개변수는 타겟 즉 프락시할 대상이 되는 객체이고(여기에는 객체, 함수, 다른 프락시 등 무엇이든 올 수 있다), 두번째 매개변수는 가로챌 동작을 가리키는 핸들러 함수다: const x= new Proxy(타겟, 핸들러함수);
                                    
                                        const cf= { a: 1, ac: 3, Any: 5 }

                                        const cf2= new Proxy(cf, {
                                            get(target, key) { // 핸들러 함수 get(타켓, 키)으로 타겟 cf의 값 체크
                                                return target[key] || 0; // 정의되지 않은 프로퍼티에는 항상 0을 전달한다!
                                            }
                                        })

                                        console.log(cf2.a, cf2.b) // 1 0
                                    
                                

여기서의 get(타겟, 키[, 수신자]); 핸들러 함수는 프로퍼티 접근자인 get과는 다르다 - 이 핸들러는 일반적인 프로퍼티나 접근자 프로퍼티 모두에서 작동된다!

2. 프로퍼티에 값을 할당하려할 때는 set(타겟, 키, 값); 핸들러 함수로 가로챌 수도 있다
                                    
                                        const road= { once: 'Wait', red: 2, blue: 5 }

                                        const preview= new Proxy(road, {
                                            set(target, key, value){
                                                if(key === 'red') { // 키가 red일 때는 미리 체크한다!
                                                    if(target.safe) return target.red= value;
                                                    else return alert("Too dangerous!");
                                                }

                                                target[key]= value
                                            }
                                        })

                                        console.log(preview.blue= 6) // 6
                                        console.log(preview.red= 1) // 1 "Too dangerous!"

                                        preview.safe= true // safe 값에 true를 주어 안전을 보장한다!
                                        console.log(preview.red= 3) // 3
                                    
                                
                                    
                                        const cities= { city: "경주", mountain: "남산" }

                                        const citiesProxy= new Proxy(cities, {
                                            get(target, city) {
                                                return target[city];
                                            }, set(target, city, value) {
                                                console.log("Changing city to.. ");
                                                target[city]= value;
                                            }
                                        });

                                        console.log(citiesProxy.city, citiesProxy.mountain) // 경주 남산
                                        console.log(citiesProxy.city= '서울', citiesProxy.mountain) // Changing city to.. 서울 남산
                                    
                                

Reflect API

Reflect API는 중간에서 가로챌 수 있는 JavaScript 작업에 대한 메서드를 제공하는 스크립트 내장객체로서, 메서드의 종류는 프록시 처리기와 동일하다

리플렉트 API
다른 대부분의 전역객체들과는 달리, Reflect는 생성자가 아니며, 따라서 함수처럼 호출하거나 new 연산자로 인스턴스를 만들 수 없다. Math 객체와 마찬가지로, Reflect의 모든 속성과 메서드는 정적이다
  • Reflect.apply(fnc, obj, args)는 함수 fncobj 의 메서드로 호출하면서 args 배열을 인자로 전달하는데, 이는 fnc.apply(obj, args)와 같다
  • Reflect.construct(c, args[, newTarget]) 함수로 사용하는 new 생성자로서, 생성자 cnew 키워드와 함께 args 배열을 인자로 전달한 것처럼 호출하는데, 이는 new target(...args)와 같다
  • Reflect.defineProperty(obj, name, descriptor) name (문자열이나 심볼)을 프로퍼티 이름으로 써서 obj 객체의 프로퍼티를 정의한다 Object.defineProperty()(리턴값: obj / TypeError)와는 달리 반환값이 true / false 라는 점만 다르다!
  • Reflect.deleteProperty(obj, name) 함수로 사용하는 delete 연산자로서 delete obj.name과 같다
  • Reflect.get(obj, name[, receiver]) 대상 속성의 값을 반환하는데, obj.name과 같다
  • Reflect.getOwnPropertyDescriptor(obj, name) 주어진 name 속성이 obj 객체에 존재하면, 그 속성의 서술자를 반환한다(아니라면; undefined) Object.getOwnPropertyDescriptor()와 비슷하다!
  • Reflect.getPrototypeOf(obj) obj 객체의 프로토타입을 반환한다 Object.getPrototypeOf()와 비슷하다!
  • Reflect.has(obj, name) 함수로 사용하는 in 연산자로서, name in obj와 비슷하다
  • Reflect.isExtensible(objA) obj 객체의 확장가능 여부를 반환하는데, Object.isExtensible()와 비슷하다
  • Reflect.ownKeys(obj) obj 객체의 자체 키(상속하지 않은 키) 목록을 배열로 반환하는데, Object.getOwnPropertyNames()와 비슷하다
  • Reflect.preventExtensions(obj) obj 의 확장불가 여부를 반환하는데, Object.preventExtensions()와 비슷하다
  • Reflect.set(obj, name, value[, receiver]) obj 객체의 name 속성에 value 값을 설정하는데, obj.name= value와 비슷하다
  • Reflect.setPrototypeOf(obj, p) obj 객체의 프로토타입을 p 로 설정하는 함수로서, Object.setPrototypeOf와 비슷하다
                                    
                                        const person= {
                                            name: "Kim",
                                            color: "white",
                                            greeting: function () {
                                                console.log(`My name is ${this.name}`)
                                            }
                                        };

                                        console.log(Reflect.has(person, "color")) // true
                                        console.log(Reflect.ownKeys(person)) // ['name', 'color', 'greeting']

                                        Reflect.set(person, "eyes", "black") // 키와 값 설정
                                        console.log(Reflect.has(person, "eyes")) // true
                                    
                                

프라미스란?

비동기 프로그래밍 방식에는 콜백, 제너레이터, 가 있는데.. 비동기적 실행 의 요점은; 코드 진행과정에서 어떤 것도 차단하지 않는다는 것이다!

비동기적 실행과 콜백, 프라미스
스크립트에서 비동기적 실행 이란 작업을 이어갈 어떤 값이 들어오거나(= 콜백) 사용자의 반응이 있을 때까지(= 이벤트) 기다려야 한다는 것이다. 예컨대, 일정 시간이 지나면 코드를 실행하는 타이머 setTimeout(콜백, 대기시간);, 사용자의 반응에 따라 실행되는 이벤트 리스너 addEventListener('이벤트명', 콜백);, 그리고 서버의 응답을 받을 때까지 대기해야 하는 네트워크 요청 및 파일시스템 작업 등..

* cf) 스크립트 코드는 각 코드 블록이 순차적으로 실행되면서 다음 단계로 넘어간다. 그런데, 메서드 체인으로 연결되는 콜백으로 비동기 코드를 작성한다면; 콜백 안에 또 다른 콜백들이 중첩되는 경우 코드를 읽기도 어렵고 에러 처리도 어렵게 된다. 이런 문제를 해결하기 위해 만들어진 프라미스는 비동기 작업의 (나중에 드러날)결과를 '약속'하는 객체로서, 콜백을 사용하는 새로운 방법이라고 할 수 있다!

1. 끊임없이 이어지는 '콜백 지옥'을 벗어나고자 만들어진 것이 바로 Promise인데, 프라미스는 함수에 메서드 체인으로 연결되는 콜백들을 전달하는 대신에, 밑으로 콜백을 첨부한다. 프라미스 기반 비동기 함수를 호출하면; 그 함수는 (나중에 드러날)성공 또는 실패를 나타내는 Promise 객체를 리턴하며, 이것은 객체이므로 콜백과는 달리 어느 곳으로든 전달하여 이전 작업이 완료될 때까지 다음 작업을 연기시키거나, 작업 실패에 대응하는 처리를 하도록 할 수 있다 - 예컨대, 여러 개의 중첩된 콜백함수에 데이터를 전달하는 대신에 여러 개의 then(); 메서드를 통해 데이터를 아래로 내려주는 방식(= 프라미스 체인)이다
[ 프라미스 체인의 기본 ]
                                        
                                            // 화살표 함수로 작성한 프라미스 체인
                                            doSomething()
                                            .then(result => doSomething2(result))
                                            .then(newResult => doSomething3(newResult))
                                            .catch(failureCallback);
                                        
                                    

각각의 프라미스는 기본적으로, 프라미스 체인 안에서 서로 다른 비동기 단계의 완료를 나타내는데, 각각의 프라미스 단계 사이에는 반드시 반환값이 있어야 한다 참고로, 화살표 함수에서 () => x() => { return x; }와 같다!

2. 프라미스는 단순히 어떤 작업이 끝났을 때 그 값을 받아 이어지는 작업을 수행할 콜백함수를 등록한다는 개념이 아니라, 프라미스 객체가 생성된 이후에 '일어날' 어떤 비동기 작업의 결과를 반환할 것임을 약속 보증 하는 것이다 - 곧, 프라미스가 반환된 다음에 작업이 이루어지므로 이 작업이 성공하여 을 반환하면서 이행 fulfill 될지, 아니면; 캐치할 수 있는 에러 및 예외가 발생하여 거부 reject 될지 여부는 아직은 미지수이다 promise.then(callback, reject);callback 을 가능한 한 빨리 호출하면; 그 프라미스는 '이행된'(fulfilled) 것이며, reject 를 가능한 한 빨리 호출하면; 그 프라미스는 '거부된'(rejected) 것이다!
[ 프라미스의 진행과정 ]
                                        
                                            /* [작업 1 시작] URL을 넘기며 fetch()를 호출한다 */
                                            fetch(URL) // fetch() 메서드는 URL을 받아 웹서버에 HTTP GET 요청을 보내고, 동시에 [프라미스1]을 반환한다: let 프라미스1= fetch(URL)

                                            /* ---
                                                [작업 2 시작] HTTP 요청 작업이 성공적으로 완료되어 응답을 받아올 때에 대비하여,
                                                [프라미스1]의 .then() 메서드에 [콜백1]을 '등록'하고, [프라미스2]를 반환한다
                                            */
                                            .then(response => response.json()) // let 프라미스2= 프라미스1.then(콜백1) ← '결정(해결)된'(Resolved) 상태(곧, [프라미스1]과 [프라미스2]가 '연결된' 상태)
                                            // 나중에 [작업 2]를 성공적으로 마치면; [콜백1]은 [값]이 되고, 그 [프라미스1]는 자동적으로 '이행'된다(Fulfilled)
                                            // 만약 이 [값] 또한 프라미스라면; [프라미스1]은 '결정'되긴 했지만, '이행'되지는 않은 상태이다
                                            // ← 프라미스가 '결정'되었다는 것은 단지 (그 값이 '이행'될지, '거부'될지와 무관하게)프라미스가 다른 프라미스와 연결되었다는 의미이다!

                                            /* ---
                                                [작업 3 시작] 위에서 프라미스가 '이행'될 때를 대비하여,
                                                이행시 반환되는 json 객체를 인자로 하여 처리하도록 [프라미스2]에 [콜백2]를 '등록'하고 [프라미스3]를 반환한다
                                            */
                                            .then(myJson => { // let 프라미스3= 프라미스2.then(콜백2)
                                                // .. 필요한 후속 작업들
                                            }) // 나중에 [작업 3]를 성공적으로 마치면; [프라미스3]과 함께 [프라미스2]도 동시에 '이행'된다!

                                            /* ---
                                                [에러 및 예외 처리] 프라미스가 거부(reject)된 경우
                                            */
                                            .catch(e => {
                                                console.log("에러 발생: " + e.message) // 에러 내용은 e.message에 들어있다
                                            });
                                        
                                    

fetch();에 의해 서버로 HTTP GET 요청이 들어가고.. 위 작업1, 2, 3은 모두 동기적으로 수행된다!

3. 일단 프라미스가 생성되면; 아직은 성공실패 도 아닌 대기 pending 상태인데.. 이제 프라미스가 수행되어 정상적으로 그 결과를 으로 반환한다면; 그 프라미스는 즉시 이행 fulfill 되고, 이 값은 다음 .then(); 블록이 존재한다면; 그 콜백함수에 인자로 전달된다. 한편, 그 결과값 또한 프라미스라면; 이는 결정된 resolved 상태이지만 이행된 fulfilled 상태는 아니다(연결된 프라미스에 의해 그 운명이 결정지어진다!)


'이행'되거나 '거부된' 프라미스는 완료된 것이며(이 값은 결코 바뀌지 않는다!), 이행되지도 거부되지도 않았다면; 대기 (보류) 상태이다. 한편, 프라미스에서 결정된 해결된 상태는 '완료된' 상태를 포함하지만, 단지 그것만은 아니다. 곧, 프라미스가 (대기 중인)다른 프라미스의 결과에 따라 결정지어질 운명에 의해 '잠긴' 경우(이는 나중에 다른 프라미스의 결과에 따라 자신의 운명이 '결정되도록' 연결되어 있지만, 아직 완료된 것은 아니다!), 그 프라미스는 결정된 상태인데, 이러한 관계는 재귀적이다 - 예컨대, 거부된 프라미스에 자신의 운명이 묶여서 이행 핸들러를 호출하는 thenable 로 '결정된' 프라미스는 함께 거부된다!

프라미스 체인

프라미스 체인과 에러 처리
프라미스 체인을 사용하면 모든 단계에서 에러를 캐치할 필요는 없다. 기본적으로 프라미스 체인은 에러나 예외가 발생하면 체인의 아래에서 .catch(); 블록을 찾는다 - 곧, 체인 어디서든 에러가 발생하면 체인 전체가 멈추고 catch 핸들러가 작동하게 된다
[ 프라미스 에러 처리 ]
                                        
                                            fetch(URL)
                                            .then(callback)
                                            .catch(reject) // .then(callback, reject)와 같다!
                                            .finally()
                                        
                                    

마지막의 .finally();는 인자는 받지 않으며, (프라미스의 이행 여부와 무관하게)파일을 닫거나 네트워크 연결을 끊는 등의 마지막 정리 작업을 수행하고자 할 때 유용하다!

                                    
                                        fetch("user/profile").then(response => { // 서버로부터 상태와 헤더를 받아서..
                                            if (! response.ok) return null; // 404 등의 예상되는 에러 처리 ← null은 유효한 '값'이므로 반환하는 프라미스는 즉시 '이행'된다!

                                            let type= response.headers.get("content-type")
                                            if (type !== "application/json") { // 서버측의 심각한 에러 ← 반환하는 프라미스는 '거부'되어 .catch()로 내려간다!
                                                throw new TypeError(`Not JSON: ${type}`); // 이 throw 문도 여기서 바로 끝나지는 않고,, 비동기적으로 처리된다!
                                            }

                                            /* ---
                                                정상 처리: 프라미스 반환 ← 반환하는 프라미스는 '결정(해결)'되었지만, 그 프라미스가 '이행'될지 '거부'될지 여부는 아직 미지수이다
                                                - 다음 단계의 결과에 달려 있다!
                                            */
                                            return response.json(); // 응답 바디를 프라미스로 반환한다
                                        }) .then(profile => { // 분석된 응답 바디 또는 null을 인자로 받는다
                                            if (profile) 유저프로필표시(profile) // 정상적으로 유저의 프로필을 표시한다
                                            else 로그아웃페이지() // null을 받은 경우는 여기서 처리하고 끝낸다!
                                        }) .catch(e => { // 모든 에러는 여기서 한번에 처리한다
                                            if (e instanceof NetworkError) console.log("인터넷 연결을 확인하십시오!") // 네트워크 에러
                                            else if (e instanceof TypeError) console.log("타입 에러!") // 타입 에러
                                            else console.error(e) // 예상못한 에러 발생 시의 처리
                                        });
                                    
                                

위에서 동기적 코드인 throw 문으로 만들어진 Error 객체도 프라미스 체인의 .catch();에서 비동기적으로 처리된다!


* cf) .catch();는 프라미스 체인의 어느 곳에서든, 중복하여 사용할 수 있다. .catch();에 전달하는 콜백은 이전 단계에서 에러가 났을 때만 호출된다 - 곧, 일단 .catch();에 전달된 에러는 프라미스 체인을 타고 내려가지 않고 거기서 멈추며, (처리가 가능하다면;)에러를 처리한 뒤, 정상적으로 프라미스를 반환하고 비동기 작업은 계속 진행되는 것이다!

                                    
                                        fetch(URL).catch(e => wait(1000).then(callback)) // 서버측 응답을 받는데 실패하면; 1초간 기다렸다 재차 시도한다!
                                        .then(callback)
                                        .catch(reject)
                                    
                                
프라미스 병렬 처리
아래 정적 메서드들은 프라미스 배열 을 매개변수로 받아 다수의 비동기 작업을 동시에 처리한 뒤, 각자의 방식으로 그 결과값을 반환한다
  • Promise.all(프라미스 배열); 다수의 프라미스를 동시에 처리하여(순서는 보장되지 않는다) 모든 프로미스가 '이행'될 때만 '이행'된다 - 곧, 어느 하나라도 '거부'되면; 더 이상 진행되지 않고 그 즉시 '거부'된다 참고로, 이터러블 배열 에 프라미스가 아닌 요소가 들어있어도 그것은 '이행'된 것으로 간주하고 그대로 으로 반환한다!
  • Promise.allSettled(프라미스 배열); 모든 프로미스가 다 '완료'되면 한번에 '이행'되는데, 각각의 반환된 객체에는 status(fulfilledrejected), value(fulfilled 의 반환값), reason(rejected 의 거부 사유) 프로퍼티가 있다
                                    
                                        const promise1= Promise.resolve(3)
                                        const promise2= "값"
                                        const promise3= new Promise((resolve, reject) => {
                                            setTimeout(() => {
                                                resolve("성공")
                                            }, 100);
                                        });

                                        Promise.all([promise1, promise2, promise3]).then((values) => {
                                            console.log(values) // [3, '값', '성공']
                                        }).catch(e => console.error(e))
                                    
                                

* cf) Promise.all();은 주로 서로 연관된 작업을 수행하거나, 하나라도 거부되면 즉시 거부하려는 때에 사용하며, Promise.allSettled();는 서로의 성공 여부와 무관하게 여러 비동기 작업을 수행할 때 사용한다. 한편, Promise.race();는 다수의 프라미스를 동시에 처리하되, '이행'이든 '거부'든 간에 가장 먼저 '완료된'(settled) 프라미스를 결과값으로 반환받고자 할 때 사용된다

프라미스 만들기

는 주로 프로미스를 지원하지 않는 함수를 감쌀 때 사용된다

프라미스 만들기
먼저 Promise 객체를 생성하고(const p= new Promise((resolve, reject) => { .. });), 이어서 pro.then(() => { .. });(및 pro.catch(() => { .. });)으로 실행 후의 처리 작업을 수행한다 프라미스 생성자resolve reject 를 인수로 받아서 각각 then();catch();로 전달하는데, 그 자체 객체이므로 어디로든 넘길 수 있다!
[ 프라미스 만들기 ]
                                        
                                            const pro= new Promise((resolve, reject) => { // 비동기 작업이 성공한 경우 resolve()를 호출하고, 실패한 경우 reject()를 호출한다
                                                setTimeout(() => {
                                                    resolve("성공!")
                                                }, 1000);
                                            }); // 여기서는 setTimeout()을 사용해 비동기 코드를 흉내내지만, 실제로는 여기서 XHR이나 HTML5 API를 사용할 것이다!

                                            pro.then((value) => { // value는 위에서 resolve() 호출에 제공한 값이다
                                                console.log(value) // "성공!"
                                            });

                                            console.log(pro) // Promise {} ← 이곳이 먼저 출력된다!
                                        
                                    

실행 함수는 비동기 작업을 시작한 후 모든 작업을 끝내면 resolve();를 호출해 프로미스를 이행하고, 오류가 발생한 경우 reject();를 호출해 거부한다 실행 함수에서 오류를 던지면; 프로미스는 거부되며, 실행 함수의 반환값은 무시된다!


* cf) resolve();reject();는 각각 resolve 되거나 reject 될 운명인 프라미스를 직접 생성하기 위한 바로 가기이다. resolve(값);은 주어진 으로 '결정'될 프라미스를 만들어 반환한다 - 그 값이 프라미스라면; 해당 프라미스가 으로 반환되어 '이행'되고, 그 값이 thenable이라면; 반환된 프로미스는 그 thenable의 최종 상태에 따르게 된다. 반대로, reject(이유);은 주어진 이유 로 '거부'될 프라미스를 만들어 반환한다

                                    
                                        function wait(delay) { // 프라미스를 만들어 반환하는 함수
                                            return new Promise((resolve, reject) => {
                                                if (delay < 0) reject(new Error("음수는 안됨!")) // 예상할 수 있는 에러 처리

                                                setTimeout(resolve, delay)
                                            })
                                        }
                                    
                                

디버깅 목적 및 까다로운 오류를 잡기 위해, reject(이유);이유 를 Error 생성자의 인스턴스로 만들면 유용하다!

                                                    
                                                        
                                                    
                                                
                                                    
                                                        
                                                    
                                                

alarm(); 함수는 프라미스를 반환하므로, promise.all();async/await 등 프로미스로 할 수 있는 모든 것을 할 수 있다!

                                                    
                                                        button.addEventListener("click", async () => {
                                                            try {
                                                                const message= await alarm(name.value, delay.value)
                                                                output.textContent= message
                                                            } catch(error) {
                                                                output.textContent= `알람 설정 불가, ${error}`
                                                            }
                                                        });
                                                    
                                                

                                                    
                                                        
                                                    
                                                
                                                    
                                                        
                                                    
                                                

Async와 Await

함수는 프라미스 기반 비동기 코드를 읽기 쉽고 이해하기 쉬운 동기적 코드처럼 작성할 수 있도록 단순화한다. async 선언은 프라미스를 반환하는 비동기 함수를 정의하며, 이 함수 내부에서는 으로 프라미스 값이 동기적으로 계산된 것처럼 프라미스를 기다릴 수 있다!

Await 문
Await 문Async 함수 안에서만 사용할 수 있으며, 프라미스를 기다리기 위해 사용된다. await 문은 프라미스가 '이행'되거나 '거부'될 때까지 async 함수의 실행을 일시 정지하고, 프라미스가 '이행'되면; async 함수를 일시 정지한 부분부터 다시 수행해나간다
[ await 사용하기 ]
                                        
                                            function resolveAfter(x) {
                                                return new Promise((resolve) => {
                                                    setTimeout(() => { resolve(x) }, 3000);
                                                });
                                            }

                                            async function asyncFnc() {
                                                console.log('calling: ')

                                                // await 표현식으로 프라미스 함수를 호출하고, 그 프라미스가 '이행'될 때까지 기다린 뒤.. 그 값을 받으면 작업을 수행하여 완료한다!
                                                const a= await resolveAfter("(3초가 지나서)성공적으로 이행됨!")
                                                console.log(a) // calling: (3초가 지나서)성공적으로 이행됨!
                                            }
                                            asyncFnc()
                                        
                                    

await 문의 반환값은 프라미스에서 '이행된' 값인데, 만약 프라미스가 '거부'되면; await 문throw 를 던지게 된다 예컨대, await p 표현식은 p 를 받아 그 값으로 '이행'하거나 '거부'하여 반환하는데, 여기서 요점은 await는 프라미스가 '완료'(곧, '이행'이나 '거부')되기 전에는 아무 것도 하지 않는다는 것이다!

Async 함수
async 함수는 항상 프라미스를 반환하는데, asyncawait를 사용할 때는 프라미스 호출 주변을 try .. catch 블록으로 둘러싸서 정상적으로 완료되지 않은 프라미스에서 발생한 오류를 처리할 필요가 있다
[ async 함수에서의 try .. catch 문 ]
                                        
                                            async function getProcessedData(url) {
                                                let v

                                                try {
                                                    v= await downloadData(url)
                                                } catch(e) {
                                                    v= await downloadFail(url)
                                                }

                                                return processDataInWorker(v); // async 함수의 반환값은 암묵적으로 Promise.resolve로 감싸진다!
                                            }
                                        
                                    

만약 async 함수의 반환값이 명시적으로 프라미스가 아니라면; 암묵적으로 '결정된'('resolved') 프라미스로 감싸진다!

                                    
                                        function resolveAfter(x) {
                                            return new Promise((resolve) => {
                                                setTimeout(() => {
                                                    resolve(x)
                                                }, 3000);
                                            });
                                        }

                                        // 변수에 할당한 async 함수표현식
                                        const add= async function(x) {
                                            const s= await resolveAfter(" 출력")

                                            return x + s;
                                        };

                                        add("3초 후").then(v => console.log(v)); // "3초 후 출력"

                                        // async 즉시실행 함수표현식
                                        (async function(x) {
                                            const s= await resolveAfter(" 출력")

                                            return x + s;
                                        }) ("3초 후").then(v => console.log(v)); // "3초 후 출력"
                                    
                                
for await .. of 문
은 동기식 이터러블만 아니라 비동기 이터러블 객체까지 순환하는 루프를 생성한다: for await (p of iter) { .. }
[ for await .. of 문 사용하기 ]
                                        
                                            for (const p of promises) { // for .. of 문으로 프라미스 배열을 순회한다
                                                res= await promises
                                                handle(res)
                                            }

                                            for await(const res of promises) { // for await .. of 문으로 프라미스 배열을 순회한다
                                                handle(res)
                                            }
                                        
                                    
                                        
                                            async function* foo() {
                                                yield 1
                                                yield 2
                                            }

                                            (async function() {
                                                for await(const n of foo()) {
                                                    console.log(n) // 1

                                                    break;
                                                }
                                            }) ();
                                        
                                    
wave