한방에 끝내는 ES6 기초

0

뷰, 리액트, 앵귤러등의 자바스크립트 프레임워크는 최신 자바스크립트 문법을 사용하기 때문에 이런 최신 자바스크립트 프레임워크를 학습하기 위해서는 최신 자바스크립트 문법에 대해 알고 있는 것이 좋습니다.

자바스크립트는 1995년에 탄생한 언어인데, 원래는 웹 페이지에 상호작용을 손쉽게 추가하기 위해 만들어졌습니다. 하지만 이후에 DHTML, AJAX등의 기술과 함께 더욱 활발히 사용되는 언어로 발전하게 되었고, 이 후에는 Node.js라는 자바스크립트 플랫폼이 등장하면서 풀스택 애플리케이션을 개발할 수 있는 언어가 되었습니다.

이런 자바스크립트의 변화를 주도하는 기관은 ECMA라는 곳인데, 자바스크립트에 대한 명세가 2015년 6월에 크게 개정되면서 ECMAScript 6 또는 ES6, ES2015등의 이름으로 불리게 됩니다. 그리고 ES6 이후의 개정판부터는 ES2016, ES2017 처럼 버전 대신 연도를 부여하면서 새로운 명세가 계속해서 추가되고 있습니다.

자바스크립트는 일반적으로 객체지향 보다는 함수형 프로그래밍에 적합한 언어로 알려져 있는데, 실제로 ES6에 포함된 대부분의 기능은 함수형 프로그래밍 기법에 최적화되어 있습니다.

함수형 프로그래밍은 여러 함수들을 서로 합성하고 조합하여 애플리케이션을 구축하는 방법으로, ES6가 함수형 프로그래밍 기법에 최적화되어 있는 만큼 함수형 프로그래밍 기법에 익숙해지는 것이 최신 자바스크립트를 제대로 활용하는 방법이라고 할 수 있습니다.

변수 선언

ES6 이전의 자바스크립트에서는 var라는 키워드를 사용하여 변수를 선언했는데, ES6 부터는 몇 가지 더 나은 방법이 추가되었습니다.

const

const는 값을 변경할 수 없는 상수로 선언하는 키워드입니다. 만약 데이터가 const로 설정되었는데, 프로그램에서 값을 변경하려고 시도하는 경우에는 오류가 발생하게 됩니다.

const es6 = 'javascript';
es6 = 'ECMAScript 6';
Uncaught TypeError: Assignment to constant variable.

let

ES6 부터는 렉시컬 스코프, 즉 구문적인 변수 영역 규칙이란 것을 지원하는데, 바로 이 ‘구문적인 변수 영역 규칙’이란 것이 기존에 사용하던 var 키워드와의 가장 큰 차이점이라고 할 수 있습니다.

자바스크립트 문법에서는 중괄호{}로 코드 블록을 만드는데, 다른 프로그래밍 언어와는 달리 if~else 영역이 별도의 코드 블록으로 구분되지 않습니다. 그래서 예전의 자바스크립트에서는 다음 처럼 글로벌 변수를 엉뚱한 값으로 덮어씌우는 상황도 만들어질 가능성이 존재했습니다.

var es6 = 'javascript';
if( es6 ) {
    var es6 = 'ECMASCript 2015';
    console.log('블록 변수: ', es6);
}
console.log('글로벌 변수: ', es6);
블록 변수:  ECMASCript 2015
글로벌 변수:  ECMASCript 2015

하지만 ES6 부터는 let 키워드를 사용하면 변수의 영역을 코드 블록 안으로 한정시킬 수 있어 글로벌 변수를 보호할 수 있습니다.

var es6 = 'javascript';
if( es6 ) {
    let es6 = 'ECMASCript 2015';
    console.log('블록 변수: ', es6);
}
console.log('글로벌 변수: ', es6);
블록 변수:  ECMASCript 2015
글로벌 변수:  javascript

앞의 코드와 같이 let 키워드를 사용하면 똑같은 이름의 변수명이 글로벌 변수에 존재해도 해당 변수에는 영향을 주지 않습니다. 다음의 예제로 각각의 키워드를 테스트해보면 var 키워드와 let 키워드로 선언된 경우의 차이를 확실히 느낄 수 있습니다.

