JavaScript) 3

여기서는 웹프로그래밍의 최고봉이라고 할 수 있는 자바스크립트를 살펴봅니다

자바스크립트는 웹문서에 동적인 움직임을 주기위해 만들어진 프로그래밍 언어로서, 자바스크립트로 작성한 프로그램이 스크립트인데, 브라우저에 내장된 자바스크립트 엔진은 스크립트 코드를 읽어들여서 분석하고(= 파싱), 컴퓨터가 알 수 있는 기계어로 번역하여(= 컴파일), 그 코드 내용을 실행해나가게 된다!

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

객체의 프로토타입

스크립트에서 모든 객체 인스턴스는 자신을 생성한 생성자 함수의 prototype 이 가리키는 프로토타입 객체를 자신의 부모 객체로 설정하는 __proto__ 링크를 통해 연결되는데, 이러한 프로토타입 체인을 통해 그 최종 단계에 위치한 Object 객체의 메서드는 모든 내장 객체로 전파되며, 모든 인스턴스에서 사용할 수 있게 된다!

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

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


✓   객체의 데이터 프로퍼티는 프로토타입 체인이 아니라 인스턴스에 정의해야 한다. 따라서, 주의 겸 항시 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를 사용하면; 프로토타입 체인상에만 정의된(해당 객체 자체적으로는 보유하지 않은, 상속된) 프로퍼티를 건너뛸 수 있다!

2. 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


✓   자바스크립트 표준 스펙에서 [[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 로 바운딩된 새로 생성된 객체가 반환된다 때문에 생성자 함수에서는 일반적으로 리턴값을 지정하지 않는다!


✓   일반 함수 호출의 경우 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 속성에 정의되어 있는 것이다!

프로토타입 체이닝과 동적 디스패치
1. 프로토타입 체이닝은 객체의 특정 프로퍼티를 읽으려고 할 때 발생한다 - 곧, (그 프로퍼티가 해당 객체에 없다면)체인을 거슬러 올라가면서 검색하는 것이다:
                                    
                                        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 ← 가까운 곳에서 못 찾으면; 프로토타입 체인을 거슬러 위로 올라가며 찾는다!
                                    
                                
2. 값을 쓰려고 할 때라면; 값을 읽을 때와는 달리, 동적으로 프로퍼티가 추가되고 값이 들어간다. 따라서 (상속받은 같은 이름의 프로퍼티가 있다면)체인의 윗 단계에 존재하는 같은 이름의 프로퍼티를 가리게된다('동적 디스패치!')
                                    
                                        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 프로퍼티는 가려진다!
                                    
                                

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}
                                    
                                

A instanceof BA 의 프로토타입 체인상에 B 생성자 함수 클래스가 있는지 여부를 확인한다. 반면, B isPrototypeOf(A)는 단순히 A 가 클래스 B (B 는 생성자 함수가 아니어도 상관없다!)의 멤버인지 여부를 확인한다!

2. 깊은 복사를 하는 Object.assign() 메서드를 쓰면 좀 더 간단하다. Object.assign(대상객체, 소스객체[, 소스객체2, ..]) 메서드는 소스객체 의 열거 가능한 (심볼도 포함하여)자체 프로퍼티를 대상객체 로 (값으로)복사하는데, 소스객체 의 프로퍼티는 대상 객체 에 있는 같은 이름의 프로퍼티를 덮게 된다:
                                    
                                        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 자체가 변경된다!
                                    
                                

대상객체 를 보존하면서 소스객체 에 넣어둔 기본값을 복사해서 쓰고자 한다면; 빈 객체를 만들어 사용하면 된다: Object.assign({}, 대상객체, 소스객체)

클래스란?

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

클래스 선언 및 정의
클래스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 를 사용해서는 안된다!


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시간 후에 만료됩니다!
                                    
                                

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

                                    
                                        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일 걸립니다. 개인 소지품 외에.. 등반대 깃발, 플랜카드도 잊지 마십시오!!
                                    
                                

객체가 클래스의 인스턴스인지를 확인할 때는 instanceof 연산자를 사용하면 된다: 변수객체 instanceof 클래스

클래스의 정적 메서드
클래스의 정적 메서드는 클래스에 관련되지만, 인스턴스와는 관련이 없는 범용적인 작업에 사용된다. 클래스의 인스턴스화 없이 바로 호출하는 정적 메서드는 메서드 이름 앞에 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__ 체인을 통해 함께 연결되며, 이는 상속 보다는 위임 에 가깝다 - 여러 면에서 위임은 상속보다 객체를 결합하는 더 유연한 방법이다!

즉, 생성자와 프로토타입을 사용하여 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()
                                        }
                                    
                                

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


이터러블 객체는 이터레이터를 반환하는 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!']
                                    
                                

✓   이터레이터가 항상 끝까지 실행되지는 않는다. 이터레이터를 해체 할당과 함께 사용할 때 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() 함수를 호출하더라도 각기 다른 이터레이터가 생성되어 서로간에 간섭은 생기지 않는다!

제너레이터

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

제너레이터 함수
1. 제너레이터 함수는 호출한 즉시 실행되지는 않고, 대신 이터레이터를 반환한다. 그리고 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가 됨

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문을 전달받는다
                                    
                                

제너레이터 안에서 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('이벤트명', 콜백), 그리고 서버의 응답을 받을 때까지 대기해야 하는 네트워크 요청 및 파일시스템 작업 등..

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

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은 모두 동기적으로 수행된다!

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

