2015에 나온 ES6 (ECMAScript = javascript)의 기능

 

1. 블록스코프, let, const

 

ES6에서 변수를 정의하는데 let, 상수를 정의하는데 const를 사용 할 수 있다. 그리고 이 키워드로 선언하면

 - 블록레벨의 스코프를 가짐

 - 같은 스코프에선 let으로 변수를 재정의 하는 것 불가

 - let, const는 호이스팅 적용이 안됨. 변수선언 이전에 접근 불가능함

function workout() {
	let gym = 'Gym A'; //함수 내부 스코프
	
	const gymStatuses = {'Gym ' : 'open', 'Gym B' : 'open', 'Gym C' : 'closed'};
	for (let gym in gymStatuses) {
		console.log(gym + ' is ' + gymStatuses[gym]);
	}
	
	{
		const gym = 'Gym B';
		console.log('Workout in ' + gym);
		// gym = 'Gym C';  => TypeError 재정의 불가
	}
	
	console.log('workout in ' + gym); // Gym A
	
	{
		function gym() {
			console.log('workout in a separate gym');
		}
		gym(); //스코프 내부에서만 접근 가능
	}
	
	if (gymStatuses[gym] == 'open') {
		let exercises = ['A','B','C'];
	}
	//console.log(exercises); => exercises에 접근 불가
	
	try {
		let gym = 'Gym C';
		console.log('workout in ' + gym);
		throw new Error('Gym is closed');
	} catch(err) {
		console.log(err);
		let gym = 'Gym D';
		console.log('workout in ' + gym);
        //위 try 블럭과 catch 블럭의 스코프는 다름
        //switch,for,if,try-catch 블럭과 블록구문으로 블록레벨 스코프 생성이 가능
	}
}
workout();

결과

 

 

 

2. 클래스 : 문법 설탕 (문법적으로 명확하고 이해하기 쉬움) 도입

class User {
    constructor(name, interests) {
        this.name = name;
        this.interests = interests;
    }
    greeting() { // 메소드
        console.log('Hi I\'m ' + this.name + '. ');
    }
    get interestsCount() { // interestsCount 게터 필드 접근자
        return this.interests ? this.interests.length : 0;
    }
}

class TeamMember extends User { // User 상속
    constructor(name, interests) {
        super(name, interests);
        this._tasks = [];
        this._welcomeText = 'Welcome to th team!';
    }
    greeting() {
        console.log('I \'m ' + this.name + '. ' + this._welcomeText);
    }
    work() {
        console.log('I \'m  working on ' + this._tasks.length + ' tasks ');
    }
    set tasks(tasks) {
        let acceptedTasks = [];

        if (tasks.length > TeamMember.maxTasksCapacity()) {
            acceptedTasks = tasks.slice(0, TeamMember.maxTasksCapacity());
            console.log('It\'s over max capacity. Can only task two');
        } else {
            acceptedTasks = tasks;
        }
        this._tasks = this._tasks.concat(acceptedTasks);
    }
    static maxTasksCapacity()       // 생성자 함수의 var 변수와 같은 기능
    {
        return 2;
    }
}
let member = new TeamMember('Sunny', ['Traveling']);
member.greeting();                  // I'm Sunny. Welcome to th team!
member.tasks = ['Buy three tickets', 'Book a hotel', 'Rent a car']; // It's over max capacity. Can only take two.
member.work();                      // I'm working on 2 tasks.
console.log(member.interestsCount); // 1 출력
member.interestsCount = 2;          //변경사항 저장 되지 않음
console.log(member.interestsCount); // 1 출력
console.log(member.tasks);          // undefined 출력

 

User.prototype.eat = function () {
    console.log('What Will I have for lunch?');
};
member.eat(); // What will I have for lunch? 출력

프로토타입 객체를 통해서 추가했고, 이를 통해서 상속 또한 이뤄짐

 

User.sleep = function () {
    console.log('Go to sleep');
}
member.sleep(); // Uncaught TypeError: member.sleep is not a function => 상속이 되지 않았음
User.sleep(); // Go to sleep

User 객체에 직접 내부프로퍼티를 추가하는 경우 상속이 안됨

생성자 함수의 property로 추가한 격임

sleep은 User 클래스의 정적 메소드가 됨

1. class 내부에 메소드를 정의하면 프로토타입에도 추가가 됨 => 생성자 함수에서 this 키워드를 사용한 것과 같음

