본문 바로가기

프로그래밍(TA, AA)/자바스크립트

[자바스크립트] ES2015(ES6) 알아보기 2

Template


 

javascript 개발에서 Template이라는 개념은 매우 중요합니다. JSON으로 응답을 받고 Javascript Object로 변환하고 어떠한 데이터처리 조작을 한 후에 dom에 추가하는 이 일련의 과정이 결국 UI 개발의 핵심이 되기 때문입니다.

 

Javascript 개발은 데이터 + HTML문자열의 결합이라고 생각할 수 있습니다. 실제 DOM 조작 작업에 많은 로드가 들어가기 때문에 React나 Angular에서는 Virtual DOM과 같은 개념을 도입하여, 템플릿 작업을 프레임단에서 할 수 있게 지원하고 있습니다. 이전에는 Underscore, Handle bar 이런 라이브러리를 활용하여 ES6의 템플릿은 이러한 기능을 일부 포함하고 있습니다.

 

const data = [
  {
    name : 'coffee-bean',
    order : true,
    items : ['americano', 'milk', 'green-tea']
  },
  {
    name : 'starbucks',
    order : false
  }
]

const template = `<div>welcome ${data[0].name} !!`;
console.log(template);

이를 잘활용하면 복잡한 처리를 하는데 매우 유용합니다. 다음은 Tagged template literals에 대해 알아보겠습니다. 템플릿을 조작하여 쓸 필요가 있을때, function에서 어떠한 처리를 가한 후에 반환이 가능합니다. 조건문이나 반복상황에서 다르게 처리할 필요가 있습니다.

 

const data = [
  {
    name : 'coffee-bean',
    order : true,
    items : ['americano', 'milk', 'green-tea']
  },
  {
    name : 'starbucks',
    order : false
  }
]

// Tagged template literals
function fn(tags, name, items) {
  console.log(tags);
  if(typeof items === "undefined") {
    items = "<span style='color:red'>주문가능한 상품이 없습니다.</span>";
  }
  return (tags[0] + name + tags[1] + items + tags[2]);
}

data.forEach((v) => {
  let template = fn`<div>welcome ${v.name} !!</div>
      <h2>주문가능항목</h2><div>${v.items}</div>`;
  document.querySelector("#message").innerHTML += template;
  console.log(template);
});

const template = fn`<div>welcome ${data[0].name} !!</div>
  <h2>주문가능항목</h2><div>${data[1].items}</div>`;

console.log(template);

 

Function


 

javascript는 다양한 형식의 함수형 프로그래밍이 가능합니다. ES2015에서는 arrow function을 지원합니다. 그 예는 아래와 같습니다. 나중에 실행되는 함수를 callback 함수라고 합니다.

 

// arrow function
setTimeout(function() {
  console.log("settimeout");
}, 1000);

// 축약 표현법
setTimeout(()=>{
  console.log("settimeout arrow")
}, 1000);


let newArr = [1,2,3,4,5].map(function(value, index, object){
  return value * 2;
});
// 그 값을 그대로 반환한다.
let newArr2 = [1,2,3,4,5].map( (v) => (v * 2) );

 

자바스크립트에서 this는 보통 실행타임에 의해서 변경이 있기 때문에 bind를 이용하여 context의 this를 잡아줍니다. Arrow function 안에서의 this context는 bind없이 우리가 원하는 this context를 잡아주는데요. 즉 선언된 것을 기준으로 this가 결정됩니다. 그에 대해 알아보겠습니다.

 

// this context of Arrow function
const myObj = {
  runTimeout() {
    setTimeout(function() {
      this.printData();
    }.bind(this), 200);
  },
  printData() {
    console.log("hi codesquad!");
  }
}

myObj.runTimeout();

/* arrow function을 쓰면, event Queue에 있다가
나중에 실행 되는 거여서 this가 window가 잡히는 건데,
arrow Function의 경우에는 이를 유지하고 있다. */
const myObj2 = {
  runTimeout() {
    setTimeout(() => {
      this.printData();
    }, 200);
  },
  printData() {
    console.log("hi codesquad!");
  }
}

myObj2.runTimeout();

 

const el = document.querySelector("p");

const myObj = {
  register() {
    el.addEventListener("click", (evt) => {
      this.printData(evt.target);
    });
  },
  printData(el) {
    console.log('clicked!!', el.innerText);
  }
};

myObj.register();

 

다음은 default parameters에 대해 알아보겠습니다. 보통 파라미터에 값을 지정하지 않으면 undefined가 들어오게 되어 함수 내부에서 if문을 이용해 기본값을 처리하곤 했습니다.

 

// default parameters

function sum(value, size) {
  // 기존 처리방식
  size = size || 1;
  return value * size;
}

function sum(value, size=1) {
  return value * size;
}

function sum(value, size={value:1}) {
  return value * size.value;
}

인자값의 개수가 여러개일때 어떻게 처리해야할까요? 보통은 함수 내장의 arguments를 이용하기도 하고, arguments가 array가 아니라서 array 관련 함수를 적용하기가 어렵습니다. 일단 예제를 보며 관련 내용을 살펴보겠습니다.

 