(* MDN의 프라미스 진행과정 이미지 참조 요)


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


✓   .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))
                                    
                                

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

프라미스 만들기

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

프라미스 만들기
먼저 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()를 호출해 거부한다 실행 함수에서 오류를 던지면; 프로미스는 거부되며, 실행 함수의 반환값은 무시된다!


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

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)
                                            }

                                            asyncFnc() // calling: (3초가 지나서)성공적으로 이행됨!
                                        
                                    

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 .. 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;
                                                }
                                            }) ();
                                        
                                    

Fetch API

클라이언트에서 서버에 자신의 요청(검색창에서의 검색어, 로그인 페이지에서의 id/password 등..)을 전달하면; 서버는 클라이언트에게 파일 및 텍스트 등을 제공하는데, 프라미스에 기반한 Fetch API는 네트워크 리소스 취득을 위한 비동기식 인터페이스를 제공하며, 콜백 기반의 XMLHttpRequest보다 더 강력하고 유연한 조작이 가능하다!

Fetch API 요청과 응답
Fetch API는 HTTP 파이프라인을 구성하는 네트워크 요청과 응답 등의 요소를 자바스크립트에서 접근하고 조작할 수 있도록 Headers(요청/응답 헤더), Request(리소스 요청), Response(요청에 대한 응답) 등의 인터페이스를 제공한다. fetch()를 사용하려면; 먼저, API 끝점(엔드포인트)가 필요한데, fetch(URL) 메서드로 리소스 요청을 생성하여 비동기적으로 네트워크 리소스를 취득할 수 있다:
                                    
                                        const f= fetch('https://jsonplaceholder.typicode.com/posts/1')

                                        console.log(f) // Promise {<pending>} ← 프라미스가 '대기'(pending) 중임..
                                    
                                

프라미스가 '대기'(= pending) 중이니 콘솔을 열어보십시오: [[Prototype]]: Promise, [[PromiseState]]: "fulfilled", [[PromiseResult]]: Response

➥ Headers 객체

Headers 객체(= 응답 객체의 headers 프로퍼티)는 has() 메서드로 존재 여부를 확인하거나 get() 메서드를 써서 헤더의 값을 읽어올 수 있다. Headers 객체는 이터러블이므로 다음과 같이 헤더를 읽을 수도 있다:

                                        
                                            fetch(url).then(response => {
                                                for(let [name, value] of response.headers) {
                                                    console.log(`${name}: ${value}`)
                                                }
                                            });
                                        
                                    
1. Fetch API는 3단계로 동작한다: 먼저, 컨텐츠를 가져올 URL을 전달하면서 fetch()를 호출한다. 다음, HTTP 응답이 도착하기 시작하면; 1단계에서 비동기적으로 반환한 응답 객체를 가져오고, 이 응답 객체의 메서드를 호출해 응답 바디를 가져온다. 마지막으로, 2단계에서 비동기적으로 반환한 바디 객체를 사용해 필요한 일을 수행한다
[ fetch()로 JSON 데이터 가져오기 ]
                                        
                                            fetch("/api/users/current") // HTTP GET 요청
                                            .then(response => response.json()) // 응답 바디를 JSON 객체로 파싱한다
                                            .then(User => { // 파싱된 객체를 사용한다
                                                displayUserInfo(User) // 내용 출력
                                            });
                                        
                                    
[ async .. await 문에서 fetch() 사용하기 ]
                                        
                                            async function isServiceReady() {
                                                let response= await fetch("/api/service/status")
                                                let body= await response.text() // 텍스트 데이터 가져오기

                                                return body === "ready";
                                            }
                                        
                                    
2-1. 가장 단순한 형태의 fetch(URL)는 가져오고자 하는 리소스의 URL 을 나타내는 하나의 인수만 받아서 프라미스로 반환하는데(여기에 응답 body까지 모두 포함되어 있지는 않다!), 반환된 Response 객체에서 JSON 본문 콘텐츠를 추출하기 위해서는; 다시 json() 메서드를 호출해야 하며, json()은 응답 body를 JSON으로 파싱한 결과로 이행하는 또 다른 프로미스를 반환하게 된다:
                                    
                                        async function logJSONData() {
                                            const response= await fetch("https://jsonplaceholder.typicode.com/todos/1");
                                            const jsonData= await response.json();

                                            console.log(jsonData)
                                        }

                                        logJSONData() // {completed: false, id: 1, title: 'delectus aut autem', userId: 1}
                                    
                                
2-2. fetch(URL)는 네트워크에서 리소스를 취득하기 위한 요청을 시작하고, 응답이 사용 가능해지면; Response 객체로 '이행'하는 프로미스를 반환한다 - 곧, fetch(URL)은 가져오려는 리소스 URL 을 매개변수로 받아 해당 요청에 대한 응답으로 이행하는 프라미스를 반환하는데, 서버로부터 헤더를 포함한 응답을 받는 순간 즉시 '이행'한다(이는 서버가 HTTP 오류 코드로 응답해도 '이행'한다는 의미이다!):
                                    
                                        fetch('https://jsonplaceholder.typicode.com/todos/1') // fetch()로 서버에 GET 요청을 하고..
                                        .then(response => { // 서버측 응답이 오면(곧, 네트워크 접속 오류는 아니다!); 우선,
                                            if(! response.ok) { // 예상할 수 있는 에러(우선, HTTP 에러)를 처리한다
                                                throw new Error(`HTTP 에러! ${response.status}`);
                                            } // IF 조건에서 데이터 타입이 맞는지 등도 추가적으로 체크해줄 수 있다!

                                            return response.json(); // 성공 시: 바디를 파싱하여 다음 단계로 '이행'할 프라미스를 반환한다
                                        }).then(todo => { // 프라미스가 '해석'되면; 원하는 작업을 수행하도록 한다
                                            console.log(todo.title) // delectus aut autem
                                        }).catch(e => { // 예기치 못한 에러를 처리한다
                                            console.log(e)
                                        });
                                    
                                