2. static (정적) 메소드를 정의하면 자바스크립트는 생성자 함수에 직접 추가를 한 것과 같음

 

console.log(User.prototype.hasOwnProperty('eat')); //true
console.log(User.hasOwnProperty('eat'))// false

프로토타입을 통해 추가한 프로퍼티인 eat 는 프로토타입만 가졌고, User 객체 자체가 소유한 것이 아님

 

console.log(User.hasOwnProperty('sleep')); // true
console.log(User.prototype.hasOwnProperty('sleep')) // false

생성자를 통해 추가한 프로퍼티인 sleep 은 User 생성자는 가졌으나 User 프로토 타입은 가지지 못함 => 즉, 상속이 안되어 있음

 

 

 

 

3. 객체 리터럴이 강화 됨

 - 객체가 프로토타입을 가질 수 있도록 강화됨 ( 프로토타입 설정이 가능 )

 - 프로퍼티 축약 표현 지원

 - 메소드 축약표현을 지원함

 - super 를 호출할 수 있게 됨

 - 표현식으로 프로퍼티를 계산할 수 있음

 

const advice = 'Stay hungry. Stay foolish.';

let advisor = {
    __proto__ : new TeamMember('Adam', ['Consulting']),     // 프로토타입 설정
    advice,                                                 // 프로퍼티 축약 표현 => advice : advice  와 같음
    greeting() {                                            // 메소드 축약 표현 => greeting : function () {super.greeting(); console.log(this.advice);} 와 같다.
        super.greeting();                                   // super 메소드 호출
        console.log(this.advice);
    },
    [advice.split('.')[0]]: 'Always learn more'             // 계산된 이름을 프로퍼티 이름으로 사용함
}

console.log(TeamMember.prototype.isPrototypeOf(advisor)); // true
console.log(advisor instanceof TeamMember); // true
// advisor의 프로토타입은 TeamMember이고, TeamMember를 인스턴스화 한 객체이다.

advisor.greeting(); // I 'm Adam. Welcome to th team! \n Stay hungry. Stay foolish.

 

 

 

 

4. 화살표함수 지원 (=>)

 - 함수를 축약하기 위한 표현 : 본문이 표현식으로 되어 있고 명령 불록으로 되어있다.

 - 인자 => 함수본문 의 형태로 되어 있다.

// 1. 인자가 없을 때 빈 괄호 세트가 필요하다.
var countFruits = () => fruits.length;

// ES5
var countFruits = function () {
    return fruits.length;
}


// 2. 인자가 1개일 때는 생략이 가능하다.
var filteredFruit = fruits.filter(fruits => fruits.price > 100); // 과일 가격이 100 넘는 것만 골라서 객체를 만들어줌
console.log(filteredFruit); // [{name: "banana", price: 120}]

// ES5
fruits.filter(function (fruit) {
   return fruit.price > 100;
});


// 3. 함수가 객체 리터럴을 반환하는 경우 꼭 () 괄호로 감싸야 한다.
var inventory = fruits.map(fruit => ({name:fruit.name, storage: 1})); // 과일을 받아다가 {name:fruit.name, storage: 1} 형태로 변환한 리스트 객체를 반환
console.log(inventory); // [{name: "apple", storage: 1}, {name: "orange", storage: 1}, {name: "banana", storage: 1}]

// ES5
var inventory = fruits.map( function (fruit) {
   return {
       name: fruit.name,
       storage: 1
   }
});

// 4. 명령 구분으로 이뤄져 있고 결과를 반환해야할 때 {} 괄호가 필요하다.
var inventory = fruits.map(fruit => {
    console.log('checking ' + fruit.name + ' storage');
    return { name: fruit.name, storage : 1};
});

// ES5
var inventory = fruits.map(function(fruit) {
    console.log('checking ' + fruit.name + ' storage');
    return { name: fruit.name, storage : 1};
});

사용법을 정리하면 

1. 인자가 없을 때 빈 괄호 세트가 필요하다.

2. 인자가 1개일 때는 생략이 가능하다.

3. 함수가 객체 리터럴을 반환하는 경우 꼭 () 괄호로 감싸야 한다.

4. 명령 구분으로 이뤄져 있고 결과를 반환해야할 때 {} 괄호가 필요하다.

 

 