var 키워드를 사용한 경우
var button, container = document.getElementById('container');

for(var i=0; i<5; i++) {
    button = document.createElement('button');
    button.innerText = '버튼' + (i + 1);
    button.addEventListener('click', function(){
        alert('이 버튼은 #' + (i + 1) + '번째 버튼입니다.');
    });
    container.appendChild(button);
}

예제를 테스트해보면 루프 카운터의 변수인 ivar 키워드를 사용하면 글로벌 영역에 i가 생성되면서 루프 카운터의 i의 값이 덮어씌워져 버리기 때문에 버튼 1부터 버튼 5까지 어느 버튼을 클릭하더라도 5번째 버튼이라는 메세지만 출력되는 것을 확인할 수 있습니다.

그렇다면 변수 ivar 대신 let 키워드를 사용하면 어떻게 될까요? 다음 처럼 동일한 코드에 var 키워드를 let 키워드로만 바꿔서 테스트해보면, 각각의 버튼을 클릭했을 때 버튼의 인덱스가 정상적으로 출력되는 것을 확인할 수 있습니다.

let 키워드를 사용한 경우
var button, container = document.getElementById('container');

for(let i=0; i<5; i++) {
    button = document.createElement('button');
    button.innerText = '버튼' + (i + 1);
    button.addEventListener('click', function(){
        alert('이 버튼은 #' + (i + 1) + '번째 버튼입니다.');
    });
    container.appendChild(button);
}

루프 카운터의 변수 ilet 키워드로 선언하면 i의 영역이 for의 중괄호{}블록으로 제한되기 때문에, 각각의 버튼을 클릭했을 때 루프를 생성하면서 사용하는 i의 값이 정상적으로 표시되는 것이죠.

템플릿 문자열

템플릿 문자열은 백틱`을 사용하는 문자열로 더하기+보다 간단하고 편리하게 변수를 포함한 문자열 연결을 할 수 있습니다.

const first = '홍',
      middle = '길',
      last = '동'
console.log( first + ' ' + middle + ' ' + last );
console.log( `${first} ${middle} ${last}` ); // 템플릿 문자열

백틱으로 감싼 템플릿 문자열에서는 ${}의 내부에 값을 만들어내는 모든 형태의 자바스크립트 식과 변수가 들어갈 수 있습니다. 그리고 템플릿 문자열은 모든 공백을 유지하기 때문에 이메일 템플릿이나 코드 템플릿공백이 유지되어야 하는 문자열을 표시하는 경우에도 유용하게 사용될 수 있습니다.

템플릿 문자열을 이용한 이메일 템플릿의 예
const thankyou = `
안녕하세요. ${name}님.
${event} 티켓을 구매해주셔서 감사합니다.

[주문 정보]
${qty}매 X ${price}
total: ${qty * price}
${event} 공연

감사합니다.
`

const template = `
<div class="template">
    <h3>${template.title}</h3>
    <div class="body">
        ${template.body}
    </div>