✓   웹서버가 fetch() 요청에 응답한다면(오직 네트워크 접속불가 등의 심각한 오류가 있을 때만 '거부'되며, 404 등의 HTTP 오류 시는 '거부'하지 않는다!); 프라미스는 즉시 '이행'되는데, 이에 fetch()는 HTTP 상태와 응답 헤더를 받는 즉시 프라미스를 '해석'한다(응답 바디는 아직 도착하지 않았지만!) 그러므로, .then() 처리기는 반드시 Response.ok(= 서버의 응답을 받음) 및 Response.status 속성을 확인해야 한다!

fetch()로 텍스트 가져오기

fetch()로 이미지 불러오기


아직 완료되지 않은 fetch() 작업을 취소하려면 new AbortController()AbortSignal 인터페이스를 사용할 수 있다:

                                    
                                        const controller= new AbortController();
                                        const signal= controller.signal
                                        const url= "video.mp4"

                                        const downloadBtn= document.querySelector("#download")
                                        const abortBtn= document.querySelector("#abort")

                                        downloadBtn.addEventListener("click", async () => {
                                            try {
                                                const response= await fetch(url, { signal });
                                                console.log("다운로드 완료", response)
                                            } catch(error) {
                                                console.error(`다운로드 오류: ${error.message}`)
                                            }
                                        });

                                        abortBtn.addEventListener("click", () => {
                                            controller.abort()
                                            console.log("다운로드 중단됨")
                                        });
                                    
                                

Fetch() 요청

요청 매개변수 전달하기
POST, PUT, DELETE 같은 요청 메서드를 사용하려면; fetch()의 두번째 매개변수로 옵션 객체 를 전달해주어야 한다: fetch(URL, { .. })
[ fetch() 옵션 설정하기 ]
                                        
                                            fetch(url, {
                                                method: "POST",
                                                body: "hello, world" // 서버로 보낼 데이터
                                            })
                                        
                                    

요청 객체를 사용하면; 브라우저는 자동으로 Content-Length 헤더를 요청에 추가한다. 예컨대, 위 경우라면 브라우저는 컨텐츠 타입을 text/plain; charset=UTF-8로 설정한다

스크립트 Post 요청 예제

1. fetch() 요청에 URL 과 함께 요청 매개변수(?이름-값)를 전달할 수도 있다:
                                    
                                        async function search(t) {
                                            let url= new URL("/api/search")
                                            url.searchParams.set("q", t)

                                            let response= await fetch(url)
                                            if(! response.ok) {
                                                throw new Error(response.statusText);
                                            }

                                            let resultsAry= await response.json()
                                            return resultsAry;
                                        }
                                    
                                
2. fetch() 호출에 리소스의 경로를 제공하는 대신, new Request() 생성자로 생성한 요청 객체를 인자로 전달할 수도 있다:
                                    
                                        let request= new Request(url, {headers});

                                        fetch(request).then(response => ..);
                                    
                                
                                    
                                        async function fetchImage(request) {
                                            try {
                                                const response= await fetch(request)
                                                if(!response.ok) throw new Error("네트워크 응답이 OK가 아님");

                                                const myBlob= await response.blob();
                                                myImage.src= URL.createObjectURL(myBlob)
                                            } catch (error) {
                                                console.error("오류:", error)
                                            }
                                        }

                                        const myHeaders= new Headers();
                                        const myRequest= new Request("flowers.jpg", {
                                            method: "GET", headers: myHeaders, mode: "cors", cache: "default"
                                        });

                                        fetchImage(myRequest)
                                    
                                

Request() 생성자는 fetch()와 동일한 매개변수를 받는데, 기존에 존재하는 요청 객체를 전달해서 복사본을 생성하는 것도 가능하다:

                                    
                                        const anotherRequest= new Request(myRequest, myInit)
                                    
                                

✓   요청과 응답 본문은 한 번만 읽을 수 있으므로 복사본 생성은 꽤 유용하다. 이렇게 복사본을 생성하면; 기존에 생성해둔 요청/응답 객체를 다시 사용하되 init 옵션만 교체할 수도 있다 참고로, 복사본은 원본의 본문을 읽기 전에 생성해야 한다!

JSON 직렬화

JSON은 자바스크립트 객체 문법을 따르는 문자 기반 데이터 포맷으로서, 일반적으로 웹에서 데이터를 전송할 때 사용된다. 프로그램에서 데이터를 저장하거나 네트워크를 통해 다른 프로그램으로 전송할 때 메모리상의 데이터 구조를 문자열로 변환해야 하는데, 이렇게 데이터 구조를 바이트나 문자 스트림으로 변환하는 것이 바로 데이터 직렬화이다!