var sum = (a,b) => { a + b }; 
console.log(sum(1,2)); // undefined


var sum = (a,b) => a+b;
console.log(sum(1,2)) // 3

위의 sum의 경우 {} 괄호를 썼음에도 return이 없고 객체를 리턴하는 것도 아니기 때문에 화살표 함수가 아니다.

아래와 같이 사용하는 것이 맞다.

 

 

화살표 함수 특징 1. 자신의 this를 가지지 않음 (무조건 상위 스코프의 실행컨텍스트를 따라감)

// 1번 : 화살표함수 사용
var shoppingCart = {
    items: ['Apple', 'Orange'],
    inventory: { Apple:1, Orange: 0 },
    checkout() {
        this.items.forEach(item => {
            if (!this.inventory[item]) { // 콜백함수로 호출되어 전역컨텍스트를 참조함에도, this라는 키워드가 shoppingCart 객체를 참조
                console.log('Item ' + item + ' has sold out');
            }
        })
    }
}
shoppingCart.checkout(); // Item Orange has sold out

// 2번 : ES5
var shoppingCart = {
    items: ['Apple', 'Orange'],
    inventory: { Apple:1, Orange: 0 },
    checkout() {
        this.items.forEach(function(item) {
            console.log(this); // Window 확인. 콜백함수는 기본적으로 직접호출을 하기 때문에 실행 컨텍스트가 전역임 
            if (!this.inventory[item]) {
                console.log('Item ' + item + ' has sold out');
            }
        })
    }
}
shoppingCart.checkout(); // Uncaught TypeError: Cannot read property 'Apple' of undefined

콜백함수는 기본적으로 직접호출을 하기 때문에 실행 컨텍스트가 전역이다. 따라서 this.inventory의 inventory는 전역 컨텍스트에서 변수를 찾는다. 하지만 전역 컨텍스트엔 inventory 가 없기 때문에 에러가 난다.

 

var shoppingCart = {
    items: ['Apple', 'Orange'],
    inventory: { Apple:1, Orange: 0 },
    checkout() {
        var that = this; // 상위 에서 받은 실행 컨텍스트를 저장해두고 function 안에서 사용하기 위해, 자신의 함수 this를 잠시 저장해둠
        this.items.forEach(function(item) {
            console.log(that);
            if (!that.inventory[item]) {
                console.log('Item ' + item + ' has sold out');
            }
        })
    }
}
shoppingCart.checkout(); // Item Orange has sold out

이를 해결하기 위해 클로저를 사용한다.

클로저란, 스코프와 환경(실행컨텍스트)가 바뀌었을 때도 접근 가능하도록 해당 환경을 저장해두는 함수. 이 때 이 함수를 변수에 담아 둘 수 있다.

 

잠시 클로저에 대해 설명하자면, 클로저는 보통 함수 내부에서 선언 된 함수로 함수 내부의 환경을 기억하는 특징이 있다. 아래 코드를 보자.

function func() {
    var val = '이 변수는 메모리에 여전히 올라와 있습니다.';
    return function() { // 함수 내부의 함수를 반환하고 있고, 함수 내부에서 선언된 함수이기 때문에 함수 func 내부의 변수를 기억하고 있음.
        return val;
    };
}

var closure = func();
console.log(closure()); // '이 변수는 메모리에 여전히 올라와 있습니다.' => 메모리에서 사라지지 않고 여전히 남아있음. 이런 특징을 이용해서 변수에 담아두면, 내부 환경을 잊지 않고 기억해둘수 있음

'이 변수는 메모리에 여전히 올라와 있습니다.' => 메모리에서 사라지지 않고 여전히 남아있음. 이런 특징을 이용해서 변수에 담아두면, 내부 환경을 잊지 않고 기억해둘수 있음

여튼 다시 화살표 함수로 돌아와서!

화살표 함수의 실행 컨텍스트는 상위 스코프의 것과 동일하기 때문에 Function.prototype.apply(), Function.prototype.bind() 와 같은 메소드로 실행컨텍스트를 변경하려고 해도 할 수 없다.

 

var name = 'Unknown';
var greeting = () => {
    console.log('Hi I\'m ' + this.name);
};
greeting.call({name : 'Sunny'});
greeting.apply({name : 'Sunny'});
var newGreeting = greeting.bind({name : 'Sunny'}); // I'm Unknown
newGreeting();
// 아무리 실행 컨텍스트를 {name : 'Sunny'} 객체로 바꾸려고 해도, 전역컨텍스트의 name인 Unknown을 바라보게 됨