</div>
`

디폴트 파라미터

C++이나 파이썬과 같은 언어에서는 함수의 인자로 디폴트 값을 선언할 수 있는데, 기존의 자바스크립트에서는 함수의 인자에 디폴트 값을 선언 할 수 없었습니다. 하지만 최신 자바스크립트의 ES6 명세에서는 디폴트 파라미터가 추가되어 함수의 인자에서 직접 디폴트 값을 설정할 수 있게 되었습니다.

const person = {
    name: '홍길동',
    favorite: '축구'
}
function likeActivity(a=person) {
    console.log( `${a.name}은 ${a.favorite}을 좋아합니다.` );
}
likeActivity();
홍길동은 축구을 좋아합니다.

화살표 함수

화살표 함수를 사용하면 function 키워드 없이도 함수를 만들 수 있고, return 키워드를 사용하지 않아도 식을 계산한 값이 자동으로 반환되기 때문에 간편하게 사용할 수 있습니다.

기존의 자바스크립트에서는 익명함수를 많이 사용했는데, ES6에서는 익명함수를 화살표 함수로 더 간단하게 표현할 수 있습니다.

기존 방식의 익명 함수
var citizen = function(name) {
    return `나는 대한민국의 시민 ${name}입니다.`;
}
console.log( citizen('홍길동') );
ES6의 화살표 함수
var citizen = name => `나는 대한민국의 시민 ${name}입니다.`;
console.log( citizen('홍길동') );

2개 이상의 파라미터와 멀티 라인 처리

화살표 함수를 사용하면 2개 이상의 파라미터를 사용하는 함수도 간단하게 표현할 수 있는데, 2개 이상의 파라미터는 괄호()로 감싸주면 됩니다.

기존 방식
var citizen = function(country, name) {
    return `나는 ${country}의 시민 ${name}입니다.`;
}
console.log( citizen('대한민국', '홍길동') );
화살표 함수 방식
var citizen = (country, name) => `나는 ${country}의 시민 ${name}입니다.`;
console.log( citizen('대한민국', '홍길동') );

그런데 일반적으로 함수는 앞의 예제와 같이 간단한 구조로 만드는 경우는 많지 않은데, 명령 처리를 위해 여러 줄로 코드를 작성하는 경우가 대부분으로, 이런 경우에도 그냥 기존의 방식과 같이 중괄호{}로 코드를 감싸주면 됩니다.

기존 방식
var citizen = function(country, name) {
    if( !country ) {
        throw new Error('국가명을 입력해주세요.');
    }
    if( !name ) {
        throw new Error('이름을 입력해주세요.');
    }
    return `나는 ${country}의 시민 ${name}입니다.`;
}
console.log( citizen('대한민국', '홍길동') );
화살표 함수 방식
var citizen = (country, name) => {
    if( !country ) {
        throw new Error('국가명을 입력해주세요.');
    }
    if( !name ) {
        throw new Error('이름을 입력해주세요.');
    }
    return `나는 ${country}의 시민 ${name}입니다.`;
}
console.log( citizen('대한민국', '홍길동') );

그런데 화살표 함수는 이렇게 표현식을 간단하게 만들어 사용할 수 있지만, 기존 함수와는 달리 새로운 this 영역을 만들지 않기 때문에 주의해야 합니다.

var sports = {
    games: ['야구', '농구', '축구', '배구', '태권도'],
    print: function(delay=1000) {
        setTimeout(function() {
            console.log(this.games.join(', '));
        }, delay);
    }
}
sports.print();
Uncaught TypeError: Cannot read property 'join' of undefined

위 코드의 경우에는 this.gamesjoin 메서드 호출을 시도하다가 에러가 발생되는데, 그 이유는 함수에서 호출되는 thiswindow 객체이기 때문입니다. 즉 gamesundefined로 출력되기 때문이죠.

그런데 이 코드를 화살표 함수로 바꿔보면 에러가 나지 않고 함수가 정상적으로 작동하는 것을 확인할 수 있습니다. 화살표 함수에서 호출되는 thissport이기 때문입니다. 하지만 print에서 화살표 함수를 사용한다면 thiswindow 객체인 것을 확인할 수 있습니다.

화살표 함수 방식
var sport = {
    games: ['야구', '농구', '축구', '배구', '태권도'],
    print: function(delay=1000) {
        setTimeout(() => {
            console.log(this.games.join(', '));
        }, delay);
    }
}
sport.print(); // 야구, 농구, 축구, 배구, 태권도
print에서 화살표 함수를 사용하는 경우
var sport = {
    games: ['야구', '농구', '축구', '배구', '태권도'],
    print: (delay=1000) => {
        setTimeout(() => {
            console.log( this === window );
        }, delay);
    }
}
sport.print(); // true

화살표 함수를 사용한다면, 다음의 두 가지만 기억하면 큰 어려움없이 사용할 수 있습니다.

  • 화살표 함수를 사용할 때는 영역에 유의해야 한다.
  • 화살표 함수는 새로운 this 영역을 만들지 않는다.

객체와 배열

ES6는 구조 분해, 객체 리터럴 개선, 스프레드 연산자 등 객체와 배열을 다루거나, 객체와 배열 안에서 변수의 영역을 제한하는 다양한 방법을 제공합니다.

구조 분해

구조 분해는 객체 안에 있는 필드 값을 원하는 변수에 대입할 수 있게 해주는 기능인데, 이 기능을 사용하면 객체에 있는 필드를 같은 이름의 변수에 쉽게 넣어줄 수 있습니다.

다음은 ES6의 구조 분해 기능으로 subway 객체의 breadsource 필드를 같은 이름의 변수에 넣어주는 예제로, 구조 분해를 사용하면 두 변수의 값은 subway에 있는 같은 이름의 필드 값으로 초기화되는데, 나중에 두 변수를 변경해도 subway에 있는 원래의 필드 값이 변경되지는 않습니다.

var subway = {
    bread: '위트',
    topping: '아보카도',
    source: '랜치 드레싱',
    vegetable: ['양상추', '토마토', '오이']
}
var {bread, source} = subway;
console.log( bread, source );
위트 랜치 드레싱
구조 분해 후 변수 값 변경
var subway = {
    bread: '위트',
    topping: '아보카도',
    source: '랜치 드레싱',
    vegetable: ['양상추', '토마토', '오이']
};
var {bread, source} = subway;
bread = '화이트';
source = '허니 머스타드';
console.log( bread, source ); // 화이트 허니 머스타드
console.log( subway.bread, subway.source ); // 위트 랜치 드레싱

구조 분해한 변수는 함수의 인자로 넘길 수도 있는데, 다음과 같이 구조 분해를 사용하면 함수를 더 간단하게 만들 수 있습니다.

구조 분해를 사용하지 않은 경우
var fullName = {
    firstName: '길동',
    lastName: '홍'
}
var citizen = fullName => console.log(`나는 대한민국의 ${fullName.firstName}입니다.`);
citizen(fullName); // 나는 대한민국의 길동입니다.
구조 분해를 사용한 경우
var fullName = {
    firstName: '길동',
    lastName: '홍'
}
var citizen = ({firstName}) => console.log(`나는 대한민국의 ${firstName}입니다.`);
citizen(fullName); // 나는 대한민국의 길동입니다.

구조 분해는 기존의 코드 보다 더 선언적이기 때문에 코드를 작성한 사람의 의도를 더 잘 설명해줄 수 있는 것이 장점입니다.

앞의 코드를 예로 들면, 구조 분해를 사용하여 fullName 객체의 firstName만을 가져옴으로써 객체의 필드 중에서 firstName만을 사용한다는 사실을 직관적으로 알 수 있습니다.

구조 분해는 객체 뿐만 아니라 배열에도 사용할 수 있는데, 다음과 같이 배열에는 대괄호[]를 사용하여 첫 번째 원소를 변수에 대입할 수 있고, 만약 다른 위치에 있는 원소를 대입하려면 콤마,를 사용하는 리스트 매칭법을 사용하면 됩니다.

첫 번째 원소 대입
var [country] = ['대한민국', '미국', '영국', '이탈리아'];
console.log(country);
리스트 매칭 대입
var [country, , , country2] = ['대한민국', '미국', '영국', '이탈리아'];
console.log(country, country2);

객체 리터럴 개선

객체 리터럴 개선은 구조 분해와 반대되는 기능인데, 구조를 다시 만들거나 여러 내용을 하나의 객체로 만들어 줍니다.

객체 리터럴 개선을 사용하면 현재 영역에 있는 변수를 객체의 필드로 묶을 수 있는데, 다음과 같이 countries라는 객체에 country1country2라는 필드를 넣어주거나 객체 내에 메서드를 만들 수도 있습니다.

var country1 = '대한민국',
    country2 = '이탈리아';
var countries = {country1, country2};
console.log(countries);
{country1: "대한민국", country2: "이탈리아"}

다음과 같이 객체 내에 메서드를 생성하는 경우, match 메서드를 정의할 때 변수에 this 키워드를 사용했다는 사실에 유의할 필요가 있습니다.

객체 내에 메서드 생성
var country1 = '대한민국',
    country2 = '이탈리아',
    match = function() {
        console.log(`${this.country1}과 ${this.country2}는 2002년 월드컵 16강전에서 만났습니다.`);
    };
var countries = {country1, country2, match};
console.log(countries.match); // ƒ () { console.log(`${this.country1}과 ${this.country2}는 2002년 월드컵 16강전에서 만났습니다.`); }
객체 메서드 선언 방법

객체의 내부에 메서드를 선언하는 경우 기존에는 function 키워드를 사용했지만, ES6 문법에서는 굳이 function 키워드를 사용할 필요가 없습니다.

객체 리터럴 개선을 사용하면 코드가 매우 간단해지고 입력해야 할 코드의 양도 줄어들기 때문에 잘 익혀두면 편리하게 사용할 수 있습니다.

기존 방식으로 객체 메서드 선언하기
var car = {
    name: name,
    sound: sound,
    accelerator: function() {
        var accel = this.sound.toUpperCase();
        console.log(`${accel} ${accel} ${accel}`);
    },
    speed: function(mph) {
        this.speed = mph;
        console.log('속도(mph): ', mph);
    }
}
ES6 방식으로 객체 메서드 선언하기
var car = {
    name,
    sound,
    accelerator() {
        var accel = this.sound.toUpperCase();
        console.log(`${accel} ${accel} ${accel}`);
    },
    speed(mph) {
        this.speed = mph;
        console.log('속도(mph): ', mph);
    }
}

스프레드 연산자

스프레드 연산자는 세계의 점으로 표현하는데, 몇 가지의 다른 기능을 제공해주는 연산자입니다.

배열과 스프레드 연산자

우선 스프레드 연산자를 사용하여 배열의 내용을 조합할 수 있는데, 두 개의 배열이 있을 경우 스프레드 연산자를 사용하여 두 배열의 모든 원소가 포함된 세 번째 배열을 만들 수 있습니다.

배열 합치기
var arr1 = ['콜라', '사이다', '환타'];
var arr2 = ['맥주', '소주', '양주'];
var arr3 = [...arr1, ...arr2];
console.log(arr3.join(', ')); // 콜라, 사이다, 환타, 맥주, 소주, 양주

앞의 코드와 같이 스프레드 연산자를 사용하면 배열에 포함된 모든 원소를 간단하게 합칠 수 있는데, 이때 스프레드 연산자는 원본 배열을 수정하지 않고 복사본을 만들기 때문에 배열의 요소들을 조작해야 하는 경우에도 유용하게 활용할 수 있습니다.

원본 배열이 수정되는 방식
var arr1 = ['콜라', '사이다', '환타'];
var [last] = arr1.reverse();
console.log(last); // 환타
console.log(arr1.join(', ')); // 환타, 사이다, 콜라

reverse 메서드는 원본 배열을 변경하는 메서드이기 때문에 해당 원본 배열을 재사용해야 하는 경우라면 문제가 발생합니다. 반면 스프레드 연산자를 사용하면 원본 배열이 아닌 복사한 배열에 연산이 적용되기 때문에 원본 배열을 보호할 수 있습니다.

스프레드 연산자로 복사본을 만드는 방식
var arr1 = ['콜라', '사이다', '환타'];
var [last] = [...arr1].reverse();
console.log(last); // 환타
console.log(arr1.join(', ')); // 콜라, 사이다, 환타

또 배열의 구조 분해와 스프레드 연산자를 사용하면 배열의 나머지 원소들도 쉽게 얻을 수 있습니다.

구조분해와 스프레드 연산자로 배열의 나머지 요소 가져오기
var arr1 = ['콜라', '사이다', '환타'];
var [first, ...rest] = arr1;
console.log(first); // 콜라
console.log(rest.join(', ')); // 사이다, 환타

스프레드 연산자와 배열의 구조 분해를 활용하면 다소 복잡한 로직도 간단하게 만들 수 있는데, 다음과 같이 열차의 시간표를 표시해주는 함수도 스프레드 연산자를 사용하여 배열을 인자로 받고 구조 분해 기능으로 각각의 변수를 저장한 뒤, 각 변수와 배열의 정보를 표시해주는 방법으로 간단히 구현할 수 있습니다.

function trainTimeTable(...args) {
    var [start, ...remaining] = args;
    var [finish, ...stops] = remaining.reverse();
    console.log(`이 열차는 ${args.length}개의 도시를 운행합니다.`);
    console.log(`이 열차는 ${start}애서 출발하는 열차입니다.`);
    console.log(`이 열차의 목적지는 ${finish}입니다.`);
    console.log(`이 열차는 ${stops.length}개의 도시를 통과합니다.`);
}

trainTimeTable(
    '서울',
    '수원',
    '천안',
    '대전',
    '대구',
    '부산'
);
이 열차는 6개의 도시를 운행합니다.
이 열차는 서울애서 출발하는 열차입니다.
이 열차의 목적지는 부산입니다.
이 열차는 4개의 도시를 통과합니다.
객체와 스프레드 연산자

스프레드 연산자는 객체에도 사용할 수 있는데 객체에 사용하는 방법도 스프레드 연산자를 배열에 사용하는 방법과 비슷합니다.

var meal = {
    breakfast: '씨리얼',
    lunch: '햄버거'
}

var dinner = '스테이크';

var meals = {
    ...meal,
    dinner
}

console.log(meals);
{breakfast: "씨리얼", lunch: "햄버거", dinner: "스테이크"}

프라미스

프라미스는 비동기적인 이벤트를 다루는 방법인데, 일반적으로 프로그램에서 비동기 요청을 보내면 ‘원하는 방식 대로’ 완벽하게 동작할 수도 있지만, ‘알 수 없는’ 오류를 만나게 될 수도 있습니다.

프로그램을 작성해 보면, 다양한 유형으로 성공하거나 실패할 수도 있고, 생각지도 못한 여러 유형의 오류가 발생될 수도 있습니다. 즉 경우의 수가 매우 다양한데, 이런 경우 프라미스를 구현하면 여러가지 다양한 성공이나 실패의 경우를 보다 단순한 성공이나 실패로 환원시킬 수 있습니다.

프라미스 구현하기

randomuser.me라는 웹사이트가 있는데, 이 웹사이트에서 배포하는 api를 활용하면 가상 멤버의 이름, 전화번호, e-mail등의 더미 정보로 웹사이트 등의 테스트 계정을 쉽게 만들 수 있습니다.

다음의 코드는 randomuser.me라는 웹사이트에서 제공해주는 api로 만든 비동기 프라미스 예제입니다. 코드는 새로운 프라미스를 반환하고, 반환된 프라미스가 randomuser.me api에 요청을 보내는 방식으로 구현되었습니다.

randomuser.me api를 활용한 비동기 프라미스
const getRandomUser = count => new Promise((resolves, rejects) => {
    const api = `https://randomuser.me/api/?nat=us?results=${count}`;
    const request = new XMLHttpRequest();
    request.open('GET', api);
    request.onload = () =>
        (request.status === 200) ?
            resolves(JSON.parse(request.response).results) :
            reject(Error(request.statusText));
    request.onerror = (err) => rejects(err);
    request.send();
});