JSON 객체와 직렬화
Json 객체는 스크립트 객체의 형태(키: 값)를 갖는 문자열로서, 이를 이용하면 스크립트 객체를 문자열화할 수 있고(= Stringification), 반대로 다시 객체로 환원(= Parsing)할 수도 있게 된다
1. JSON 표기법은 우선, 전체를 { }로 묶어주고, 객체의 프로퍼티(오직 프로퍼티만!) "이름"과 "문자열"은 큰 따옴표로(오직 큰 따옴표만!) (꼭!)묶어주어야 한다. JSON에 주석은 사용할 수 없으며, 숫자나 문자열, 논리값, 배열, 객체를 사용할 수 있는데, 스크립트 객체 표기법으로 작성해주면 된다:
[ JSON 데이터 계층 구축하기 ]
                                        
                                            {
                                                "squad": "Super hero",
                                                "year": 2016,
                                                "active": true,
                                                "member": [
                                                    {"name": "Kjc", "age": 49, "eat": ["사과", "배", "감"]},
                                                    {"name": "Kjh", "age": 29, "eat": ["귤", "사과", "수박"]},
                                                    {"name": "Lee", "age": 35, "eat": ["오이", "감자", "고구마"]}
                                                ]
                                            }
                                        
                                    

위와 같이 데이터 계층으로 구축한 객체를 자바스크립트 프로그램으로 로드하고, 예컨대 const members= { .. }와 같이 변수로 파싱해주면 .[] 표기법을 통해 객체 내 데이터에 접근할 수 있다: members.year, members["member"][1]["eat"][2]

2. JSON.stringify(value[, replacer])valuereplacer (변환 함수)로 변환하여 제이슨 문자열로 반환한다. 반대로, JSON.parse(text[, reviver])textreviver(변환 함수)로 변환하여 스크립트 코드로 돌려준다:
                                    
                                        let obj= {s: "", n: 0, a: [true, false, null]}

                                        let str= JSON.stringify(obj) // 객체를 문자열 형태로 변환
                                        console.log(str) // {"s":"","n":0,"a":[true,false,null]}

                                        let re= JSON.parse(str) // 직렬화한 문자열을 객체 형태로 환원
                                        console.log(re) // {s: '', n: 0, a: [true, false, null]}
                                    
                                
                                    
                                        const obj= { // 객체
                                            name: 'Kjc', region: '경주시', age: 29
                                        }

                                        const replacer= (key, value) => { // JSON 데이터 변환 함수
                                            if(typeof value === 'number') return undefined;
                                            else return value;
                                        }

                                        const str= JSON.stringify(obj, replacer, ' ') // 변환함수를 써서 문자열로 변환한다
                                        console.log(str) // { "name": "Kjc", "region": "경주시" }
                                    
                                

JSON.stringify(value, null, ' ')와 같이 3번째 인수로 ' ' (공백), '\t' () 등을 줄 수도 있는데(콘솔에서는; 여러 행 형식으로 표시된다), JSON.parse()를 써서 다시 객체 형태로 변환할 때는; 이 공백은 무시하므로 문제없다!


✓   JSON은 함수, 정규표현식, 형식화 배열, 맵과 셋, Error 객체, Date 객체, undefined, Symbol 등은 직렬화할 수 없다. 또한, 객체 자신이 가지고 있는 열거 가능한 프로퍼티만 직렬화하며, 직렬화할 수 없는 프로퍼티는 문자열로 출력되지 않는다!

JSON Placeholder

사이트에서는 더미 데이터가 필요할 때마다 사용할 수 있는 무료 온라인 REST API 서비스를 제공한다

JSON 플레이스홀더 사용하기
JSON Placeholder는 데모 웹사이트나 로컬 컴퓨터에서의 테스트를 위해 더미 JSON 데이터를 요청하여 불러오고자할 때 이용할 수 있다 (* 더 자세한 사용법은 JSONPlaceholder Guide 참조 요)
[ JSON 리소스 가져오기 ]
                                        
                                            fetch('https://jsonplaceholder.typicode.com/todos/1')
                                            .then(response => response.json())
                                            .then(json => console.log(json)) // {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
                                        
                                    

✓   아래는 {JSON} Placeholder 사이트의 /todos 에서 제공하는 JSON 데이터의 내용이다:

                                        
                                            [ /* /todos */
                                                {
                                                    "userId": 1,
                                                    "id": 1,
                                                    "title": "delectus aut autem",
                                                    "completed": false
                                                }, {
                                                    "userId": 1,
                                                    "id": 2,
                                                    "title": "quis ut nam facilis et officia qui",
                                                    "completed": false
                                                },
                                                // ..
                                            ]
                                        
                                    

/todos 내 모든 데이터를 가져오려면; fetch('https://jsonplaceholder.typicode.com/todos')로 요청하면 된다!

                                                
                                                    fetch('https://jsonplaceholder.typicode.com/posts', {
                                                        method: 'POST',
                                                        body: JSON.stringify({title: 'foo', body: 'bar', userId: 1}),
                                                        headers: {'Content-type': 'application/json; charset=UTF-8'}
                                                    }).then((response) => response.json())
                                                    .then((json) => console.log(json)) // {body: "bar" id: 101 title: "foo" userId: 1}
                                                
                                            