즉, 화살표 함수는 상위 스코프의 실행 컨텍스트를 이용하기 때문에 객체의 메소드를 정의할 때 사용하는 것은 적합하지 않다! 예제를 보자.

 

var shoppingCart = {
    items: ['Apple', 'Orange'],
    inventory : { Apple: 1, Orange: 0},
    checkout: () => {
        this.items.forEach(item => { // 이 부분에서 에러가 남! 자신인 shoppingCart 객체의 item을 참조해야하는데 전역컨텍스트를 바라보고 있기 때문임. 전역 컨텍스트엔 items 가 없다.
            if (!this.inventory[item]) {
                console.log('Item ' + item + ' has sold out');
            }
        })
    }
}
shoppingCart.checkout(); // Uncaught TypeError: Cannot read property 'forEach' of undefined

이 부분에서 에러가 남! 자신인 shoppingCart 객체의 item을 참조해야하는데 전역컨텍스트를 바라보고 있기 때문임. 전역 컨텍스트엔 items 가 없다.

심지어 프로토타입으로 메소드를 넘길 때, 화살표함수를 사용하면 작동조차 하지 않음

 

class User {
    constructor(name) {
        this.name = name;
    }
}
User.prototype.swim = () => {
    console.log(this.name + ' is swimming');
}
var user = new User('Ted');
console.log(user.swim());// is swimmin 출력 => this.name 을 참조하지 못함

전역변수의 this를 바라본다.

 

class User {
    constructor(name) {
        this.name = name;
    }
}
User.prototype.swim = () => {
    console.log(this.name + ' is swimming');
    return 1;
}
var user = new User('Ted');
console.log(user.swim());// Sunny is swimmin 출력  
var name = 'Sunny'; // 전역 변수의 Sunny

 

화살표 함수 특징 2. 프로토타입 객체를 가지지 않는다.

화살표 함수는 프로토타입 객체를 가지지 않기 때문에 생성자 함수(constructor)가 없고 따라서 new로 호출이 안된다. 함수 표현식으로 만들어도 되는 생성자함수 호출이, 화살표 함수는 안됨

var func = function(){};
var temp = new func();

var func = () => {};
var temp = new func(); // Uncaught TypeError: func is not a constructor

 

 

5. 매개변수 기본값 지정이 가능

ES5에선 구현이 복잡하고 가독성이 떨어지는 코드를 ES6에선 쉽게 작성이 가능함

const shoppingCart = [];
function addToCart(item, size=1) {
    shoppingCart.push({item:item, count:size});
}
addToCart('Apple');
addToCart('Orange', 2);
console.log(shoppingCart); // [{item: "Apple", count: 1}, {item: "Orange", count: 2}] => Apple 에 매개변수 size를 넣어주지 않았음에도 기본값 1이 들어가 있음

위 코드를 ES5 문법으로 작성하면

 

const fruitCart = [];
function AddToFruitCart(item, size) {
    size = (typeof size !== 'undefined') ? size : 1;
    fruitCart.push({item:item,count:size});
}
AddToFruitCart('Apple');
AddToFruitCart('Orange', 2);
console.log(fruitCart);

한 줄을 더 추가해야함

 

 

6. 나머지 매개변수 배열을 정의할 수 있다.

 

ES5 에서는 함수 본문 내 함수의 매개변수를 반복하기 위해 arguments 객체를 사용했다.

function workout(exercise1) {
    var todos = Array.prototype.slice.call(arguments, workout.length); // 나머지 매개 변수를 받아옴
    for (var i in todos) {
        console.log(todos[i]);
    }
}
workout('A','B');       // B
workout('A','B','C');   // B C

 

ES6 에선 3점 표기법으로 나머지 매개변수를 받을 수 있다. (slice 할 필요 없음)

function workout(exercise1, ...todos) {
    for (var i in todos) {
        console.log(todos[i]);
    }
}

workout('A','B');       // B
workout('A','B','C');   // B C

그리고 todos 로 받은 나머지 매개변수는 매개변수 exercise1 에 영향을 끼치지 않음

 

 

7. 전개구문을 사용할 수 있다.

 