randomuser.me api에서 가져오고 싶은 멤버수를 getRandomUser에 전달하여 호출하면 프라미스를 사용할 수 있는데, 프라미스가 성공한 경우 실행해야 할 작업then함수를 이용하여 프라미스의 뒤에 체이닝시켜주면 되고, 오류를 처리하기 위한 콜백도 함께 제공해 줄 수 있습니다.

getRandomUser(10).then(
    members => console.log(members),
    err => console.error(new Error('멤버 정보를 가져올 수 없습니다.'))
);
[
    {
        gender: "female",
        name: {…},
        location: {…},
        email: "charlotte.hughes@example.com",
        login: {…}, …
    }
]

자바스크립트 프로젝트에서는 비동기로 데이터를 처리하는 경우가 많습니다. 프라미스는 이런 비동기 요청을 더 쉽게 처리할 수 있게 해주는데, 특히 최근에는 Node.js를 많이 활용하면서 프라미스 기능을 많이 사용하고 있습니다. 만약 Node.js와 함께 자바스크립트 프레임워크를 사용해야 하는 상황이라면 프라미스의 사용법을 정확하게 이해하고 있는 것이 좋습니다.

클래스

예전의 자바스크립트 명세에는 클래스라는 개념이 존재하지 않았기 때문에, 클래스를 구현하려면 우선 함수를 정의하고 그 함수의 객체에 있는 프로토타입을 사용해 메서드를 정의하는 방식을 주로 사용했습니다.