참고로, 이 데이터가 실제로 서버에 업데이트되는 것은 아니다!

                                                
                                                    fetch('https://jsonplaceholder.typicode.com/posts/1', {
                                                        method: 'PUT',
                                                        body: JSON.stringify({id: 1, title: 'foo', body: 'bar', userId: 1}),
                                                        headers: {'Content-type': 'application/json; charset=UTF-8'}
                                                    }).then((response) => response.json())
                                                    .then((json) => console.log(json)) // {body: "bar" id: 1 title: "foo" userId: 1}
                                                
                                            

                                                
                                                    fetch('https://jsonplaceholder.typicode.com/posts/1', {
                                                        method: 'PATCH',
                                                        body: JSON.stringify({body: 'jc'}),
                                                        headers: {'Content-type': 'application/json; charset=UTF-8'}
                                                    }).then((response) => response.json())
                                                    .then((json) => console.log(json)) // {body: "jc" id: 1 title: "foo" userId: 1}
                                                
                                            

                                                
                                                    fetch('https://jsonplaceholder.typicode.com/posts/1', {
                                                        method: 'DELETE'
                                                    })
                                                
                                            

                                                
                                                    // 첫번째 유저에 속한 모든 게시물을 가져온다
                                                    fetch('https://jsonplaceholder.typicode.com/posts?userId=1')
                                                    .then((response) => response.json())
                                                    .then((json) => console.log(json));
                                                
                                            

                                                
                                                    // 중첩된 리소스 필터링: /comments?postId=1
                                                    fetch('https://jsonplaceholder.typicode.com/posts/1/comments')
                                                    .then((response) => response.json())
                                                    .then((json) => console.log(json))
                                                
                                            

{JSON} Placeholder 사이트의 중첩된 라우트 목록: /posts/1/comments, /albums/1/photos, /users/1/albums, /users/1/todos, /users/1/posts

URL 쿼리스트링

브라우저에 URL 을 입력하거나 링크 를 클릭하면; 브라우저는 웹서버에 HTTP 요청을 보내고( URL 경로쿼리스트링 ), 웹서버에서는 이 요청 을 받아 어떻게 반응할 지를 결정하여 응답 하게 된다!

URL 객체
1. URL(= 웹 주소, 또는 링크)은 인터넷에서 웹페이지, 이미지, 비디오 등 리소스의 위치를 가리키는 문자열인데, URL 을 제대로 인코딩/디코딩하려면; encode/decodeURI()encode/decodeURIComponent() 같은 String 객체의 메서드보다는 URL 객체를 쓰는 것이 보다 확실하다!
[ URL의 각 부분들 ]
                                        
                                            let url= new URL("http://www.sosohan.xyz:80/about?test=1#history=express")

                                            console.log(url.href) // http://www.sosohan.xyz/about?test=1#history=express
                                            console.log(url.origin) // http://www.sosohan.xyz
                                            console.log(url.protocol) // http:
                                            console.log(url.host) // www.sosohan.xyz ← 'url.hostname'도 같다
                                            console.log(url.port) // 80
                                            console.log(url.pathname) // /about ← /
                                            console.log(url.search) // ?test=1 ← ?
                                            console.log(url.hash) // #history=express ← #
                                        
                                    
                                        
                                            let url= new URL("ftp://admin:1237!@ftp.example.com/")

                                            console.log(url.href) // ftp://admin:1237!@ftp.example.com/
                                            console.log(url.origin) // ftp://ftp.example.com
                                            console.log(url.username) // admin
                                            console.log(url.password) // 1237!
                                        
                                    
2. URLsearch 속성은 URL 의 쿼리 문자열을 가져오며, searchParams.get() 메서드를 쓰면 쿼리 문자열의 개별 매개변수 값을 조회할 수도 있다:
                                    
                                        /* URL 가져오기 */
                                        let url= new URL("http://www.sosohan.xyz:80/about?test=1&pages=news#history=express")
                                        console.log(url.search) // ?test=1&pages=news

                                        try {
                                            console.log(url.searchParams.get("test")) // 1
                                            console.log(url.searchParams.get("pages")) // news
                                        } catch(e) { console.error("오류:", e) }
                                    
                                
                                    
                                        /* URL 변경하기 */
                                        let url= new URL("http://sosohan.xyz") // 서버 주소

                                        url.pathname= "api/search" // API 엔드포인트 경로 추가
                                        url.search= "q=test" // 검색 매개변수 추가
                                        console.log(url.toString()) // http://sosohan.xyz/api/search?q=test
                                    
                                

이제, http://sosohan.xyz 로 접속하면; http://sosohan.xyz/api/search?q=test 로 이동하게 된다 물론, 변경된 URL 로 이동하도록 하는 적절한 작업이 추가되어야 한다!

➥ TCP/IP 전송 프로토콜

HTTP 통신에서는 클라이언트가 서버에 요청 메시지를 보내고, 이에 서버는 클라이언트에 응답 메시지를 보내는데, 이는 TCP/IP 전송 프로토콜에 의거하여 IP주소:포트를 통해 연결된다!