ES6 문법에서 함수선언 내부에 3점 표기법(...)을 사용하면 그것이 자동으로 배열의 요소를 전개해서 대입시켜줌. 이 방법으로 배열의 각 요소를 함수에 전달할 수 있음

let urgentTasks = ['Buy three tickets'];
let normalTasks = ['Book a hotel', 'Rent a car'];
let allTasks = [...urgentTasks, ...normalTasks]; // ["Buy three tickets", "Book a hotel", "Rent a car"]
let otherTasks = [urgentTasks, normalTasks]; // [Array(1), Array(2)]

((first, second) => {
    console.log('Workout on ' + first + ' and ' + second);
})(...allTasks);
//화살표 함수 + 즉시실행함수 (괄호로 감쌈)

 

 

8. 비구조화 할당

ES6에서 지원하는 문법으로, 배열이나 객체의 속성을 해체해서 각각을 변수로 보고 값을 할당해주는 문법이다.

변수 선언, 변수 할당, 함수 매개변수 할당에 활용이 가능하다.

 

 

- 객체 비구조화

let user = {name:'Sunny', interests: ['Traveling', 'Swimming']};
let {name, interests, tasks} = user;
console.log(name); // Sunny
console.log(interests); // ['Traveling', 'Swimming']
console.log(tasks); // undefined

{name, interests, tasks} 객체를 쪼개어 내부를 구성하는 속성을 각각 변수로 보고 

값을 할당했다.

 

let user = {name:'Sunny', interests: ['Traveling', 'Swimming']};
let {name, interests : hobby, tasks = []} = user;
console.log(name); // Sunny
console.log(hobby); // ['Traveling', 'Swimming']
console.log(tasks); // undefined
console.log(interests); // 실행되지 않음

tasks 와 같이 기본 값을 주는 것도 가능하고, hobby와 같이 interests 속성의 변수 이름을 선택할 수 있다는 것이다.

user 객체의 interests 프로퍼티를 비구조화 할당을 통해 선택한 값을 이름이 hobby 변수에 할당한 것이다.

 

 

- 배열 비구조화

var [a, b] = ['Traveling', 'Swimming', 'Shopping'];

console.log(a); // Traveling
console.log(b); // Swimming


var [, , a, b, c = 'default'] = ['Traveling', 'Swimming', 'Shopping']; -- 1번

console.log(a); // Shopping
console.log(b); // undefined
console.log(c); // default => 기본값을 할당할 수 있다.

배열의 순서에 맞춰서 같은 인덱스에 해당하는 값을 할당 시킨다.

이때 값이 비어있으면 위 코드의 1번 줄과 같이 해당 자리는 건너 띄고 다음 변수를 할당 시킨다.

 

 

- 중첩 비구조화

let user = {name:'Sunny', interests: ['Traveling', 'Swimming']};
let {interests:[,b]} = user;

console.log(b); // Swimming
console.log(interests); // ReferenceError

비구조화를 중첩해서 진행한 것이다.

보면 알겠지만, interests 는 Error를 내는 것으로 보아, 해당 변수는 비구조화 할당을 통해서 생성&할당 되지 않았다.

사실상 우측에 [,b] 라는 객체를 이름으로 쓰겠다는 것을 선언 했기 때문이다.

 

const fruits = [{name:'Apple', price: 100}, {name:'Orange', price:80}];
let [,{name:secondFruitName}] = fruits;
console.log(secondFruitName); // Orange 

위는 중첩 비구조화의 또 다른 예시이다.

 

 

- 나머지 요소를 활용한 비구조화 

let [first, ...others] = ['Traveling', 'Swimming', 'Shopping'];
console.log(others); // ['Swimming', 'Shopping'] => 나머지 요소라는 문법을 통해서 배열의 비구조화를 진행했다.

배열의 두번 째 요소부터 복사되서 others 라는 변수에 할당 되었다.

그런데 복사를 하는 것은 맞는데 얕은복제(shallow clone) 일 뿐이다.

복사한 객체와 원본 객체는 본질적으로 같은 객체를 참조하고 있을 뿐이다.

따라서 복사한 객체 프로퍼티를 수정하면 원본의 배열 요소도 수정된다.

 