자바스크립트 클래스 만들기
function Travel(destination, length) {
    this.destination = destination;
    this.length = length;
}
Travel.prototype.guide = function(){
    console.log( '이번 ' + this.destination + ' 여행은 ' + this.length + '일 간의 일정으로 진행됩니다.');
}
var europe = new Travel('유럽', 15);
europe.guide(); // 이번 유럽 여행은 15일 간의 일정으로 진행됩니다.

하지만 이런 방법은 전통적인 객체 지향 프로그래밍 언어에서 지원하는 방식과는 다소 차이가 있는 낯선 방식인데, 자바스크립트에서 함수는 객체의 역할을 하고 상속은 프로토타입을 통해서 처리하기 때문입니다.

ES6에서는 클래스의 개념이 추가되었는데 다음과 같이 class 키워드를 사용하여 구현할 수 있습니다. 참고로 일반적인 클래스의 네이밍 규칙은 첫 글자를 대문자로 시작합니다.

ES6로 클래스 정의하기
class Travel {
    constructor(destination, length) {
        this.destination = destination;
        this.length = length
    }
    guide() {
        console.log( '이번 ' + this.destination + ' 여행은 ' + this.length + '일 간의 일정으로 진행됩니다.');
    }
}

class라는 키워드를 사용하니까 기존의 방식보다 더 가독성이 좋아지고 이해할 수 있는 코드가 되었는데, 정의된 클래스를 새로운 인스턴스로 만들어서 사용하고 싶다면 기존과 같이 new 키워드를 사용하면 됩니다.