http://www.sosohan.xyz:80/about?test1=1&test2=2#history=express

  • 전송 프로토콜 TCP http:// http, https, file, ftp 등..
  • 호스트 서버 IP www.sosohan.xyz(= 웹 주소) localhost(= 현재 컴퓨터)
  • 포트 :80 생략 시; http에서는 80, https에서는 443 으로 간주되며, 포트 0 ~ 1023 까지는 예약된 번호이므로, 일반적으로는 1024 ~ 65536 까지 쓸 수 있다!
  • 경로 /about
  • 쿼리스트링 ?test1=1&test2=2 ?로 시작하는 이름=값&로 구분하며, 모두 URL 인코딩을 사용해야 한다!
  • 해시태그 #history=express #으로 시작하는 해시는 서버로는 전송되지 않고 브라우저에서만 사용되는데, 이는 문서 내부 내비게이션 용도로도 사용할 수 있다!
URLSearchParams 객체
URLSearchParams 인터페이스는 URL 의 쿼리 문자열을 대상으로 작업할 수 있는 다양한 메서드들을 제공하는데, URLSearchParams 객체는 for .. of 문으로 직접 키/값 쌍을 순회할 수 있고, 키/값 쌍의 순회 순서는 쿼리 문자열에 나타나는 순서와 같다
[ 쿼리 매개변수 순회하기 ]
                                        
                                            let params= new URLSearchParams("test=1&pages=news")

                                            /* for .. of 문으로 검색 매개변수 순회하기 */
                                            for(const p of params) {
                                                console.log(p) // ['test', '1'] ['pages', 'news']
                                            }

                                            for(const [key, value] of params) {
                                                console.log(`${key}: ${value}`) // test: 1 pages: news
                                            }

                                            for(const [key, value] of params.entries()) {
                                                console.log(`${key}: ${value}`) // test: 1 pages: news
                                            }

                                            /* URLSearchParams 객체의 forEach() 메서드로 순회하기 */
                                            params.forEach((value, key) => {
                                                console.log(`${key}: ${value}`) // test: 1 pages: news
                                            });
                                        
                                    

그외, 검색 매개변수의 순회에는 URLSearchParams 객체params.entries(), params.keys(), params.values() 등의 메서드를 쓸 수 있고, params.sort()로 키 값으로 정렬할 수도 있다 모두 배열과 객체의 순회에서 익히 살펴본, 비슷한 것들이니 더 설명 안합니다..

1. URL 객체의 검색 부분(= query) 참조 시; 쿼리 전체를 다루는 search만 아니라 쿼리의 각 부분들을 다루는 searchParams 프로퍼티도 사용할 수 있는데, 이 속성은 URLSearchParams 객체의 읽기 전용 참조이며, URL의 검색 부분에 포함된 매개변수를 읽고, 쓰고, 추가/삭제, 정렬하는 API를 제공한다:
[ URLSearchParams 객체의 메서드 정리 ]
                                        
                                            let params= new URLSearchParams("http://www.sosohan.xyz/about?test=1&pages=news")

                                            params.has("pages") // true ← 존재 여부 확인
                                            params.get("pages") // news ← 값 가져오기
                                            params.getAll("pages") // ['news'] ← 모든 값 (배열로)가져오기

                                            params.append("pages", "webdev") // pages 추가 ← &로 연결된다!
                                            console.log(params.toString()) // http%3A%2F%2Fwww.sosohan.xyz%2Fabout%3Ftest=1&pages=news&pages=webdev

                                            params.set("pages", "more webdev") // pages와 그 값 설정 ← 이미 존재하면 값을 변경한다!
                                            console.log(params.toString()) // http%3A%2F%2Fwww.sosohan.xyz%2Fabout%3Ftest=1&pages=more+webdev

                                            params.delete("pages") // pages 제거
                                            console.log(params.toString()) // http%3A%2F%2Fwww.sosohan.xyz%2Fabout%3Ftest=1
                                        
                                    
2. URL 매개변수를 쿼리스트링으로 인코딩해야 한다면; URLSearchParams 객체를 생성하고 매개변수를 추가한 다음 문자열로 변환해서 URL의 search 프로퍼티로 설정할 수 있다:
                                    
                                        let url= new URL("http://sosohan.xyz") // 서버 주소
                                        let params= new URLSearchParams() // URLSearchParams 객체의 인스턴스 생성

                                        params.append("q", "tm") // 쿼리 추가
                                        params.append("opts", "ex") // 다시 쿼리 추가
                                        console.log(params.toString()) // q=tm&opts=ex ← 추가된 쿼리는 &로 연결된다!

                                        url.search= params // params의 쿼리 문자열을 가져와서 url에 넣기
                                        console.log(url.href) // http://sosohan.xyz/?q=tm&opts=ex
                                        console.log(...params) // ['q', 'tm'] ['opts', 'ex'] ← URLSearchParams 객체는 이터러블이다!
                                    
                                
➥ 구형 URL 함수들의 문제점

1. encodeURI()는 문자열을 인자로 받아 ASCII 문자가 아닌 문자와 스페이스 같은 일부 ASCII 문자를 이스케이프한 새 문자열을 반환하는데, decodeURI()는 그 역이다: 이스케이프할 문자를 먼저 UTF-8 인코딩으로 변환한 뒤, 각 바이트를 %xx(xx 는 16진수 숫자)로 대체한다 - 이 함수는 URL 전체를 인코드할 목적으로 설계되었기에 /, ?, # 같은 URL 구분자는 이스케이프하지 않는데, 그로 인해 이런 문자들이 포함된 URL은 정확히 처리하지 못한다!

2. encodeURIComponent()는 URL의 각 구성요소를 인코드할 목적으로 설계되었기에 /, ?, # 같은 URL 구분자도 이스케이프하지만, 경로에 포함된 /까지도 이스케이프하므로 주의가 요구된다. 또한, URL의 공백은 +로 이스케이프하도록 되어 있지만, 실제로는 %20으로 변환한다!