const fruits = [{name:'Apple', price: 100}, {name:'Orange', price:80}];
let [first, second] = fruits;
first.name = 'Apple pen'; // 배열 비구조화 할당을 통해 받은 변수를 수정했다.
console.log(fruits[0].name); // Apple pen => 원본에도 영향을 받은 모습

let [...myFruits] = fruits; 
console.log(myFruits[0].name); 
myFruits.push({name:'Banana', price: 90});
console.log(myFruits.length); // 3
console.log(fruits.length); // 2 => 추가한 아이템은 원본에 영향을 미치지 않는다는 사실을 알 수 있다.
myFruits[0].price = 110; 
console.log(fruits[0].price); // 110

위 코드를 보면 비구조화 할당을 통해 배열 객체를 복제했고, 복제한 객체를 통해서 이루어진 수정이 원본에 영향을 미치고 있다.

 

const fruits = {a:{name:'Apple', price: 100}, b: {name:'Orange', price:80}};

let {a,b} = fruits;

a.count = 2;

console.log(a); // {name: "Apple", price: 100, count: 2}
console.log(fruits); // {a: {name: "Apple", price: 100, count: 2}, b: {name: "Orange", price: 80}}

배열이 아니어도 얕은 복제가 일어난다는 사실을 알 수 있다.

 

 

- 함수 매개변수 비구조화

 

function workout({gym}, times) {
	console.log('Workout in ' + gym + ' for ' + times + ' times');
}
let thisWeek = {gym: 'Gym A'};
workout(thisWeek, 7); // Workout in Gym A for 7 times

매개 변수에서도 비구조화 할당을 적용할 수 있다.

주의 할 점은 일반 함수의 경우 매개변수의 값이 null 이나, undefined 가 되어도 문제가 없지만 비구조화 할당을 적용한 변수는 해당 값이 들어가면 TypeError를 내뱉는다.

 

function noValue(first, second, third) { 
	console.log(first); // first
	console.log(second); // null
	console.log(third); // undefined
};
noValue('first', null);

위 코드를 보면 null 이나, undefined이 출력 될 뿐 함수는 정상 실행이 되었음을 알 수 있다.

 

function workout({gym, todos}) {
	let [first] = todos;
	console.log('Start ' + first + ' in ' + gym); 
}
let today = {gym: 'Gym A', todos: ['Treadmill']};
workout(today); // Start Treadmill in Gym A
workout({gym: 'Gym B'}); // TypeError

기본값을 할당함으로써 Type 에러를 예방할 수 있다.

다만 위 코드에서 workout({gym: 'Gym B', todos = ['Treadmill'] }); // TypeError  과 같이 전달할 때 기본 값을 할당하는 것은 동작하지 않는다.

아래는 Type 에러를 예방한 코드이다.

function workout({gym, todos=['Treadmill']}) {
	let [first] = todos;
	console.log('Start ' + first + ' in ' + gym); 
}
let today = {gym: 'Gym A', todos: ['Treadmill']};
workout(today); // Start Treadmill in Gym A
workout({gym: 'Gym B'}); // Start Treadmill in Gym B
workout(); // TypeError 

아래와 같이 매개변수 전체에 대해 값을 할당 시켜놓아도 좋다.

function workout({gym = 'no gym', todos=['Treadmill']} = {gym : '', todos : []}) {
	let [first] = todos;
	console.log('Start ' + first + ' in ' + gym); 
}
workout(); // Start undefined in 

 

 

 

9. 템플릿 리터럴

 

문자열을 템플릿화 해두고 내부의 특정 값만 변경해서 사용하는 문법. 따옴표나, 쌍따옴표 대신, 역따옴표를 사용해서 묶는다.

let user = {
  name:  'Ted',
  greeting() {
    console.log(`Hello, I'm ${this.name}.`);
  }
};

user.greeting(); // Hello, I'm Ted.

이 때 주의 할 점은 역 따옴표 문자 내부에 있는 모든 공백도 출력이 된다는 점이다.

 

 

10. 모듈

  1. 컴파일 시점에서 가져오기(import)와 내보내기(export)를 수행한다.
  2. 가져오기와 내보내기에 대한 선언이 최상위에 위치해야하며, if나 try/catch 같은 블록 내부에 넣을 수 없다.
  3. 모듈을 생성하기 위해서 할일은 단지 js 파일을 생성하는 것 뿐이며, 바벨이나 웹팩 같은 도구를 이용해서 컴파일 또는 코드를 묶어서 사용할 수 있다. 또는 <script type="module">으로 모듈 파일을 브라우저로 불러오는 방법이 있다.

 