클래스 사용하기
const myTrip = new Travel('태국', 7);
myTrip.guide(); // 이번 태국 여행은 7일 간의 일정으로 진행됩니다.

클래스는 만들고 나면 new를 호출하여 새로운 객체를 원하는 만큼 생성할 수 있고, 다른 객체지향 프로그래밍 언어처럼 클래스를 확장할 수도 있습니다.

기존의 클래스인 부모 클래스 또는 상위 클래스는 확장하여 새로운 자식 클래스 또는 하위 클래스를 만들 수 있는데, 확장한 새로운 클래스는 상위 클래스의 모든 프로퍼티메서드를 상속합니다. 그리고 이렇게 상속한 프로퍼티나 메서드는 하위 클래스의 선언 안에서 변경할 수 있습니다.

추상 클래스 만들기

기존의 클래스를 추상 클래스로 사용할 수도 있는데, 앞에서 만든 Travel 클래스는 extends 키워드로 프로퍼티를 추가해 추상 클래스로 만들 수 있습니다.

추상 클래스 만들기
class Expedition extends Travel {
    constructor(destination, length, tool) {
        super(destination, length);
        this.tool = tool
    }
    guide() {
        super.guide();
        console.log(`이번 여행에는 ${this.tool.join('와 ')}가 필요합니다.`);
    }
}