✓   인코딩/디코딩 전역 함수인 encodeURI()decodeURI(), encodeURIComponent()decodeURIComponent() 등은 URL 전체에 단 한 가지 인코딩 스키마를 적용하려 하기에 문제가 있다. 하지만, URL의 각 부분은 서로 다른 인코딩을 적용해야 하며, 따라서 URL을 제대로 인코딩하려면; URL 클래스를 쓰는 것이 좋다!

ES6 Module

ES6 모듈은 일반적인 '스크립트'와는 달리, 각 모듈마다 비공개 컨텍스트가 있으며 import, export 문을 사용할 수 있다. 모든 ES6 모듈 코드는 자동적으로 스트릭트 모드로 들어가는데, 이 '스트릭트 모드'는 보다 엄격하여, 예컨대 최상위 코드에서도 thisundefined 값을 갖게된다!

모듈 내보내기: export
익스포트는 변수 및 상수, 함수나 클래스, 객체 등을 내보낼 수 있는데(export const PI= Math.PI), 스크립트 코드의 최상위 레벨에서만 존재한다 - 클래스나 함수, 루프, 조건문 안에서는 값을 내보낼 수 없다. 따라서, 모듈은 항상 같은 값을 내보내며, 이로 인해 정적 분석이 가능해진다:
[ 모듈 내보내기: _js.js ]
                                        
                                            const PI= Math.PI // 상수

                                            function hi(name) { // 함수
                                                return `hi, {name}!`
                                            }
                                            hi('Kjc')

                                            export { PI, hi } // 다수의 변수나 상수, 함수, 클래스 등을 묶어서 내보내기 ← {}가 객체를 의미하는 것은 아니다!
                                        
                                    

모듈 하나만 내보낼 때는; 보통 export 대신 export default를 사용하는데, 일반 익스포트 내보내기가 이름이 있는 선언에서만 사용할 수 있는 반면, 여기서는 익명함수 표현식이나 객체 리터럴까지도(따라서, 여기서의 {}는 실제 객체 리터럴을 의미하게 된다!) 내보낼 수 있다: export default class Circle { .. } 참고로, 디폴트 내보내기에서 이름없이 내보낸 경우에는; 임포트 시 이름을 지정해주면서 가져오면 된다!

모듈 가져오기: import
임포트 또한 익스포트와 마찬가지로 스크립트 코드의 최상위 레벨에서만 존재할 수 있으며, export default로 내보낸 경우에는 import Circle from './class-set.js'(Circleclass-set.js 에서 디폴트로 내보내기한 값이다)와 같이 가져올 수 있는데, 임포트 선언은 최상단으로 끌어올려지므로 어느 위치에든 둘 수 있으며, 임포트한 식별자 Circle 은 상수로 선언된다. 반면, 일반 임포트는 이름이 있는 익스포트 선언에서만 사용할 수 있다: import { PI, hi } form './_js.js'

임포트하는 파일의 경로는 ./(= 현재 디렉토리) 및 ../(= 상위 디렉토리)로 시작하는 상대 경로, 또는 완전한 URL 중 하나여야 한다. 현재 파일의 위치를 기준으로 하는 상대 경로는 아래와 같이 작성해주면 된다:

[ 임포트 상대경로 지정하기 ]
                                        
                                            ./util.js ← 현재 폴더의 util.js
                                            ../util.js ← 한단계 상위 폴더의 util.js
                                            ../Archi/util.js ← 형제 폴더 Archi의 util.js
                                            default/util.js ← 하위 폴더 default의 util.js
                                        
                                    

참고로, 현재 폴더의 util.js 파일 임포트 시 util.js와 같이 쓰는 것은 권장되지 않는다. 반드시 ./를 붙인 상대경로로 적어야 한다!

1. 모듈 모두를 가져올 때는 import * as utils from './util.js' 식으로 사용하여 util.js 의 모든 모듈을 utils 란 이름으로(각각의 모듈들은 utils 객체의 프로퍼티가 된다) 불러와서 utils.모듈명()와 같은 방식으로 사용할 수 있다:
[ 모듈 전체 가져오기 ]
                                        
                                            import * as utils from './util' // (객체)utils에 util.js의 모든 모듈을 불러온다

                                            function hi(name) {
                                                return `hi, ${utils.caps(name)}!` // utils 객체에 속한 메서드처럼 필요한 모듈을 호출하여 사용한다!
                                            }
                                            hi('Kjc')
                                        
                                    

모듈 가져오기에서 이름 충돌이 있는 경우(예컨대, 변수의 이름 충돌 문제가 생겨날 때)는 as 키워드를 써서 이름을 변경하여 가져오면 된다: import { render as renderUI } from './ui.js'

2. 익스포트 시 일반 익스포트와 익스포트 디폴트를 섞어서 내보낼 수도 있는데, 이때는 다음과 같은 방식으로 임포트할 수 있다:
                                    
                                        import Circle from './util.js' // 익스포트 디폴트로 내보낸 Circle만 가져온다
                                        import Circle, { PI, hi } from './util.js' // 일반 익스포트로 내보낸 PI, hi도 함께 가져온다
                                    
                                