- 내보내기

명명된 내보내기(named export)와 기본 내보내기(default export) 가 존재한다.

아래 각각의 파일은 모듈이다.

 

user.js

export default class User {
	constructor(name, role) {
		this.name = name;
		this.role = role;
	}
}

객체, 함수, 변수를 내보낼 수 있다. 위는 기본 내보내기이다. 한 파일에서는 오직 하나의 내보내기만이 가능하다.

 

role.js

const DEFAULT_ROLE = 'User';
const ADMIN = 'Admin';

export {DEFAULT_ROLE as USER, ADMIN}

위와 같이 명명된 내보내기가 가능하다. 명명된 내보내기를 할 때는 위와 같이 괄호로 감싼다. 여러개를 한번에 내보내는 것이 가능하다.

 

task.js

console.log("inside tasks module");
export default function completeTask(user) {
	console.log(`${user.name} completed a task`);
	completedCount++;
}
//완료된 작업 개수를 추적
export let completedCount = 0;
// 기본 내보내기는 모듈당 하나의 default export 만 가능합니다. 따라서 불러올 때는 아무 이름으로 불러도 상관없습니다.

 

위에서 만든 모듈을 가져와보자.

 

app.js 이다.

import User from './user.js';
import * as Roles from './roles.js'; // named export는 * as NAME 또는 {} 를 통해서 불러올 수 있다.
import completeTask, {completedCount} from './tasks.js'; // 일반으로 내보낸 모듈과, 명명 내보내기의 차이점


let user = new User('Ted', Roles.USER);
completeTask(user);
console.log(`Total completed ${completedCount}`);
// completedCount++; => 가져온 변수는 상수가 되어서 재할당이 불가능해지므로 에러가 난다.

//가져온 객체를 변경을 할 수 있긴 하나 좋은 예제가 아니다. 가져온 모듈에 해당하는 값을 재할당 할 수는 없으나, 프로퍼티를 변경할 수는 있다.
User.prototype.walk = function () {
	console.log(`${this.name} walks`);
};
user.walk();

console.log(this);
// https://ko.javascript.info/import-export
// 명명 규칙과 관련해서 해당 내용을 꼭 읽어보기

가져오기는 호이스팅 되기 때문에 import가 맨 아래에 선언되어도 작동 되나 가독성 때문에 권장되는 방법은 아니다.

 

index.html

<!-- 모듈은 특수한 키워드나 기능과 함께 사용되므로 <script type="module"> 같은 속성을 설정해 해당 스크립트가 모듈이란 걸 브라우저가 알 수 있게 해줘야 합니다. -->
<!-- 브라우저 환경에서도 <script type="module">을 사용해 모듈을 만들면 독립적인 스코프가 만들어집니다. -->
<script type="module" src="/resources/js/app.js"></script><!-- 작게 나뉜 js 파일을 모듈이라고 부름 -->
<script>console.log('A embedded script');</script>

 

출력결과

A embedded script
inside tasks module
Ted completed a task
Total completed 1
Ted walks
undefined

위에서 볼 수 있듯, 모듈파일 로드를 멈추고, 스크립트를 먼저 실행한다. 그리고, 모듈 파일을 로드한다.

다만, 스크립트는 엄격모드를 적용하기 위해 'use strict'를 넣어야 하는 반면, 모듈은 자동으로 엄격 모드가 된다.

 

 

11. 프로미스

자바스크립트는 비동기 프로그래밍을 위해서, 콜백을 사용한다. 콜백함수로 비동기 요청이 처리 된 뒤에 실행할 함수를 전달 받는다. 이 때, 콜백함수를 사용하게 되면 필연적으로 코드가 계단형태가 된다. 예를 들면

ajax 요청 후, 성공시에 다른 ajax 요청을 또 하는 상황이다.