코드에서 하위 클래스가 상위 클래스의 프로퍼티를 상속하는 간단한 상속 관계를 확인 할 수 있는데, guide()는 상위 클래스에 있는 guide 메서드를 호출한 후 Expedition에 있는 추가 정보를 출력합니다.

이렇게 상속으로 만든 하위 클래스의 인스턴스도 new 키워드를 사용하여 인스턴스를 생성해 사용할 수 있습니다.

const myTrip2 = new Expedition('에베레스트', 30, ['등산장비', '카메라']);
myTrip2.guide();
에베레스트 여행은 30일 간의 일정으로 진행됩니다.
이번 여행에는 등산장비와 카메라가 필요합니다.

클래스 상속과 프로토타입 상속

사실 ES6의 객체지향 프로그래밍은 완벽한 객체지향 프로그래밍이라고 할 수는 없는데, class 키워드를 사용해도 실제적으로는 prototype을 사용하기 때문입니다.

앞에서 만든 Travel 클래스에 prototype을 연결하여 콘솔에 출력해보면, 프로토타입에 포함되어 있는 생성자와 guide() 메서드를 확인할 수 있습니다.

ES6의 상속 객체
console.log(Travel.prototype); // {constructor: ƒ, guide: ƒ}

ES6 모듈

ES6에서 모듈은 다른 자바스크립트 파일에서 불러와 활용할 수 있는 ‘재사용이 가능한 코드 조각’을 의미합니다. 이전의 자바스크립트에서 모듈화를 구현하기 위해서는 모듈의 임포트와 익스포트를 처리할 수 있는 라이브러리를 활용해야 했는데, ES6에서는 자체적으로 모듈을 지원합니다.