내보내기가 없는 모듈을 임포트할 수도 있다. 예컨대, 어떤 이벤트에 대응하여 특정 데이터를 서버로 보내도록 설정된 파일과 같은 경우이다:

                                    
                                        import './ani.js'
                                    
                                
인라인 스크립트 모듈
웹문서에서 인라인 임포트를 사용하려면; type= "module" 속성을 써서 모듈 코드임을 알려주어야 한다. 이는 즉시 스크립트 코드를 불러오되 html 분석이 끝난 뒤에 순차적으로 실행되는, defer 속성을 준 것과 같은 방식으로 작동된다:
[ 인라인 import 스크립트 ]
                                        
                                            <script type="module">import "./main.js"</script>
                                        
                                    

참고로, async 속성은 html 분석이 끝나지 않았어도 스크립트간 순서와 무관하게 코드를 불러오는 즉시 실행한다!


일반적인 스크립트와 모듈 스크립트는 교차출처 정책에도 차이가 있다. 일반 <script> 태그는 인터넷에 존재하는 어떤 서버에서든 스크립트 코드를 불러오지만(예: 인터넷 광고, 분석, 추적 등), type= "module 속성은 교차출처 정책에 충실하다!

동적 import()

웹에서는 코드를 파일 시스템에서 읽지 않고 네트워크를 통해 전송되므로, 프로그램 전체를 불러와야만 실행을 시작하는 정적 모듈 가져오기는 적당하지 않다. 따라서 비동기식 동적 로딩이 요구되는데, 이 문제에 대한 해결책으로 나온 것이 바로 동적으로 모듈을 가져오는 import()이다!

동적 import()
모듈 지정자를 import()에 전달하면; import()는 지정된 모듈을 비동기로 불러오고 실행하는 프라미스 객체를 반환한다. 동적 가져오기가 완료되면; 프라미스는 '이행'되며, 정적 가져오기 문 형태로 import * as를 사용한 것과 같은 객체를 반환하게 된다(import * as stats from './stats.js'). 이 모듈은 다음과 같이 동적으로 가져와서 사용할 수 있다:
[ 동적 import() 사용하기 ]
                                        
                                            // 동적 임포트:
                                            import('./stats.js').then(stats => {
                                                let average= stats.mean(data)
                                            })
                                        
                                    
                                        
                                            // async .. await 문:
                                            async aData(data) {
                                                let stats= await import('./stats.js')
    
                                                return { average: stats.mean(data), stddev: stats.stddev(data) }
                                            }
                                        
                                    

참고로, import()는 함수가 아니라 특별히 ()가 붙은 연산자이다. 따라서, import()의 모듈 지정자는 문자열 리터럴만 아니라 적절한 문자열 형태로 평가되는 표현식도 사용할 수 있다!

임포트 메타
1. import.meta는 현재 실행 중인 모듈에 관한 메타 데이터를 담은 객체를 참조하는데, 이는 쿼리 매개변수(?) 및 해시(#)를 포함하는, 모듈에 대한 전체 URL 이다. 곧, import.meta.url은 주로 모듈과 같은 디렉토리, 또는 그 디렉토리에 상대적인 경로를 통해 이미지, 데이터 파일 등의 자원을 참조하는데 쓰인다:
[ import.meta.url ]
                                        
                                            <script type="module">
                                                import "./index.mjs?someURLInfo=5"
                                            </script>
                                        
                                    
                                        
                                            // index.mjs
                                            new URL(import.meta.url).searchParams.get("someURLInfo") // 5
                                        
                                    
                                        
                                            // index.mjs
                                            import "./index2.mjs?someURLInfo=5"

                                            // index2.mjs
                                            new URL(import.meta.url).searchParams.get("someURLInfo") // 5
                                        
                                    

이 객체의 url 속성은 브라우저에서는; 모듈을 불러온 (외부 스크립트의)URL, 또는 포함된 문서의 (인라인 스크립트의)URL 이며, 노드에서는; file://URL와 같은 프로토콜을 포함한 파일 경로이다

2. 노드의 커먼 JS 모듈에는 현재 모듈이 포함된 폴더의 절대경로를 포함하는 __dirname 변수가 있으며, 이는 상대경로를 절대경로로 치환하는 데 유용하다. import.meta.url을 사용하면; ES6) 모듈에서도 (파일시스템 경로 대신)URL 을 써서 파일의 위치를 절대경로로 치환할 수 있다:
                                    
                                        const fs= require("fs/promises")
                                        const path= require("path")

                                        const filePath= path.join(__dirname, "someFile.txt")
                                        fs.readFile(filePath, "utf8").then(console.log)
                                    
                                
                                    
                                        import fs from "node:fs/promises"

                                        const fileURL= new URL("./someFile.txt", import.meta.url)
                                        fs.readFile(fileURL, "utf8").then(console.log)
                                    
                                

✓   new URL() 생성자를 사용하면; 상대 URL을 import.meta.url 같은 절대 URL을 기준으로 쉽게 해석할 수 있다. 예컨대, 지역에 맞게 변환해야 할 문자열이 포함된 모듈이 있고, 그 지역화 파일은 모듈과 같은 loc 폴더에 저장되어 있다고 하면; 다음과 같은 방식으로 문자열을 가져올 URL을 얻을 수 있다:

                                    
                                        function localStringsURL(locale) {
                                            return new URL(`loc/${locale}.json`, import.meta.url);
                                        }
                                    
                                
wave