function checkNum() {
  const argArray = Array.prototype.slice.call(arguments);
  console.log(toString.call(argArray));
  const result = argArray.every((v) => typeof v === "number")
  console.log(result);
}

// const result = checkNum(10,2,"55");
const result = checkNum(10,2,3,4,5,"55");

// rest parameters
// 변환없이 바로 배열로 받을 수 있다.
function checkNum(...argArray) {
  console.log(toString.call(argArray));
  const result = argArray.every((v) => typeof v === "number")
  console.log(result);
}

 

Class


자바스크립트에는 클래스가 없습니다. ES6에서는 이러한 단점을 보완하여 Class 키워드가 새로 생겼습니다. 기존에는 Function과 prototype 등을 이용해서 Class와 비슷하게 구현해 왔습니다. 기존의 방식을 이용하는 것도 무방하지만, ES6가 제공하는 Class 키워드를 이용하면 모듈화가 되기때문에 가독성이 굉장히 좋아지는 것이 있습니다. Function과 prototype을 이용한 기존 방식과 내부적인 구현은 완전히 동일합니다.


// 객체를 generate하는 기존 방식
function Health(name) {
  this.name = name;
}

Health.prototype.showHealth = function() {
  console.log(this.name + "님 안녕하세요");
}

// h는 객체가 됨.
const h = new Health("crong");
h.showHealth();


// ES6 Class
class Health {
  constructor(name, lastTime) {
    this.name = name;
    this.lastTime = lastTime;
  }
  
  showHealth() {
    console.log("안녕하세요" + this.name);
  }
}

const myHealth = new Health("crong");
myHealth.showHealth();

// 객체 타입을 확인하는 방법
console.log(toString.call(Health));


ES6에서 Object, Assign, Prototype이 어떻게 개선되었는지도 알아보겠습니다. Object를 잘활용하여 모듈을 만드는 방법은 순수 javascript 방식을 따른다라고 볼 수 있습니다. 


// Object assign 메서드.

var healthObj = {
  showHealth : function() {
    console.log("오늘 운동시간 : " + this.healthTime);
  }
}

const myHealth = Object.create(healthObj);

myHealth.healthTime = "11:20";
myHealth.name = "crong";

console.log(myHealth);

// assign을 이용하면 prototype 속성값만 가져오고,
// 객체의 속성값들을 따로 지정하여 선언할 수도 있습니다.
const myHealthSecond = Object.assign(Object.create(healthObj), {
  name : "crong",
  lastTime : "11:20"
});
console.log(myHealthSecond);


Object assign 활용하는 다른 방법도 함께 알아보겠습니다. Object assign은 새로운 객체를 만드는 방법이기도 합니다. React같은 경우는 immutable관련 라이브러리를 이용하여 Object나 Array를 복사합니다. Object assign 기능 혹은 Spread Operator을 이용하면 라이브러리 없이도 복사가 가능해집니다. immutable을 이용하면 이전 시점 되돌리기 기능들을 개발할 때 아주 유용합니다.


const previousObj = {
  name : "crong",
  lastTime : "11:20"
};

// 첫번째 인자값에 어떤 것을 지정해주면 그것이 prototype 값으로 지정됩니다.
// 두번째 인자에 들어온 Object의 모든 값을 가져오고,
// 세번째 인자에 두번째 인자를 대체할 값이 있다면 대체하고, 없으면 대체하지 않습니다.
const afterObject = Object.assign({}, previousObj, {
      "lastTime" : "12:30",
      "name" : "honux",
      "age" : 99
});

// previousObj와 afterObject는 다른 객체 입니다.
console.log(previousObj, afterObject);


이번에는 setPrototypeOf에 대해 알아보겠습니다. 메소드명에서도 알 수 있듯이 어떤 객체의 prototype을 세팅한다는 의미입니다. immutable 객체를 만들수 있는 Object assign과도 유사하지만, Object assign은 추가적인 기능을 범용적으로 지원해주는 것에 가깝다면, setPrototypeOf은 prototype객체만 추가하는 것이라 명확하고 단순한 기능을 유지합니다. setPrototypeOf 잘 활용하고 싶은 모듈 덩어리를 만들고 싶을때 쓰면 됩니다. Object.Create 혹은 Object.setPrototypeOf를 쓰는 것이 복잡한 상속 구조가 들어갈 경우에는 세부적인 구현들이 복잡해질 수는 있습니다. 하지만 순수 자바스크립트를 이용하는 가장 표준적인 방법이라고 할 수 있습니다.


// setPrototypeOf
const healthObj = {
  showHealth : function() {
    console.log("오늘 운동시간 : " + this.healthTime);
  },
  setHealth : function(newTime) {
    this.healthTime = newTime;
  }
}

const myHealth = {
  name : "crong",
  lastTime : "11:20"
};

// myHealth에 healthObj에 추가.
Object.setPrototypeOf(myHealth, healthObj);