var title = ''; // title 변수
$.ajax({
    type: 'POST',
    url: '/board/title',
    dataType: 'json',
    data: {},
    error:	function(res)	{
        alert('error');
    },
    success: function(res) {
    	if(res.status == 200) {
    		title = res.title; // title을 받아온다.
    		
    		$.ajax({
    		    type: 'POST',
    		    url: '/board/content',
    		    dataType: 'json',
    		    data: {title : title}, // title에 해당하는 데이터를 보낸다. {title : ''} 와 같은 데이터가 보내질 것이다.
    		    error:	function(res)	{
    		        alert('error');
    		    },
    		    success: function(res) {
    		    	if(res.status == 200) {
    		    		console.log(res.content);
    		    	} else {
    		    		alert("상태코드 : " + res.status);
    		    	}
    		    }
    		});
    	} else {
    		alert("상태코드 : " + res.status);
    	}
    }
});

비동기란 여러 작업을 한번에 시켜줄 수 있도록 돕는 작업인데, 이때 해야할 작업을 콜백으로 전달한다.

다만, 콜백을 통해서 작업하면 위 코드와 같이 코드가 중첩 되면서 지저분해진다.

이를 해결할 수 있는 것이 프로미스이다.

 

아래 코드를 하나 더 보자, 아래 코드는 콜백함수를 통해서 비동기 호출을 구성했다.

function getProjects(callback) {
	setTimeout(() => {
		callback([{id:1,name: 'Project A'}, {id:2, name:'Project B'}]);
	}, 100);
}

function getTasks(projects, callback) {
	console.log("받은 프로젝트가 있나요?");
	console.log(projects);
	setTimeout(() => {
		// 여기서 받은 콜백함수의 형태는 
		/*
		function (tasks) {
			render({projects, tasks});
		}
		이다. 즉, 여기서 첫번째 매개변수로 받은 projects와 tasks를 비구조화 할당으로 변경해서 render를 부르는 함수를 전달해준 것이다.
		*/
		callback([{id:1, projectId:1, title: 'Tasks A'}, {id:2, projectId:2, title: 'Tasks B'}]);
	},100);
}

function render({projects, tasks}) {
	console.log("받은 projects");
	console.log(projects);
	console.log("받은 tasks");
	console.log(tasks);
	console.log(`Render ${projects.length} projects and ${tasks.length} tasks`);
}

getProjects((projects) => {// getProjects의 콜백 함수로 getTasks를 호출하는 메소드 전달
	
	// project : [{id:1,name: 'Project A'}, {id:2, name:'Project B'}]
	getTasks(projects, (tasks) => { // 받은 매개변수와 function을 함께 전달 
		// 받은 projects와 tasks를 비구조화 할당으로 render에 보내고 있음
		render({projects, tasks});
	});
});

코드가 충첩 되어서 보기도 불편하고, 이해도 상당히 어렵다.

이런 콜백 함수 대신에 프로미스를 사용해서 변경해보자.

 

function getProjects() {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve([{id:1,name: 'Project A'}, {id:2, name:'Project B'}]);
		}, 100);
	});
}

/*
getTasks(projects, (tasks) => { // 받은 매개변수와 function을 함께 전달 
	// 받은 projects와 tasks를 비구조화 할당으로 render에 보내고 있음
	render({projects, tasks});
});
=> 받은 콜백함수에 projects 를 추가해서 render를 부르고 있다. => 원하는 형태로 콜백함수를 부르고 있다.
 */
function getTasks(projects) {
	// 이 함수를 주목하고 싶다.
	// 콜백함수가 실행 되는 형태를 외부에서 정할 수 없고 오직 then이라는 메소드를 통해서 호출하기 때문에
	// resolve를 부르는 부분을 직접 수정했다.
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve({
				projects,
				tasks: [{id:1, projectId:1, title: 'Tasks A'}, {id:2, projectId:2, title: 'Tasks B'}]});
		}, 100);
	});
}

function render({projects, tasks}) {
	console.log(`Render ${projects.length} projects and ${tasks.length} tasks`);
}

// 비동기 연산을 래핑한 Promise 객체를 바로 반환
// Promise 객체가 내부의 비동기 함수를 적절하게 처리해주고 그 다음 전달받은 콜백함수(실행함수)를 then 메소드를 통해서 실행해준다.
// 이 때 then의 첫번째 매개변수로는 resolve 에 해당하는 함수를, 두번째 매개변수로는 실패시 실행이 될 reject 함수를 전달하면 됩니다.
getProjects()
	.then(getTasks)
	.then(render)
	.then(undefined, function(error){console.log('Handling error', error)})
	.catch((error) => {
		console.log('Handling error', error);
	}) // 위에 then 대신 사용할 수 있는 구문이다.