ES6는 각각의 모듈을 별도의 파일로 저장하는데, 저장된 모듈을 외부에 익스포트하는 방법은 크게 두 가지 방법이 있습니다.

첫번째는 한 모듈에서 여러 자바스크립트 객체를 외부에 노출시키는 것으로, export 키워드를 사용하여 여러개의 객체를 외부에 노출시키는 방법입니다. export 키워드는 다른 모듈에서 사용할 수 있도록 함수 또는 객체, 변수 등을 외부에 익스포트해주는 기능을 제공합니다.

여러개의 객체를 노출시키는 경우
export const print(msg) => log(msg, new Date());
export const log(msg, timeStamp) => console.log(`${timeStamp.toString()}: ${msg}`);

두번째 방법은 모듈에서 단 하나의 타입(변수, 객체, 배열, 함수 등)만 노출시키는 경우로, 단 하나의 객체만 노출시키는 경우에는 export default를 사용합니다.

하나의 타입만 노출시키는 경우
const myTrip = new Expedition('에베레스트', 30, ['등산장비', '카메라']);
export default myTrip;

exportexport default는 모두 자바스크립트의 변수나 객체, 배열, 함수 등의 모든 타일을 외부에 노출 시킬 수 있는데, 노출된 타입은 import 명령으로 불러와 사용할 수 있습니다.

외부에 여러 타입을 노출한 모듈을 import하는 경우에는 객체 구조 분해를 활용하여 불러올 수 있고, export default를 사용하여 하나의 타입만 노출한 경우에는 이름을 사용하여 불러올 수 있습니다.

모듈 import
import {print, log} from './msg.js';
import myTrip from './trip.js'
print('메시지 print');
log('메시지 log');
myTrip.guide();

모듈을 임포트할 때 as 키워드를 사용하여 다른 이름을 부여하거나, *를 사용하여 모든 모듈을 가져올 수도 있습니다. *를 사용하면 다른 모듈에서 가져온 모든 타입을 사용자의 로컬 객체로 가져올 수 있습니다.

as 키워드를 사용한 경우
import {print as msg, log as logMsg} from './msg.js';
msg('메시지 print');
logMsg('메시지 log');
전체 모듈을 가져오는 경우
import * as funcs from './msg.js';

Common JS

Common JS는 노드에서 지원하는 일반적인 모듈 패턴으로, Common JS를 사용하면 module.exports를 사용하여 자바스크립트 객체를 익스포트할 수 있는데, 모듈을 불러올 때는 import 대신 require 함수를 사용해야 합니다.

Node.js에서 모듈 내보내기
const print(msg) => log(msg, new Date());
const log(msg, timeStamp) => console.log(`${timeStamp.toString()}: ${msg}`);
module.exports = {print, log};

자바스크립트의 모듈 시스템에 대해서는 자바스크립트 모듈 시스템: ESM과 CommonJS에 조금 더 자세히 정리했습니다. 모듈 시스템에 대해 조금 더 자세히 알고 싶다면 해당 글을 참고하세요.

Epilogue

최신 자바스크립트에서 지원하는 주요 기능들은 이미 대부분의 브라우저에서 지원하고 있고, 대표적인 자바스크립트 프레임워크인 Vue, React 등에서도 대부분 사용할 수 있습니다.

최신 자바스크립트의 새로운 문법은 대부분 더 쉽고 간단한 코딩을 할 수 있도록 지원하는 문법이기 때문에, 더 효율적이고 빠른 코딩을 할 수 있는 만큼, 새로운 문법에 익숙해 진다면 코딩 생산성도 획기적으로 상승시킬 수 있을 겁니다.

Leave a Reply