console.log("myhealth is ", myHealth);

// 추가하는 다른 방식. Object.assign과 유사함.
// but, Object.Create를 안써도 됨.
const newObj = Object.setPrototypeOf({
  name : "crong",
  lastTime : "11:20"
}, healthObj);


// setPrototypeOf가 지원되지 않을 경우
Object.setPrototypeOf = Object.setPrototypeOf || function(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

console.log("newobj is " + newobj);


이번에는 setPrototype을 이용하여 prototype chain을 구현하는 방법에 대해 알아보겠습니다. 이를 알아보기전에 prototype을 구성하는 순수 속성을 이해할 필요도 있습니다. prototype chain은 일종의 상속입니다.


// parent
const healthObj = {
  showHealth : function() {
    console.log("오늘 운동시간 : " + this.healthTime);
  },
  setHealth : function(newTime) {
    this.healthTime = newTime;
  }
}

// child obj
const healthChildObj = {
  getAge : function() {
    return this.age;
  }
}

const childObj = Object.setPrototypeOf({
  age : 22
}, healthChildObj);

console.log("childobj is ", childObj);

// 다른쪽에 있는 Obj를 쓰고싶다면, prototype에 chain을 맺으면 됨.
Object.setPrototypeOf(healthChildObj, healthObj);

Object.setPrototypeOf(healthChildObj, healthObj);

const lastHealthObj = Object.setPrototypeOf({
  age : 22
}, healthChildObj);

console.log("lastHealthObj is", lastHealthObj);



module


javascript에서의 module import는 아직 표준화되지 않은 실험적인 기능입니다. 반면에 javascript를 이용한 백엔드 기반 개발에서는 많은 파일들이 필요하고, 브라우저와 달리 script src를 써서 스크립트를 불러올 수 없기 때문에 module를 불러오고 수많은 클래스들의 의존성을 정리할 필요가 있습니다. 그래서 amd, common 등등의 파일들을 require들을 이용하여 import 하는 기능을 지원하기도 합니다. ES2015에서는 import, export 에 대한 스펙을 제시는 했지만 아직 많은 브라우저들이 지원하지 않고 있고, 안정화 될때까지 지원하도록 기다리고 있는 상황입니다. ie 엣지에서는 지원을 하긴하지만 전체적으로 지원이 완전하지 않습니다. 이를 지원하려면 webpack 기반으로 빌드환경을 구성하고, 바벨을 이용하여 ES5로 트랜스파일링(transpiling) 하는 작업이 필요합니다. 그럼에도 불구하고 import, export를 이용하여 개발이 가능합니다. 이를 통해서 서비스 코드를 출시할 수 있습니다. 



Proxy


어떤 Object가 있을때, 그 Object를 가로채서 다른 작업을 추가로 하도록 제공하는 역할을 합니다. 프론트엔드 개발에서 기존에 할수없던 것을 한다기보다는 Object가 변경되거나 추가/삭제, 값을 얻으려고 하는 작업 사이에 부수적인 작업을 추가할 수 있습니다. 


// Proxy.
const myObj = {name : 'crong'};
const proxy = new Proxy(myObj, {});

toString.call(proxy);  // myObj와 같은 값.
console.log(proxy === myObj); // false 리턴.
console.log(proxy.name == myObj.name); // true 리턴.

// Proxy 활용.
const myObj = {name : 'crong', changedValue : 0};
const proxy = new Proxy(myObj, {
  get : function(target, property, receiver) {
    console.log('get value');
    return target[property];
  },
  set : function(target, property, value) {
    console.log('change value');
    target['changedValue']++;
    target[property] = value;
  }
});


이와 같이 Proxy를 이용하면 객체의 변화나 접근을 중간에 가로채서 작업을 추가할 수도 있습니다. 변화량을 모니터링해서 그 변화량에 대해 다른 영역에 내용을 표시하는 기능도 적용하고 있습니다. set과 get만 잘 활용해도 그 two-way binding 등 데이터바인딩 상황에서 매우 유용하게 쓰일 수 있습니다.


혹자는 값을 반환하는 것만으로는 Proxy라고 생각하지 않기도합니다. 이때는 Reflect를 권장하기도 합니다.


// Proxy.
const myObj = {name : 'crong'};
const proxy = new Proxy(myObj, {});

toString.call(proxy);  // myObj와 같은 값.
console.log(proxy === myObj); // false 리턴.
console.log(proxy.name == myObj.name); // true 리턴.

// Proxy 활용.
const myObj = {name : 'crong', changedValue : 0};
const proxy = new Proxy(myObj, {
  get : function(target, property, receiver) {
    console.log('get value');
    return Reflect.get(target, property);
  },
  set : function(target, property, value) {
    console.log('change value');
    target['changedValue']++;
    target[property] = value;
  }
});


만약 default값이 존재한다면 아래와 같이 구현하기도 합니다.


  get : function(target, property, receiver) {
    console.log('get value');
    return (property in target) ? target[property] : "anonymouse";
  }