Primitive(값) vs Object(참조)

Primitive Type

  • String : 텍스트를 셋팅하는데 사용하는 타입.
  • Number : 숫자를 셋팅하는데 사용하는 타입. 기본적으로 소수점도 가능하다.(infinity, -inifinity, NaN 표현이 가능하다.)
  • Null : null타입은 정확히는 1개의 값은 가지고 있지만 비어있다는 뜻이다.
  • Undefined : 값이 할당되지 않는 것을 나타내는 타입.
  • Boolean : true 또는 false 로 나타내는 타입.
  • Symbol : 새로 추가된 타입으로 unique하고 immutable한 원시값 으로 사용된다.(ES6)

Primitive Type의 생성 방법

  1. Literal
  • Literal로 생성한다고 하면 우리가 가장 많이 사용하는 방법
var bol = true; var str = "hello"; var num = 3.14; var nullType = null; var undef = undefined; var bol2; var str2; bo2 = false str2 = "world"
  1. Wrapper Object

  • Wrapper Object를 사용해서 만든다고 하면 Constructor를 사용해서 만드는 것
  • 즉, new 를 사용하여 생성
new Boolean(false); new String("world"); new Number(42); Symbol("foo"); //Symbol 타입의 생성방법

Literal vs Wrapper

typeof true; //"boolean" typeof Boolean(true); //"boolean" typeof new Boolean(true); //"object" typeof (new Boolean(true)).valueOf(); //"boolean" typeof "abc"; //"string" typeof String("abc"); //"string" typeof new String("abc"); //"object" typeof (new String("abc")).valueOf(); //"string" typeof 123; //"number" typeof Number(123); //"number" typeof new Number(123); //"object" typeof (new Number(123)).valueOf(); //"number"

Literal로 생성한 것의 타입은 6가지 중 하나로 나오게 된다. 그런데 new를 사용하여 Wrapper Object로 만들게 되면 Object타입이 나오게 된다. 사용을 하려면 valueOf라는 Function을 사용해야만 입력한 값이 나오게 된다.

값 타입

var a = 13 // assign `13` to `a` var b = a // copy the value of `a` to `b` b = 37 // assign `37` to `b` console.log(a) // => 13

b의 값을 변경을 했지만 a에는 영향이 가지 않았다. 이유는 2개의 값이 저장된 공간이 다르기 때문이다.

Object Type

  • Array : 우리가 알고 있는 배열, 리스트의 형태를 가지고 있다.
  • Function : Javascript에서는 Function Object가 존재하지만 결국 Function도 Object.
  • Object : Map처럼 사용하는 즉, key : value의 형태로 사용하고 있는 Object.
var a = { c: 13 } // assign the reference of a new object to `a` var b = a // copy the reference of the object inside `a` to new variable `b` b.c = 37 // modify the contents of the object `b` refers to console.log(a) // => { c: 37 }
var a = []; var b = a; a.push(1); console.log(a); // [1] console.log(b); // [1] console.log(a === b); // true
function changeAgeImpure(person) { person.age = 25; return person; } var alex = { name: 'Alex', age: 30 }; var changedAlex = changeAgeImpure(alex); console.log(alex); // -> { name: 'Alex', age: 25 } console.log(changedAlex); // -> { name: 'Alex', age: 25 }

원시타입과는 다르게 복사한 것을 변경을 했더니 기존 객체에도 영향이 간다. 이유는 같은 값의 주소를 복사했기 때문이다.


🙏 Reference

Primitive(값) vs Object(참조)

Primitive Type

  • String : 텍스트를 셋팅하는데 사용하는 타입.
  • Number : 숫자를 셋팅하는데 사용하는 타입. 기본적으로 소수점도 가능하다.(infinity, -inifinity, NaN 표현이 가능하다.)
  • Null : null타입은 정확히는 1개의 값은 가지고 있지만 비어있다는 뜻이다.
  • Undefined : 값이 할당되지 않는 것을 나타내는 타입.
  • Boolean : true 또는 false 로 나타내는 타입.
  • Symbol : 새로 추가된 타입으로 unique하고 immutable한 원시값 으로 사용된다.(ES6)

Primitive Type의 생성 방법

  1. Literal
  • Literal로 생성한다고 하면 우리가 가장 많이 사용하는 방법
var bol = true; var str = "hello"; var num = 3.14; var nullType = null; var undef = undefined; var bol2; var str2; bo2 = false str2 = "world"
  1. Wrapper Object

  • Wrapper Object를 사용해서 만든다고 하면 Constructor를 사용해서 만드는 것
  • 즉, new 를 사용하여 생성
new Boolean(false); new String("world"); new Number(42); Symbol("foo"); //Symbol 타입의 생성방법

Literal vs Wrapper

typeof true; //"boolean" typeof Boolean(true); //"boolean" typeof new Boolean(true); //"object" typeof (new Boolean(true)).valueOf(); //"boolean" typeof "abc"; //"string" typeof String("abc"); //"string" typeof new String("abc"); //"object" typeof (new String("abc")).valueOf(); //"string" typeof 123; //"number" typeof Number(123); //"number" typeof new Number(123); //"object" typeof (new Number(123)).valueOf(); //"number"

Literal로 생성한 것의 타입은 6가지 중 하나로 나오게 된다. 그런데 new를 사용하여 Wrapper Object로 만들게 되면 Object타입이 나오게 된다. 사용을 하려면 valueOf라는 Function을 사용해야만 입력한 값이 나오게 된다.

값 타입

var a = 13 // assign `13` to `a` var b = a // copy the value of `a` to `b` b = 37 // assign `37` to `b` console.log(a) // => 13

b의 값을 변경을 했지만 a에는 영향이 가지 않았다. 이유는 2개의 값이 저장된 공간이 다르기 때문이다.

Object Type

  • Array : 우리가 알고 있는 배열, 리스트의 형태를 가지고 있다.
  • Function : Javascript에서는 Function Object가 존재하지만 결국 Function도 Object.
  • Object : Map처럼 사용하는 즉, key : value의 형태로 사용하고 있는 Object.
var a = { c: 13 } // assign the reference of a new object to `a` var b = a // copy the reference of the object inside `a` to new variable `b` b.c = 37 // modify the contents of the object `b` refers to console.log(a) // => { c: 37 }
var a = []; var b = a; a.push(1); console.log(a); // [1] console.log(b); // [1] console.log(a === b); // true
function changeAgeImpure(person) { person.age = 25; return person; } var alex = { name: 'Alex', age: 30 }; var changedAlex = changeAgeImpure(alex); console.log(alex); // -> { name: 'Alex', age: 25 } console.log(changedAlex); // -> { name: 'Alex', age: 25 }

원시타입과는 다르게 복사한 것을 변경을 했더니 기존 객체에도 영향이 간다. 이유는 같은 값의 주소를 복사했기 때문이다.


🙏 Reference

명시적 변환 vs 암묵적 변환

Number(value) 와 같은 코드를 작성하여 변환할 의사를 명확하게 표현하는 것을 명시적 변환이라고 한다. JavaScript는 동적 타입 언어이므로 값을 자동으로 여러 유형간에 변환을 자동으로 한다. 이것을 암묵적 변환 이라고 한다.

암묵적 변환을 하지 않는 연산자는 === 이며, 완전 항등 연산자 라고 한다. 반면에 느슨한 항등 연산자 == 는 필요하다면 비교와 타입 강제 변환을 수행한다.

String 변환

String(123) // 명시적 123 + '' // 암시적
String(123) // '123' String(-12.3) // '-12.3' String(null) // 'null' String(undefined) // 'undefined' String(true) // 'true' String(false) // 'false'

Symbol 변환은 명시적으로만 변환될 수 있고, 암시적 변환은 되지 않는다.

String(Symbol('my symbol')) // 'Symbol(my symbol)' '' + Symbol('my symbol') // TypeError is thrown

Boolean 변환

Boolean(2) // 명시적 if (2) { ... } // 논리적 문맥 때문에 암시적 !!2 // 논리적 문맥 때문에 암시적 2 || 'hello' // 논리적 문맥 때문에 암시적

논리 연산자(예 : ||&& )에 따른 Boolean 변환을 내부적으로 수행하지만 Boolean값이 아니더라도 원래 피연산자의 값을 실제로 반환한다.

// true를 반환하는 것이 아닌 123를 반환하고 있다. // 'hello' and 123 은 표현식을 계산하기 위해서 Boolean으로 강제 변환을 한다. let x = 'hello' && 123; // 123
Boolean('') // false Boolean(0) // false Boolean(-0) // false Boolean(NaN) // false Boolean(null) // false Boolean(undefined) // false Boolean(false) // false

object, function, Array, Date, 사용자 정의 유형등은 true 로 변환한다.

Boolean({}) // true Boolean([]) // true Boolean(Symbol()) // true !!Symbol() // true Boolean(function() {}) // true

Numeric 변환

Number() 함수를 사용하면 된다. 암시적 변환은 많은 경우에서 작동이 되기 때문에 까다롭다.

Number('123') // 명시적 - 123 +'123' // 암시적 - 123 123 != '456' // 암시적 - true 4 > '5' // 암시적 - false 5 / null // 암시적 - Infinity true | 0 // 암시적 - 1
Number(null) // 0 Number(undefined) // NaN Number(true) // 1 Number(false) // 0 Number(" 12 ") // 12 Number("-12.34") // -12.34 Number("\n") // 0 Number(" 12s ") // NaN Number(123) // 123
  • 문자열을 숫자로 변환할 때 엔진은 먼저 앞뒤의 공백, \ n, \ t 문자를 제거하고, 문자열이 유효한 숫자를 나타내지 않으면 NaN 을 반환한다. string이 비어 있으면 0을 반환한다.
  • null와 undefined는 다르게 처리가 되는데 null은 0으로 undefined는 NaN으로 된다.

Symbol은 명시적 또는 암시적으로 숫자로 변환될 수 없다. 또한 TypeErrorundefined로 발생하는 것처럼 NaN으로 자동 변환하는 대신 throw 된다.

Number(Symbol('my symbol')) // TypeError is thrown +Symbol('123') // TypeError is thrown

Tips

  • ==null 또는 undefined 에 적용하면 숫자 변환이 발생하지 않는다. nullnull, undefined 와 동일하다.
null == 0 // false, null is not converted to 0 null == null // true undefined == undefined // true null == undefined // true null === undefined // false
  • NaN은 그 자체가 동등하지 않다.
var value = NaN; if (value !== value) { console.log("we're dealing with NaN here") }

🙏 Reference

Functional Scope vs Block Scope

Functional Scope

자바스크립트는 함수를 단위로 Scope를 구분한다. 즉 같은 함수 안에서 선언된 변수들은 같은 레벨의 Scope를 가지게 되는 것이다. 각각의 함수는 독립적인 Scope를 가지게 되어 다른 함수의 Scope에 접근을 할 수 없다.

// Global Scope function someFunction() { if (true) { var name = "BKJang"; } console.log(name); //BKJang }

위와 같이 Global Scope에 someFunction() 을 선언하고 내부에 if문 괄호 안에 선언한 변수는 someFunction function Scope 에 붙게 된다. 함수를 단위로 스코프가 생기기 때문에 name을 출력하면 undefined가 아닌 BKJang이 출력된다.

Block Scope

Block Statement는 우리가 많이 보는 if문, switch문, for, while문이다. 이러한 문장들은 괄호로 감싸진 부분이 존재하지만 새로운 Scope를 만들지는 않는다. Block Statement 안에서 정의한 변수는 가장 가까운 함수의 Scope에 붙게 된다.

if (true) { var name = "BKJang"; } console.log(name); // BKJang

ES6에서는 let, const가 추가 되었다. 이 2개는 var 대용으로 사용된다. 그러나 그보다 더 중요한 개념이 들어간다. 바로 Block Level Scope 라는 것이다. 기존의 자바스크립트는 위에서 본 것처럼 Functional Scope 이다. 그러나 let, const 를 사용하게 되면 Block Level Scope 지원이 가능하다.

if (true) { var name = "BKJang"; let likes = "Coding"; const lang = "Javascript"; } console.log(name); // 'BKJang' console.log(likes); // Uncaught ReferenceError: likes is not defined console.log(lang); // Uncaught ReferenceError: lang is not defined

var와는 다르게 let, const는 Block Statement내에서 Local Scope 를 지원한다. 즉 이제 Scope가 가장 가까운 function에 붙는 것이 아닌 해당 Block에 붙게 되는 것이다.

참고로 Global Scope는 응용 프로그램이 살아있을 때까지 유효하며, Local Scope은 함수가 호출되고 실행되는한 유지가 된다.


🙏 Reference

JavaScript의 this

자바스크립트에서 this의 바인딩은 함수의 호출 방식에 따라 결정된다.

  • 객체의 메서드 호출
  • 일반 함수 호출
  • 생성자 함수의 호출
  • callapply를 이용한 this 바인딩
  • ES6의 화살표 함수

객체의 메서드 호출

var obj = { organization: "Im-D", sayHello: function() { return "Welcome to " + this.organization; } }; console.log(obj.sayHello());

객체의 메서드를 호출할 때 this해당 객체에 바인딩된다.

일반 함수 호출

var organization = "Im-D"; function sayHello() { var organization = "Kyonggi"; return "Welcome to " + this.organization; } console.log(sayHello());

일반 함수를 호출할 때, 자바스크립트의 this전역 객체(window 객체)에 바인딩 된다.

생성자 함수의 호출

function Organization(name, country) { this.name = name; this.country = country; } var imD = new Organization("Im-D", "South Korea"); var kyonggi = Organization("Kyonggi", "South Korea"); console.log(imD); console.log(kyonggi);

생성자 함수를 new키워드를 통해 호출할 경우, 새로 생성되는 빈 객체에 바인딩 된다. 단, new키워드를 사용하지 않으면 this는 전역객체에 바인딩된다.

call, apply, bind를 활용한 this바인딩

call

function Module(name) { this.name = name; } Module.prototype.getName = function() { const changeName = function() { console.log(this); return this.name + "입니다."; }; // return changeName.call(this, 1,2,3,4); return changeName.call(this); }; const module = new Module("BKJang"); console.log(module.getName());

apply

function Module(name) { this.name = name; } Module.prototype.getName = function() { const changeName = function() { console.log(this); return this.name + "입니다."; }; // return changeName.apply(this, [1,2,3,4]); return changeName.apply(this); }; const module = new Module("BKJang"); console.log(module.getName());

또한 call이나 apply메서드를 활용하여 유사배열 객체를 일반 배열로 바꿀 수도 있다.

function sayHello() { console.log(arguments); var args = Array.prototype.slice.apply(arguments); console.log(args); } sayHello("Im-D", "South Korea");

callapply는 내부 함수에서 사용할 this를 설정하고 함수 바로 실행까지 해주지만, bindthis만 설정해주고 함수 실행은 하지 않고 함수를 반환한다.

function Module(name) { this.name = name; } Module.prototype.getName = function() { const changeName = function() { console.log(this); return this.name + "입니다."; }; let bindChangeName = changeName.bind(this); return bindChangeName(); }; const module = new Module("BKJang"); console.log(module.getName());

화살표 함수

var obj = { organization: "Im-D", outerFunc: function() { var that = this; console.log(this.organization); innerFunc = function() { console.log(that.organization); console.log(this.organization); }; innerFunc(); } }; obj.outerFunc();
var obj = { organization: "Im-D", outerFunc: function() { console.log(this.organization); innerFunc = () => { console.log(this.organization); }; innerFunc(); } }; obj.outerFunc();

ES5에서는 원래 내부 함수에서의 thiswindow객체에 바인딩 되었기 때문에 var that=this;와 같이 선언하여 thatthis를 할당하고 내부 함수에서는 that을 활용하는 방식을 사용했었다.

하지만, ES6에서 등장한 화살표 함수에서는 this가 무조건 상위 스코프의 this를 가리킨다. 이에 따라 내부함수에서 var that=this;와 같은 구문을 사용할 필요가 없다. 이처럼 정적으로 this가 바인딩되기 때문에 Lexical this라고 한다.


🙏 Reference

Prototype

프로토타입 체인

특정 객체의 메서드나 프로퍼티에 접근하고자할 때, 해당 객체에 접근하려고 하는 프로퍼티나 객체가 없다면 프로토타입 링크([[Prototype]] 프로퍼티)를 따라 자신의 부모 역할을 하는 프로토타입 객체를 차례로 검색한다. 이를 프로토타입 체인이라 한다.

var developer = { name: "BKJang", age: 25, sex: "male" }; console.log(developer.hasOwnProperty("name")); //true console.log(developer.__proto__ === Object.prototype); //true console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); //true

developer객체에는 hasOwnProperty()메서드가 존재하지 않지만 에러가 나지 않는다. 이는 developer 객체의 부모 객체인 Object.prototype 해당 메서드를 검색하기 때문이다.

객체 리터럴 방식으로 생성했을 때 프로토타입 체인

var developer = { name: "BKJang", age: 25, sex: "male" }; console.log(developer.__proto__ === Object.prototype); //1.true console.log(Object.prototype.constructor === Object); // 2.true console.log(Object.__proto__ === Function.prototype); //3.true console.log(Function.prototype.__proto__ === Object.prototype); //4.true

JavaScript

developer 객체와 Function.prototype 객체의 프로토타입 객체는 Object.prototype 객체다.

객체리터럴 방식으로 객체를 생성하면 해당 객체의 프로토타입 객체는 Object.prototype 객체다.

생성자 함수를 생성했을 때 프로토타입 체인

function Developer(name) { this.name = name; } var web = new Developer("BKJang"); console.log(web.__proto__ === Developer.prototype); //1.true console.log(Developer.prototype.__proto__ === Object.prototype); //2.true console.log(Developer.prototype.constructor === Developer); //3.true console.log(Developer.__proto__ === Function.prototype); //4.true console.log(Function.prototype.__proto__ === Object.prototype); //5.true

Developer.prototype 객체와 Developer() 생성자 함수의 프로토타입 객체인 Function.prototype 객체의 프로토타입 객체는 Object.prototype 객체다.

JavaScript

프로토타입 체인의 종점(End of prototype chain)

객체 리터럴 방식으로 객체를 생성하든 생성자 함수를 이용해 객체를 생성하든 결국 모든 객체의 부모 객체(프로토타입 객체)는 Object.prototype 객체다. 이 때 Object.prototype 객체를 프로토타입 체인의 종점이라 한다.

프로토타입 객체의 확장

프로토타입 객체 역시 객체다. 따라서, 객체의 프로퍼티를 동적으로 추가하거나 삭제할 수 있다.

function Developer(name) { this.name = name; } var web = new Developer("BKJang"); web.printAge(25); // Uncaught TypeError: web.printAge is not a function

위의 코드의 결과를 보면 web객체에서 printAge()라는 메서드가 없기 때문에 에러가 나는 것을 볼 수 있다.

function Developer(name) { this.name = name; } var web = new Developer("BKJang"); Developer.prototype.printAge = function(age) { console.log("The age of this developer is", age); }; web.printAge(25); // The age of this developer is 25

web 객체의 프로토타입 객체(부모 객체)인 Developer.prototype 객체에 printAge(age)라는 메서드를 추가했다.

이에 따라 web 객체에서 printAge(age)메서드에 접근하면 결과 값을 출력하는 것을 볼 수 있다.

JavaScript

기본 데이터 타입의 확장

자바스크립트에서 숫자, 문자열과 같은 기본 데이터 타입에서 사용되는 표준 메서드의 경우 Number.prototypeString.prototype 객체에 정의되어 있다.

var str = "hello world"; str.printStr = function(text) { console.log(text); }; str.printStr("This is the test"); //Uncaught TypeError: str.printStr is not a function

원시 데이터 타입인 문자열의 경우에는 객체가 아니기 때문에 프로퍼티를 동적으로 추가할 수 없다.

그렇다면, 위에서 str변수에 printStr 메서드를 동적으로 추가하는 코드에서는 왜 에러가 발생하지 않을까?

그 이유는 기본 데이터 타입으로 프로퍼티나 메소드를 호출하면 기본 데이터 타입과 연관된 객체로 일시적으로 변환되어 프로토타입 객체를 공유하게 되기 때문이다.

var str = "hello world"; String.prototype.printStr = function(text) { console.log(text); }; str.printStr("This is the test"); //This is the test "this is string".printStr("This is the test"); //This is the test

문자열 타입의 경우, String.prototype 객체에 표준 메서드가 정의 되어있기 때문에 해당 객체에 메서드를 추가해주면 기본 데이터 타입에서도 해당 메서드를 사용할 수 있다.

var str = "hello world"; String.prototype.printStr = function(text) { console.log(text); }; console.log(str.__proto__ === String.prototype); //1.true console.log(String.prototype.__proto__ === Object.prototype); //2.true console.log(String.prototype.constructor === String); //3.true console.log(String.__proto__ === Function.prototype); //4.true console.log(Function.prototype.__proto__ === Object.prototype); //5.true str.printStr("This is the test"); //This is the test

JavaScript

프로토타입 객체의 변경

자바스크립트에서 특정 객체는 부모 객체인 프로토타입 객체를 임의로 변경할 수 있다.

function Developer(name) { this.name = name; } var web = new Developer("BKJang"); Developer.prototype = { age: 25 }; var android = new Developer("YAKim"); console.log(web.age); //undefined console.log(android.age); //25 console.log(web.constructor); //Developer(name) console.log(android.constructor); //Object()
  • 변경 전 : 파란색 번호
  • 변경 후 : 주황색 번호

JavaScript

프로토타입 객체를 변경하기 전, web객체의 constructor는 프로토타입 체이닝에 따라 Developer()생성자 함수를 가리킨다.

프로토타입 객체를 변경한 후, android객체의 constructorObject() 함수를 가리킨다.

프로토타입 객체가 변경되면서 Developer.prototype 객체의 constructor 프로퍼티와 Developer() 생성자 함수의 연결이 깨진다.

이에 따라 프로토타입 체인이 동작하고 android 객체의 constructorObject.prototype 객체의 constructor 프로퍼티가 가리키는 Object() 함수가 되는 것이다.

  • 프로토타입 객체를 변경하기 전과 후의 프로토타입 링크([[Prototype]] 프로퍼티)는 각각 다른 프로토타입 객체와 바인딩 된다.
  • 프로토타입 객체를 변경한 후에는 프로토타입 객체의 constructor 프로퍼티와 생성자 함수와의 연결이 깨진다.

프로토타입 체인의 동작 조건

프로토타입 체인은 객체의 특정 프로퍼티에 접근할 때, 그 프로퍼티가 해당 객체에 없는 경우 동작한다.

function Developer(name) { this.name = name; } Developer.prototype.age = 25; Developer.prototype.sex = "male"; var web = new Developer("BKJang"); var android = new Developer("YAKim"); android.sex = "female"; console.log(web.age); //1.25 console.log(web.sex); //2.male console.log(android.age); //1.25 console.log(android.sex); //3.female

JavaScript

  1. web 객체에는 agesex 프로퍼티가 없기 때문에 프로토타입 체인에 따라 Developer.prototype 객체의 agesex 프로퍼티에 접근하고 있다.

  2. android 객체에는 age 프로퍼티는 없지만 sex 프로퍼티는 있기 때문에 sex 프로퍼티의 경우엔 프로토타입 체인이 동작하지 않고 android 객체의 sex 프로퍼티 값을 반환하고 있다.


🙏 Reference

실행 컨텍스트

실행 컨텍스트는 자바스크립트가 동작하는 원리라고 할 수 있다.

쉽게 말하면, 코드가 실행되는 환경이라고 보면 된다.

  • 전역 컨텍스트 생성 후, 함수 호출 시마다 함수 컨텍스트가 생긴다.

  • 컨텍스트 생성 시 컨텍스트 안에 변수객체(arguments, variable), scope chain, this가 생성된다.

  • 컨텍스트 생성 후 함수가 실행되는데, 사용되는 변수들은 변수 객체 안에서 값을 찾고, 없다면 스코프 체인을 따라 올라가며 찾는다.

  • 함수 실행이 끝나면 해당 컨텍스트는 사라지고, 페이지가 종료되면 전역 컨텍스트는 사라진다.

실행 컨텍스트 스택

코드가 실행 될 때, 실행 컨텍스트 스택(Stack) 이 생성하고 소멸한다.

현재 실행 중인 컨텍스트에서 관련없는 코드(예를 들어, 다른 함수)가 실행되면 새로운 컨텍스트가 생성된다.

var global = "global"; function foo() { var local1 = "local1"; function bar() { var local2 = "local2"; console.log(local1, local2, global); //local1 local2 global } bar(); } foo();

JavaScript

변수 객체(Variable Object)

실행 컨텍스트가 생성되면 자바스크립트 엔진은 실행에 필요한 여러 정보들을 담을 객체를 생성한다. 이를 Variable Object(VO / 변수 객체) 라고 한다.

변수 객체는 arguments(인수 정보)variable(스코프의 변수) 을 담고 있고, 전역 컨텍스의 경우와 함수 컨텍스트의 경우에 가리키는 객체가 다르다.

전역 컨텍스트

전역 컨텍스트의 경우, 변수 객체는 arguments를 가지지 않는다.

그리고 변수 객체는 모든 전역 변수, 전역 함수 등을 포함하는 전역 객체(Global Object / GO)를 가리킨다.

전역 객체는 전역 변수와 전역 함수를 프로퍼티로 가진다.

JavaScript

함수 컨텍스트

함수 컨텍스트의 경우, 변수 객체는 Activation Object(AO / 활성 객체)를 가리킨다.

또한, 전역 컨텍스트와 다르게 매개변수와 인수들의 정보를 배열의 형태로 담고 있는 유사 배열 객체 arguments도 가진다.

JavaScript

스코프 체인(Scope Chain)

스코프 체인은 현재 컨텍스트의 유효 범위를 나타내는 스코프 정보를 담고 있으며, 연결 리스트의 형태와 유사하게 생성된다.

이 리스트를 이용해 현재 컨텍스트의 변수와 상위 실행 컨텍스트의 변수에도 접근할 수 있다.

이 리스트는 현재 실행 컨텍스트의 활성 객체를 먼저 가리키고 순차적으로 상위 컨텍스트의 활성 객체를 가리키고 마지막으로 전역 객체를 가리킨다.

JavaScript

즉, 스코프 체인은 식별자 중 변수를 검색하는 것을 말하고, 변수가 아닌 객체의 프로퍼티를 검색하는 것을 프로토타입 체인이라고 한다.


🙏 Reference

클로저(Closure)

클로저는 실행 컨텍스트와 밀접한 관련이 있다.

생성된 함수 객체는 [[Scopes]] 프로퍼티를 가지게 된다.

[[Scopes]] 프로퍼티는 함수 객체만이 소유하는 내부 프로퍼티(Internal Property)로서 현재 실행 컨텍스트의 스코프 체인이 참조하고 있는 객체를 값으로 설정한다.

JavaScript

내부 함수의 [[Scopes]] 프로퍼티는 자신의 실행 환경(Lexical Enviroment) 과 자신을 포함하는 외부 함수의 실행 환경과 전역 객체를 가리킨다.

이 때, 자신을 포함하는 외부 함수의 실행 컨텍스트가 소멸하여도 [[Scopes]]프로퍼티가 가리키는 외부 함수의 실행 환경(Activation Object)은 소멸하지 않고 참조할 수 있다. 이것이 클로저이다.


외부함수에서 내부함수를 반환하는 코드를 보자.

function foo() { var x = "variable of outerFunc"; function bar() { console.log(x); } return bar; } var innerFunc = foo(); innerFunc(); //variable of outerFunc

위의 코드를 보면 외부함수 foo()에서 bar()를 반환하고 소멸한다.

외부함수 foo()는 실행된 이후, 실행 컨텍스트 스택에서 제거되기 때문에 변수 x도 같이 소멸될 것으로 보인다. 이에 따라 변수 x에 접근할 방법이 없어 보인다. 하지만 innerFunc()함수를 호출하면 변수 x의 값이 출력되는 것을 볼 수 있다.

이처럼 클로저는 외부함수(foo()) 밖에서 내부함수(bar())가 호출되더라도 외부함수의 지역 변수(var x)에 접근할 수 있다.

JavaScript

클로저가 외부함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는 이유를 설명한 그림이다.

외부함수인 foo()함수가 종료되면 함수 실행 컨텍스트도 소멸하지만 foo() 함수 실행 컨텍스트의 활성 객체는 유효하다.

이 때문에 외부 함수 foo()가 실행이 종료되어도 내부 함수 bar()에서 접근이 가능한 것이다.

클로저를 사용하면 클로저에서의 스코프 체인 접근 방식, 메모리의 부담 등의 이유로 성능적인 면과 자원적인 면에서 손해를 볼 수 있다.
그렇기 때문에 좋은 구현을 위해서는 충분한 경험을 쌓을 필요가 있다.

클로저를 활용한 전역 변수의 사용 억제

클로저를 활용한 대표적인 예로 카운터가 있다. 우선, 전역 변수를 사용한 예를 한 번 살펴보자.

var counter = 0; function calculator() { return console.log(++counter); } calculator(); //1 calculator(); //2 calculator(); //3

위의 결과는 에상대로 잘 나오고 있지만 전역 변수 counter를 쓰고 있다.
전역 변수는 어디서든 접근이 가능하기 때문에 값이 변할 수 있고 이에 따라 오류를 불러올 수 있다.

var outerFunc = (function() { var counter = 0; function calculater() { return console.log(++counter); } return calculater; })(); outerFunc(); //1 outerFunc(); //2 outerFunc(); //3

위의 코드와 같이 클로저를 이용하면 전역 변수의 사용을 줄일 수 있다.

루프 안에서의 클로저 활용

클로저를 활용하는데 있어 주의할 사항에 대해 설명할 때 가장 많이 등장하는게 이 경우다.

function count(numberOfCount) { for (var i = 1; i <= numberOfCount; i++) { setTimeout(function() { console.log(i); }, i * 1000); } } count(4);

보면 알겠지만, 위 코드의 의도는 1초 간격으로 1,2,3,4를 출력하는 것이다. 하지만 결과는 예상과 다르게 5가 4번 1초 간격으로 출력된다.

그 이유는 변수 i는 외부함수의 변수가 아닌 전역변수이고 setTimeout()함수가 실행되는 시점은 count()함수가 종료된 이후다. 이 때는 이미 i의 값이 5인 상태이다.

function count(numberOfCount) { for (var i = 1; i <= numberOfCount; i++) { (function(j) { setTimeout(function() { console.log(j); }, i * 1000); })(i); } } count(4);

즉시 실행 함수를 실행시켜 루프의 i 값을 j에 복사하고 setTimeout()함수에서 사용했다.
이 때 j는 상위스코프의 자유변수이므로 그 값이 유지된다.

이러한 문제는 자바스크립트의 함수형 스코프로 인해 for 루프의 초기문에서 사용된 변수는 전역 스코프를 가지기 때문에 발생한다.

ES6에서는 let을 이용해 블록 레벨 스코프를 구현할 수 있다.

function count(numberOfCount) { for (let i = 1; i <= numberOfCount; i++) { setTimeout(function() { console.log(i); }, i * 1000); } } count(4);

첫 번째 코드에서 varlet으로만 바꿔주면 위의 코드처럼 깔끔하게 구현할 수 있다.


🙏 Reference

템플릿 리터럴(Template Literal)

ES6(ECMA Script2015)에서는 새로운 문자열 표기법이 도입됐는데 이것이 템플릿 리터럴이다.

템플릿 리터럴을 사용하면 기존 문자열 표기 방법의 불편함을 어느 정도 해결할 수 있다.

  • 템플릿 리터럴은 `(백틱)을 사용한다.

  • 이스퀘이프 시퀀스를 사용하지 않아도 템플릿 리터럴 내의 white space가 그대로 인식된다.

//ES5 var str = "Hello.\n My Name is BKJang. \n I'm developer."; //ES6 let templateStr = `Hello. May Name is BKJang I'm developer.`; console.log(templateStr); /* Hello. May Name is BKJang I'm developer. */
  • 여러 개의 문자열을 연결할 때는 +연산자를 사용하지 않고 String Interpolation(문자열 인터폴레이션)을 사용한다.
let str1 = "Hello"; let str2 = "World"; let id = "bkjang"; //ES5 console.log(str1 + " " + str2); //Hello World //ES6 console.log(`${str1} ${str2}`); //Hello World let url = `http://localhost:3000/api/user/${id}`; console.log(url); //http://localhost:3000/api/user/bkjang
console.log("1 더하기 2 는 " + (1 + 2) + " 입니다,"); console.log(`1 더하기 2 는 ${1 + 2} 입니다.`);

🙏 Reference

화살표 함수(Arrow Function)

화살표 함수는 function 대신 =>를 사용함으로써 좀 더 간결하게 함수를 선언할 수 있다.

또한 화살표 함수는 익명 함수로만 사용할 수 있기 때문에 함수 표현식을 사용한다.

const foo = () => {...} //매개변수가 없을 때 const foo = x => {...} //매개변수가 하나일 때 const foo = (x, y) => {...} //매개변수가 여러 개일 때 const foo = x => { return x; } const foo = x => x; // 함수의 블록이 한줄이라면 중괄호와 return을 생략 const sum = (x, y) => { return x + y; } console.log(sum(1, 2)); //3
//ES5 var arr = ["JS", "Java", "Go"]; var foo = arr.map(function(element) { return { Lang: element }; }); console.log(foo); //[{Lang: "JS"}, {Lang: "Java"}, {Lang: "Go"}] //ES6 const arr = ["JS", "Java", "Go"]; const foo = arr.map(element => { return { Lang: element }; }); console.log(foo); //[{Lang: "JS"}, {Lang: "Java"}, {Lang: "Go"}]

화살표 함수에서의 this 바인딩

ES5에서는 함수의 호출 방식에 따라 this가 동적으로 바인딩이 이뤄진다.

ES5의 this 바인

하지만 화살표 함수에서는 this가 무조건 상위 스코프의 this를 가리킨다.

즉 정적인 방식으로 this가 바인딩이 되는데 이를 Lexical this 라고 한다.

위의 소스를 화살표 함수를 이용해서 바꾸면 var that = this;라는 구문을 쓸 필요가 없다.

const lang = "Korean"; const obj = { lang: "English", outerFunc() { //ES6의 축약 메서드 표현 console.log("outerFunc : ", this.lang); innerFunc1 = () => { console.log("innerFunc1 : ", this.lang); innerFunc2 = () => { console.log("innerFunc2 : ", this.lang); }; innerFunc2(); }; innerFunc1(); } }; obj.outerFunc(); /* outerFunc : English innerFunc1 : English innerFunc2 : Engilsh */

항상 화살표 함수일까?

화살표 함수는 위에서 본 것 처럼 정적으로 this를 바인딩(Lexical this)를 지원하기 때문에 콜백 함수로 쓰기 매우 편하다. 하지만, 주의해서 써야할 경우가 몇 가지 있다.

addEventListener의 콜백 함수 선언

const btn = document.getElementById("submitBtn"); btn.addEventListener("click", () => { console.log(this); //Window });

위에서 볼 수 있듯이 addEventListener콜백 함수를 화살표 함수로 선언하면 thiswindow에 바인딩되므로 그냥 일반함수를 사용하여 선언하여야 한다.

객체의 메서드에 선언

객체의 메서드를 선언할 때, 화살표 함수를 쓰면 그 객체에 this가 바인딩되지 않고 전역(window)에 바인딩된다.

var lang = "Korean"; const obj = { lang: "English", foo: () => { console.log("outerFunc : ", this.lang); } }; obj.foo(); //outerFunc : Korean

위에선 English가 나온다고 생각할 수도 있다.
하지만, foo 선언할 때 화살표 함수를 사용했고 이에 따라 상위 컨텍스트인 windowthis가 바인딩 되기 때문에 결과는 Korean이 나온다.

따라서 원하는 결과가 English라면 축약 메서드 표현법으로 정의하는 것이 맞다.

var lang = "Korean"; const obj = { lang: "English", foo() { console.log("outerFunc : ", this.lang); } }; obj.foo(); //outerFunc : English

프로토타입 객체의 메서드 선언

프로토타입 객체에 메서드를 화살표 함수로 정의하면 객체의 메서드에 화살표 함수로 선언할 때와 같이 thiswindow에 바인딩된다.

const devleoper = { name: "BKJang" }; Object.prototype.renderData = () => console.log(this.name); devleoper.renderData(); //undefined

생성자 함수 선언

결론부터 말하면, 화살표 함수는 prototype 프로퍼티를 갖고 있지 않다. 따라서 생성자 함수를 선언할 때 화살표 함수를 쓰면 인스턴스를 생성할 수 없다.

const Person = name => { this.name = name; }; const jang = new Person("BKJang"); //Uncaught TypeError: Person is not a constructor

Spread연산자 & rest파라미터

rest 파라미터

rest 파라미터는 Spread 연산자(...)를 사용하여 파라미터를 정의한다. 기존에 ES5에서는 가변 파라미터를 정의할 때 arguments객체를 사용할 수 있었지만 이를 rest 파라미터로 대체할 수 있다.

//ES5 function foo() { console.log(Array.isArray(arguments)); console.log(arguments); } foo(1, 2, 3, 4, 5); /* false {'0': 1, '1': 2, '2': 3, '3': 4, '4': 5, length: 5} */
//ES6 function foo(...rest) { console.log(Array.isArray(rest)); console.log(rest); } foo(1, 2, 3, 4, 5); /* true [1, 2, 3, 4, 5] */

위의 결과를 보면 arguments 객체와 rest 파라미터의 중요한 차이를 알 수 있다.

arguments객체는 유사배열 객체다. 따라서 Array.isArray(arguments)의 결과는 false를 반환한다.

여기서 큰 차이가 발생하는데 유사배열객체는 배열의 메서드를 사용할 수 없다. 배열의 메서드를 사용하기 위해서는 이를 배열로 변환하는 과정을 거쳐야하는 불편함이 있다.
하지만 가변 인자 함수를 rest 파라미터로 정의하면 파라미터는 배열의 형태로 넘어온다.

또한 ES6의 화살표 함수(Arrow Function)에서는 arguments를 사용할 수 없다.

//ES6 화살표 함수 const foo = () => { console.log(arguments); }; foo(1, 2, 3, 4, 5); //Uncaught ReferenceError: arguments is not defined
  • rest 파라미터는 arguments와 달리 배열로 파라미터가 넘어온다.
  • ES6의 화살표 함수에서는 arguments를 사용할 수 없다.

Spread 연산자

Spread 연산자는 말그대로 전개 연산자다. 배열 또는 Iterable object(반복 가능한 객체)의 엘리먼트를 하나씩 분리하여 전개한다.

let a = "Hello"; let arr = [...a]; console.log(arr); //["H", "e", "l", "l", "o"]

Spread 연산자의 활용

  • concat

ES5에서는 배열을 합칠 때 concat을 사용했었다. 이를 Spread연산자로 대체할 수 있다.

//concat var arr1 = [5, 6]; var arr2 = [1, 2, 3, 4]; console.log(arr2.concat(arr1)); //[1, 2, 3, 4, 5, 6] console.log(arr1.concat(arr2)); //[5, 6, 1, 2, 3, 4]
//ES6 Spread Operator let arr1 = [5, 6]; let arr2 = [1, 2, 3, 4, ...arr1]; console.log(arr2); //[1, 2, 3, 4, 5, 6] console.log([1, 2, ...arr1, 3, 4]); //[1, 2, 5, 6, 3, 4]

단순히 앞, 뒤에 배열의 요소를 붙이는데는 concat이 성능이 더 좋다. 하지만 중간에 특정 배열의 값을 추가하고 싶다면 Spread연산자를 사용하는 것도 좋은 방법이다.

  • split

문자열을 배열로 변환할 때 많이 쓰이는 함수가 split()이다. 이 또한 Spread 연산자를 활용하면 좀 더 편하게 변환할 수 있다.

//split var a = "Hello"; var arr = a.split(""); console.log(arr); //["H", "e", "l", "l", "o"]
//ES6 Spread Operator let a = "Hello"; let arr = [...a]; console.log(arr); //["H", "e", "l", "l", "o"]
  • 함수의 인자로 사용

기존에 ES5에서는 배열의 각 요소를 개별적인 파라미터로 전달하고 싶은 경우, Function.prototype.apply를 사용하는 것이 일반적이었다.
하지만 ES6의 Spread 연산자를 활용하여 함수의 인자에 들어가는 배열을 개별요소로 전달할 수 있다.

//ES5 apply var arr = [1, 2, 3]; function sum(a, b, c) { console.log(a, b, c); //1 2 3 return a + b + c; } console.log(sum.apply(null, arr)); //6
//ES6 Spread Operator let arr = [1, 2, 3]; const sum = (a, b, c) => a + b + c; console.log(sum(...arr)); //6
  • 객체에서 사용

객체는 Iterable Object아니지만 Spread 연산자를 사용하면 객체를 손쉽게 병합 또는 변경할 수 있다.

let obj1 = { name: "BKJang", job: "Developer" }; let obj2 = { ...obj1, lang: "Korean" }; console.log(obj2); //{name: "BKJang", job: "Developer", lang: "Korean"}

Spread 연산자를 활용하면 유사배열객체(arguments, HTMLCollection 등)를 배열로 변환하기도 편하다.

function foo() { let args = arguments; let arr = [...args]; console.log(Array.isArray(args)); //false console.log(Array.isArray(arr)); //true console.log(arr); // } foo(1, 2, 3, 4, 5); //[1, 2, 3, 4, 5]

🙏 Reference

Destructuring(비구조화 할당)

디스트럭처링은 구조화된 배열 혹은 객체를 분해하여 변수에 할당하는 방식이다. 이 개념을 몰랐더라도 React를 사용해봤던 개발자라면 아마 많이 봤을 문법이다.

const { state } = this.props;

오른쪽의 특정 값을 해체하여 왼쪽에 할당하는 표현식을 **Destructuring Assignment**라고 한다.

배열 디스트럭처링

//ES5 var arr = ["JS", "Java", "Node.js"]; var x = arr[0]; var y = arr[1]; var z = arr[2]; console.log(x, y, z); //JS Java Node.js
//ES6 const arr = ["JS", "Java", "Node.js"]; let [x, y, z] = arr; console.log(x, y, z); //JS Java Node.js
const numArr = [1, 2, 3, 4]; let [x, y, , z] = numArr; console.log(x, y, z); //1 2 4

위의 결과를 보면 알 수 있듯이 배열을 디스트럭처링하면 각각의 변수에 배열의 index를 기준으로 할당된다.

디스트럭처링을 사용했을 때 편한 대표적인 예는 변수의 swap처리를 할 때다.

//ES5(For Swap) var x = 1; var y = 2; var tmp = y; console.log(x, y); //1 2 y = x; x = tmp; console.log(x, y); //2 1
//ES6 let x = 1; let y = 2; console.log(x, y); //1 2 [x, y] = [y, x]; console.log(x, y); //2 1

객체 디스트럭처링

객체 또한 디스트럭처링이 가능하며 배열과 크게 다르지 않다.

//ES5 var obj = { name: "BKJang", lang: "Korean", job: "Developer" }; var name = obj.name; var lang = obj.lang; var job = obj.job; console.log(name, lang, job); //BKJang Korean Developer
//ES6 const obj = { name: "BKJang", lang: "Korean", job: "Developer" }; let { name, lang, job } = obj; console.log(name, lang, job); //BKJang Korean Developer

만약 변수 명을 다르게 하고 싶다면 다음과 같이 처리하면 된다.

var obj = { a: 1, b: "hello" }; var { a: key, b: value } = obj; console.log(key, value); // 1, 'hello'

중첩 객체의 경우에는 아래와 같이 사용한다.

const developer = { name: "BKJang", stack: { front: "HTML / CSS / JS", back: "Java / Node.js" } }; const { name, stack: { front } } = developer; console.log(name, front); //BKJang HTML / CSS / JS

디스트럭처링을 사용하면 **기본 값(Default Value)**이나 **기본 파라미터(Default Parameter)**를 세팅할 수 있고, Speread Operator또한 사용할 수도 있다.

Spread Operator 용

const arr = [1, 2, 3, 4]; let [x, y, ...z] = arr; console.log(x, y, z); //1 2 [3, 4]
const obj = { one: 1, two: 2, three: 3, four: 4 }; let { one, two, ...rest } = obj; console.log(one, two, rest); //1 2 {three: 3, four: 4}

기본 값(Default Value)

const arr = [1, 2]; let [x, y, z = 3] = arr; console.log(x, y, z); //1 2 3
const obj = { one: 1, two: 2 }; let { one, two, three = 3 } = obj; console.log(one, two, three); //1 2 3

기본 파라미터(Default Parameter)

const doSomething = (name, stack = "FrontEnd") => { stack = stack === null ? "FullStack" : stack; console.log(`${name}${stack}개발자입니다.`); }; doSomething("BKJang"); //BKJang은 FrontEnd개발자입니다. doSomething("BKJang", "BackEnd"); //BKJang은 BackEnd개발자입니다. doSomething("BKJang", undefined); //BKJang은 FrontEnd개발자입니다. doSomething("BKJang", null); //BKJang은 FullStack개발자입니다. //Default Parameter는 함수의 length에 포함되지 않는다. console.log(doSomething.length); //1

자바스크립트에서 함수는 length프로퍼티를 가지는데 인자의 갯수를 나타낸다. Default Parameter는 이 length프로퍼티에 포함되지 않는다.


🙏 Reference

클래스(class)

ES6에서부터는 ES5까지는 존재하지 않았던 class가 생겨났다. Java에서의 Class와는 똑같은 기능을 한다고 생각해서는 안된다. 자바스크립트는 기본적으로 Prototype기반의 객체지향 언어다. 즉, ES6Class또한 프로토타입을 기반으로 동작하며 이는 기존의 자바스크립트에서 객체지향적으로 설계할 때의 방식을 좀 더 편하게 보완한 Syntatic Sugar다.

클래스의 정의

//ES5 var Person = (function() { //생성자 함수 정의 function Person(name, job) { this.name = name; this.job = job; } Person.prototype.sayInfo = function() { console.log("Name : " + this.name + ", Job : " + this.job); }; return Person; })(); var bkJang = new Person("BKJang", "Developer"); bkJang.sayInfo(); //Name : BKJang, Job : Developer

ES6에서 Class가 생기기 전 우리는 위와 같은 방식으로 생성자 함수와 프로토타입을 이용해 객체지향 프로그래밍을 진행했었다. 위와 같은 코드를 ES6Class를 사용하여 구현하면 아래와 같이 좀 더 간결하게 구현할 수 있다.

//ES6 class Person { constructor(name, job) { this.name = name; this.job = job; } sayInfo() { console.log(`Name : ${this.name}, Job : ${this.job}`); } } const bkJang = new Person("BKJang", "Developer"); bkJang.sayInfo(); //Name : BKJang, Job : Developer

클래스는 기본적으로 위와 같이 Class Person {}l으로 정의하며, 흔치는 않지만 const Person = class MyClass {};처럼 함수 표현식으로도 정의 가능하다.

둘의 또 다른 차이점은 생성자 함수를 이용하여 선언하면 window에 할당되지만, Class를 이용하여 선언하면 window에 할당되지 않는다.

또한 Class 안에 있는 코드는 항상 strict mode 로 실행되기 때문에 "use strict" 명령어가 없더라도 동일하게 동작한다.

function Person() {} class Developer {} console.log(window.Person); //ƒ Person() {} console.log(window.Developer); //undefined

인스턴스의 생성과 호이스팅

Class를 사용하여 인스턴스를 생성할 때는 반드시 new를 이용해 호출해야하며 new를 사용하지 않으면 Type Error가 발생한다.

class Foo {} const foo = Foo(); //Uncaught TypeError: Class constructor Foo cannot be invoked without 'new'

ES6Classlet, const와 마찬가지로 호이스팅이 일어나지만, 선언이 일어나고 할당이 이뤄지기 전 TDZ(Temporary Dead Zone)에 빠지기 때문에 할당 이전에 호출하면 Reference Error가 발생한다.

const foo = new Foo(); //Uncaught ReferenceError: Foo is not defined class Foo {}

constructor

constructor는 인스턴스를 생성하고 Classproperty를 초기화한다. ES5에서는 생성자 함수를 이용해 property를 초기화하고 생성자 함수를 반환함으로써 객체지향을 구현했었다.

  • class는 constructor를 반환하며 생략할 수 있다.

const foo = new Foo()와 같이 선언했을 때 Fooclass명이 아닌 constructor다.

class Foo {} const foo = new Foo(); console.log(Foo == Foo.prototype.constructor); //true

위의 코드에서 볼 수 있듯이 new와 함께 호출한 Fooconstructor와 같음을 확인할 수 있다.

또 확인할 수 있는 것은 class Foo내부에 constructor를 선언하지 않았음에도 인스턴스의 생성이 잘 이뤄지는 것을 볼 수 있다. 이는 Class내부에 constructor는 생략할 수 있으며 생략하면 Classconstructor() {}를 포함한 것과 동일하게 동작하기 때문이다. 즉, 빈 객체를 생성하기 때문에 property를 선언하려면 인스턴스를 생성한 이후, property를 동적 할당해야 한다.

console.log(foo); //Foo {} foo.name = "BKJang"; console.log(foo); //Foo {name: "BKJang"}
  • class의 property는 constructor 내부에서 선언과 초기화가 이뤄진다.

class의 몸체에는 메서드만 선언 가능하며, propertyconstructor내부에서 선언하여야 한다.

class Foo { name = ""; //Syntax Error }
class Bar { constructor(name = "") { this.name = name; } } const bar = new Bar("BKJang"); console.log(bar); //Bar {name: "BKJang"}

constructor내부에서 선언한 propertyClass의 인스턴스를 가리키는 this에 바인딩 된다.

getter, setter

class의 프로퍼티에 접근하기 위한 인터페이스로서, gettersetter를 정의할 수 있다.

class Person { constructor(name) { this.name = name; } //getter get personName() { return this.name ? this.name : null; } //setter set personName(name) { this.name = name; } } const person = new Person("BKJang"); console.log(person.personName); //BKJang person.personName = "SHJo"; console.log(person.personName); //SHJo

Static 메서드

class에서는 정적 메서드를 정의할 때, static 키워드를 사용하여 정의한다. 정적 메서드는 인스턴스를 생성하지 않아도 호출가능하며, 인스턴스가 아닌 Class의 이름으로 호출한다. 이와 같은 특징 때문에 애플리케이션을 위한 유틸리티성 함수를 생성하는데 주로 사용한다.

또한 정적 메서드 내부에서는 this를 사용할 수 없다. 왜냐하면 정적 메서드 내부에서는 thisClass의 인스턴스가 아닌 Class자기 자신을 가리키기 때문이다.

class Person { constructor(name) { this.name = name; } //getter get personName() { return this.name ? this.name : null; } //setter set personName(name) { this.name = name; } static staticMethod() { console.log(this); return "This is static"; } } console.log(Person.staticMethod()); /* class Person { ... } This is static */ const instance = new Person("BKJang"); console.log(instance.staticMethod()); //Uncaught TypeError: instance.staticMethod is not a function

위에서 볼 수 있듯이 인스턴스로는 Class의 정적 메서드를 호출할 수 없다.

또한 정적 메서드는 prototype에 추가되지 않는다.

console.log(Person.staticMethod === Person.prototype.staticMethod); //false console.log(new Person().personName === Person.prototype.personName); //true

클래스의 상속

class를 이용하여 OOP의 특징 중 하나인 상속을 구현할 수 있다. class의 상속을 위해서는 extendssuper 키워드에 대해서 알아야 한다.

class Person { constructor(name, sex) { this.name = name; this.sex = sex; } getInfo() { return `Name : ${this.name}, Sex : ${this.sex}`; } getName() { return `Name : ${this.name}`; } getSex() { return `Sex : ${this.sex}`; } } class Developer extends Person { //extends를 사용하여 Person 클래스 상속 constructor(name, sex, job) { //super메서드를 사용하여 부모 클래스의 인스턴스를 생성 super(name, sex); this.job = job; } //오버라이딩 getInfo() { //super 키워드를 사용하여 부모 클래스에 대한 참조 return `${super.getInfo()} , Job: ${this.job}`; } getJob() { return `Job : ${this.job}`; } } const person = new Person("SHJo", "Male"); const developer = new Developer("BKJang", "Male", "Developer"); console.log(person); //Person {name: "SHJo", sex: "Male"} console.log(developer); //Developer {name: "BKJang", sex: "Male", job: "Developer"} console.log(person.getInfo()); //Name : SHJo, Sex : Male console.log(developer.getName()); //Name : BKJang console.log(developer.getSex()); //Sex : Male console.log(developer.getJob()); //Job : Developer console.log(developer.getInfo()); //Name : BKJang, Sex : Male , Job: Developer console.log(developer instanceof Developer); //true console.log(developer instanceof Person); //true

위의 소스를 기준으로 중요한 특징을 정리하자면 다음과 같다.(대부분의 객체 지향 언어에서 상속의 특징과 거의 동일하다.)

  • 부모 클래스(슈퍼 클래스)의 메서드를 사용할 수 있다.

  • 부모 클래스의 메서드를 오버라이딩(Overriding)할 수 있다.

  • super 키워드를 통해 부모 클래스의 메서드에 접근할 수 있다.

  • super 메서드(위의 Developer 클래스의 constructor내부에 선언)는 자식 클래스의 constructor 내부에서 부모 클래스의 constructor(super-constructor)를 호출한다.


🙏 Reference

모듈(Module)

자바스크립트(ES5)에서 기본적으로 모듈 기능이 없다. 기본적으로 자바스크립트에서 변수는 전역(global)에 할당되기 때문에 모듈을 구현하기 위해서는 Namespace Pattern 혹은 Module Pattern과 같은 기법이 필요했다.

이러한 상황에서 클라이언트 사이드에서뿐만이 아니라 범용적인 사용이 일어나면서 모듈화의 필요성이 대두되었다. 이에 따라 CommonJS와 AMD 이렇게 2개의 진영으로 나뉘게 되었다. 우리가 잘 알고 있는 Node.jsCommonJS의 모듈화 방식을 따르고 있다.

ES6에서는 클라이언트 사이드에서도 모듈화를 제공하기 위해 exportimport가 추가되었다. 단, ES6의 모듈 기능은 대부분의 브라우저에서는 지원하지 않기 때문에 RequireJS와 같은 모듈 로더나 Webpack과 같은 모듈 번들러와 함께 babel과 같은 트랜스파일러를 사용하여야 한다.

export

ES6의 모듈은 보통 파일 단위로 구성되며 독립적인 파일 스코프를 가지기 때문에 외부에서 모듈의 기능을 사용하고 싶다면 위와 같이 export를 해줘야한다.

//module.js export const message = "this is variable"; export function sayHello() { console.log("Hello World"); } export function sayName(name) { console.log(`Hi ${name}`); } export class Person { constructor(name, job) { this.name = name; this.job = job; } }

위와 같이 각각의 변수, 함수, 클래스에 export키워드를 붙여 export할 수 있고 아래와 같이 하나의 객체로 묶어 한 번에 export할 수도 있다.

//module.js const message = "this is variable"; function sayHello() { console.log("Hello World"); } function sayName(name) { console.log(`Hi ${name}`); } class Person { constructor(name, job) { this.name = name; this.job = job; } } export { message, sayHello, sayName, Person };

import

ES6에서 export한 모듈을 사용하기 위해서는 해당 파일에서 import키워드를 사용하여 가져와 쓰면 된다.

import { message, sayHello, sayName, Person } from "./module"; console.log(message); //this is variable console.log(sayHello()); //Hello World console.log(sayName("BKJang")); //Hi BKJang console.log(new Person("BKJang", "Developer")); //Person { name : BKJang, job: Developer }

위와 같이 각각를 import하지 않고 한꺼번에 import하거나 이름을 변경하여 import 할 수도 있다.

//한꺼번에 묶어서 import import * as module from "./module"; console.log(module.sayName("BKJang")); //Hi BKJang
//이름을 변경하여 import import { sayHello as hello } from "./module"; console.log(hello()); //Hello World

default

모듈에서 하나만 export할 경우에는 default키워드를 사용하면 된다.

//Person.js export default class Person { constructor(name) { this.name = name; } }
//Person.js class Person { //... } export default

이를 import할 때는 {}없이 해당 모듈을 임의의 이름으로 가져와 사용하면 된다.

import Person from "./Person";

🙏 Reference

Promise와 async-await

Promise

이전에는 비동기 작업을 처리 할 때에는 콜백 함수로 처리를 해야 했다. 콜백 함수로 처리를 하게 된다면 비동기 작업이 많아질 경우 코드의 가독성이 떨어지게 되었다. 이를 콜백 지옥이라고 한다.

Promise는 이러한 콜백 지옥에서 탈출하기 위해 ES6에서 도입된 문법이다.

const printNumber = number => { return new Promise((resolve, reject) => { setTimeout(() => { const value = number + 1; if (value === 4) { const error = new Error("This is Error!"); reject(error); return; } console.log(value); resolve(value); }, 1000); }); }; printNumber(0) .then(n => { return printNumber(n); }) .then(n => { return printNumber(n); }) .then(n => { return printNumber(n); }) .then(n => { return printNumber(n); }) .catch(error => { console.log(error); });

Promise에서는 resolvereject를 파라미터로 전달하고 성공시에는 resolve를 실패시에는 reject를 실행한다. 다만, Promise를 사용했을 때는 어떤 부분에서 error를 잡아야할지가 조금 애매할 수 있고 조건 분기를 해줄 때 불편함이 있다. 이러한 점을 보완한 것이 async-await 문법이다.

Promise.all()

Promise.all()은 여러 개의 비동기적 처리를 한 번에 처리하고 싶을 때 사용한다. 단, 모든 비동기 함수가 처리가 완료되면 결과를 출력한다. 따라서 비동기 함수 중 하나라도 에러가 발생한다면 결과 값은 에러가 된다.

const myPromise = time => { return new Promise(resolve => setTimeout(resolve, time)); }; const printHello = async () => { await myPromise(100); return "Hello"; }; const printName = async () => { await myPromise(500); return "BKJang"; }; const printWorld = async () => { await myPromise(300); return "world"; }; const printError = async () => { await myPromise(600); throw new Error("This is Error!"); }; const makeProcess = async () => { try { const result = await Promise.all([printHello(), printName(), printWorld()]); console.log(result); } catch (error) { console.error(error); } }; const makeProcessWithError = async () => { try { const result = await Promise.all([ printHello(), printName(), printWorld(), printError() ]); console.log(result); } catch (error) { console.error(error); } }; makeProcess(); //['Hello', 'BKJang', 'world'] makeProcessWithError(); //Error : This is Error!

Promise.race()

Promise.race()Promise.all()과 마찬가지로 모든 비동기 함수를 동시에 요청하지만 가장 먼저 처리된 비동기 함수의 결과만을 반환한다.

const myPromise = time => { return new Promise(resolve => setTimeout(resolve, time)); }; const printHello = async () => { await myPromise(100); return "Hello"; }; const printName = async () => { await myPromise(500); return "BKJang"; }; const printWorld = async () => { await myPromise(300); return "world"; }; const printError = async () => { await myPromise(600); throw new Error("This is Error!"); }; const makeProcessWithError = async () => { try { const result = await Promise.race([ printHello(), printName(), printWorld(), printError() ]); console.log(result); } catch (error) { console.error(error); } }; makeProcessWithError(); //Hello

위의 코드를 보면 에러를 발생시키는 비동기 함수가 printHello함수보다 늦게 처리되기 때문에 에러가 아닌 Hello가 출력되는 것을 볼 수 있다.

async-await

async-await문법은 ES8에서 새롭게 도입된 문법으로 Promise를 좀 더 편하게 다룰 수 있게 해준다. 또한 try-catch구문을 사용할 수 있기 때문에 에러 처리도 좀 더 간편하게 할 수 있다.

const printNumber = number => { return new Promise((resolve, reject) => { setTimeout(() => { const value = number + 1; if (value === 4) { const error = new Error("This is Error!"); reject(error); return; } console.log(value); resolve(value); }, 1000); }); }; const makeProcess = async () => { try { const num1 = await printNumber(0); const num2 = await printNumber(num1); const num3 = await printNumber(num2); const num4 = await printNumber(num3); } catch (error) { console.error(error); } }; makeProcess();

🙏 Reference

Iteration Protocol

Iteration Protocol은 ES6에서 도입되었다. 이는 새로운 구문이 아닌 하나의 프로토콜, 규약이다. 즉, 같은 규칙을 준수하는 객체에 의해 구현될 수 있다. Iteration Protocol에는 Iterable ProtocolIterator Protocol 이 있다.

Iterable

IterableIterable Protocol을 준수한 객체다. Iterable은 반복 가능한 객체이며 Symbol.iterator 메서드를 구현하거나 프로토타입 체인에 의해 상속한 객체를 말한다. Iterablefor...of문에서 반복 가능하며 Spread Operator의 대상이 될 수 있다.

IterableSymbol.iterator을 가지기 때문에 해당 메서드가 없는 객체는 Iterable 객체가 아니다.

Iterable 객체로는 내장 객체인 Array, Map, Set, String 등이 있다.

const iterator = [1, 2, 3][Symbol.iterator](); iterator.next().value; // 1 iterator.next().value; // 2 iterator.next().value; // 3 iterator.next().done; // true

반면, 일반 객체는 Symbol.iterator 메서드를 가지고 있지 않다. 따라서 일반 객체는 Iterable 객체가 아니다.

const obj = { a: 1, b: 2 }; console.log(Symbol.iterator in obj); // false // TypeError: obj is not iterable for (const item of obj) { console.log(item); }

하지만 일반 객체도 Iterable Protocol을 준수하도록 구현하면 Iterable 객체가 될 수 있다.

const iterableObj = function(max) { let i = 0; return { [Symbol.iterator]() { return { next() { return { value: ++i, done: i === max }; } }; } }; }; const iterator = iterableObj(10); for (let item of iterator) { console.log(item); }

Iterator

Iterator Protocolnext 메서드를 가진다. next 메소드를 호출하면 Iterable 객체를 순회하며 value, done 프로퍼티를 갖는 Iterator Reuslt 객체를 반환한다. 이 규약을 준수한 객체가 Iterator 객체다.

위에서 잠깐 봤던 코드의 일부분을 다시 한 번 살펴보자.

[Symbol.iterator]() { return { next() { return { value: ++i, done: i === max }; } }; }

Symbol.iterator 메서드를 실행하면 next 메서드를 가진 객체를 반환하고 next 메서드를 실행하면 value, done 프로퍼티를 가진 객체를 반환한다. 그렇다.
Iterable 객체가 가진 Symbol.iterator 메서드를 실행하면 Iterator 객체를 반환한다.

const array = [1, 2, 3]; const iterator = array[Symbol.iterator](); console.log("next" in iterator); // true iterator.next().value; // 1 iterator.next().value; // 2 iterator.next().value; // 3 iterator.next(); // { value: undefined, done: true }

Iterator 객체의 next 메서드가 반환하는 Iterator Result 객체의 value 프로퍼티는 Iterable 객체의 값을 반환하고 done 프로퍼티는 Iterable 객체의 반복 완료 여부를 반환한다.

Iteration Protocol이 왜 필요할까?

for...of, Spread Operator, Destructuring, Map/Set constructor등을 데이터 소비자(Data Consumer) 라고 한다.
반면, 배열, 문자열, Map, Set, DOM Data Structure 등과 같은 Iterable 객체는 데이터 공급자(Data Provider) 라고 한다.

만약, 위와 같은 Data Provider인 Iterable 객체들이 각각 다른 방식의 순회 방식을 갖는다면 어떨까? 당연히 효율적이지 못하다.

순회 방식에 대한 하나의 규약을 정해놓고 사용한다면 Data Consumer가 여러 구조의 Iterable을 효율적으로 사용할 수 있을 것이다. 즉, Iteration Protocol은 Data Consumer와 Data Provider를 연결하는 인터페이스 역할을 해주기 때문에 필요하다고 볼 수 있다.


🙏 Reference

Generator

GeneratorES6에서 도입되었으며 Iterable을 생성하는 함수다. Generator를 사용하면 Iteration Protocol을 사용하여 Iterable을 생성하는 방식보다 간편하다.
Iteration Protocol에 대한 자세한 내용은 다음을 참고하길 바란다.

Generator 함수의 정의

Generator 함수는 코드 블록을 한 번에 실행하지 않고 함수 코드 블록의 실행을 중지했다가 필요한 시점에 다시 시작할 수 있는 함수다.
Generator 함수는 function * 키워드를 사용하며 하나 이상의 yield 문을 포함한다.

// 함수 선언문 function* decFunc() { yield 1; } let genObj = decFunc(); // 함수 표현식 const expFunc = function*() { yield 1; }; genObj = expFunc(); // 메서드 const obj = { *objectMethod() { yield 1; } }; genObj = obj.objectMethod(); // 클래스 메서드 class GenClass { *classMethod() { yield 1; } } const genClass = new GenClass(); genObj = genClass.classMethod();

Generator 객체

Generator 함수를 호출하면 코드 블록이 실행되는 것이 아니라 Generator 객체를 반환한다. Generator 객체는 이터러블이면서 동시에 이터레이터다. 따라서 Symbol.iterator를 사용하여 이터레이터를 생성할 필요 없다.

function* counter() { console.log("First"); yield 1; console.log("Second"); yield 2; console.log("Third"); yield 3; console.log("The end"); } const genObj = counter(); console.log(genObj.next()); //{value: 1, done: false} console.log(genObj.next()); //{value: 2, done: false} console.log(genObj.next()); //{value: 3, done: false} console.log(genObj.next()); //{value: undefined, done: true}

Generator 객체는 이터러블이면서 이터레이터이기 때문에 next()메서드를 가지고 있다. 따라서 next() 메서드를 호출하면 yield문까지 실행되고 일시 중지된다. 다시 next() 메서드를 호출하면 다음 yield문을 만날 때까지 실행된 뒤 일시 중지된다.

Generator 객체를 이용한 이터러블 구현

const genObj = (function*() { let i = 0; while (true) { yield ++i; } })(); for (let item of genObj) { if (item === 10) break; console.log(item); }
// Generator 함수에 파라미터 전달 const genObj = function*(max) { let i = 0; while (true) { if (i === max) break; yield ++i; } }; for (let item of genObj(10)) { console.log(item); }
// next 메서드에 파라미터 전달 function* genFunc(n) { let res; res = yield n; console.log(res); res = yield res; console.log(res); res = yield res; console.log(res); return res; } const genObj = genFunc(0); console.log(genObj.next()); console.log(genObj.next(1)); console.log(genObj.next(2)); console.log(genObj.next(3));

Generator를 이용한 비동기 처리

Generator의 진면목은 비동기 프로그래밍에서 볼 수 있다. 함수가 실행 도중에 멈춘다니. 언제 응답이 올지 알 수 없기 때문에, callback을 등록하는 비동기 프로그래밍에 응용하면 callback hell을 탈출할 수 있지 않을까?

function getId(phoneNumber) { // … iterator.next(result); } function getEmail(id) { // … iterator.next(result); } function getName(email) { // … iterator.next(result); } function order(name, menu) { // … iterator.next(result); } function* orderCoffee(phoneNumber) { const id = yield getId(phoneNumber); const email = yield getEmail(id); const name = yield getName(email); const result = yield order(name, "coffee"); return result; } const iterator = orderCoffee("010-1234-1234"); iterator.next();

Generator는 어떻게 구현되어 있을까?

// ES6 function* foo() { yield bar(); } // ES5 Compiled ("use strict"); var _marked = /*#__PURE__*/ regeneratorRuntime.mark(foo); function foo() { return regeneratorRuntime.wrap( function foo$(_context) { while (1) { switch ((_context.prev = _context.next)) { case 0: _context.next = 2; return bar(); case 2: case "end": return _context.stop(); } } }, _marked, this ); }

Genrator는 결국 iterable Protocol를 구현하는 객체이다. 그러나 프로토콜과 관련된 어느것도 보이지 않는다.

대신 regeneratorRuntime이 보인다.

babel에서는 regeneratorRuntime라이브러리를 사용해서 구현을 했다.

코드의 역사를 따라가다 보면 facebook/regenerator repository에 도달하게 된다.

이 라이브러리는 2013년 Node.js v0.11.2에서 generator syntax를 지원하기 위해 만들어 졌으며, Babel에서는 이 라이브러리를 사용하여 generator를 구현하고 있다. 실제 코드를 들여다보면 Symbol과 Iterator를 이용해서 Iterable Protocol을 구현하고 있다.


🙏 Reference

이벤트 루프(Event Loop)

자바스크립트는 싱글쓰레드 기반이다.

자바스크립트를 공부해본 개발자라면 한 번쯤은 자바스크립트는 싱글 쓰레드 기반의 언어다.라는 말을 들어봤을 것이다. 하지만 우리는 실제 웹 애플리케이션에서 여러 개의 작업이 동시에 처리되는 것처럼(비동기적) 느끼는 일이 더 많다. 싱글 쓰레드 기반의 언어에서 즉, 한 번에 하나의 작업만 처리가능한 환경에서 어떻게 많은 작업이 동시에 처리되는 것처럼 느낄 수 있을까? 그 답은 이벤트 루프에 있다.

브라우저 환경을 간단히 표현하면 다음 이미지와 같다.

eventLoop

우선, 위의 그림에서 보여지는 각각에 대해서 살펴본 후, 전체적으로 이벤트 루프가 동작하는 방식을 살펴보도록 하자.

자바스크립트 엔진

Heap

동적으로 생성된 객체 인스턴스는 Heap에 할당이 된다. Heap은 메모리에서 대부분 구조화되지 않은 영역을 나타낸다.

Call Stack(호출 스택)

호출 스택은 이름 그대로 Stack이며 LIFO(Last-In-First-Out)구조를 갖는다. 함수를 호출하면(작업을 요청하면) 작업은 순차적으로 호출 스택에 쌓이고 실행된다. 자바스크립트 엔진은 하나의 스택만 가지고 있기 때문에 하나의 작업이 끝나기 전까지 다른 작업을 수행할 수 없다.

Web APIs

흔히 WebAPI라 불리는 API들은 실행환경에 내장되어 있다.

이것은 자바스크립트에 포함되는 것이 아니다. 즉, 우리는 Web API의 내부는 조작할 수 없으며 호출만 가능하다. 또한 자바스크립트 언어를 사용하는데 있어 강력한 성능을 제공한다.

Web API의 종류는 다음을 참조하면 알 수 있다.

콜백함수

자바스크립트의 싱글 쓰레드 구조에서 비동기성의 이벤트 기반 실행(대표적으로 setTimeout)이나 ajax요청이 필요하다면, 콜백 함수를 큐로 보내고 큐에서는 호출 스택으로 보내 해결하게 된다.

자바스크립트에서는 쓰레드를 통해 병렬처리가 안되기 때문에 콜백 함수의 사용은 필수불가결하게 되는 것이다.

Event Queue(이벤트 큐)

이벤트 큐는 말 그대로 콜백 함수들이 대기하는 Queue이며 FIFO(First-In-First-Out)의 구조를 갖는다. 이벤트 루프는 호출 스택이 비워질 때마다 큐에서 콜백 함수를 호출 스택에 넣어주는 역할을 해준다.

이벤트 루프를 통한 비동기적 처리

이벤트 루프의 역할은 생각보다 단순한다. 호출 스택에 실행 중인 작업이 있는지, 이벤트 큐에 대기 중인 작업이 있는지 반복해서 확인한다. 만약 호출 스택이 비어있다면 이벤트 큐에 있는 작업을 호출 스택으로 옮긴다. 그리고 이 작업을 수행하는 것은 결국 호출 스택이다.

function func1() { console.log("func1"); func2(); } function func2() { setTimeout(function() { console.log("func2"); }, 0); func3(); } function func3() { console.log("func3"); } func1();

위 예제는 이벤트 루프를 설명할 때 가장 많이 사용되는 예제다. 만약, 이벤트 루프가 수행하는 과정이 없고 순차적으로 호출 스택에만 쌓이게 된다면 func1, func2, func3의 순서로 출력될 것이다. 하지만 실제로 위 코드를 실행해보면 func1, func3, func2의 순서로 출력되는 것을 볼 수 있을 것이다. 이런 결과가 나오는 이유는 위에서 설명한 것 처럼 이벤트 큐와 이벤트 루프를 통해 비동기 처리를 수행하는 setTimeout함수가 다른 함수들과 다르게 동작하기 때문이다.

아래 이미지는 위 코드가 실행되는 과정을 보여준다.

event_loop_gif

이미지 출처: https://poiemaweb.com/js-event


위 과정을 순차적으로 정리하면 다음과 같다.

  1. func1함수가 호출되고 이는 호출 스택에 올라가고 console.log('func1')이 실행된다.
  2. func2함수가 호출 스택에 올라가고 setTimout함수를 호출한다.
  3. 호출된 setTimeout함수의 수행은 비동기적 처리를 수행하는 Web API에 넘어간다.
  4. func3함수가 호출 스택에 올라가고 console.log('func3')이 실행된다.
  5. Web API에서 setTimout함수에서 지정한 시간이 지나면 callback함수를 이벤트 큐로 넘긴다.
  6. 작업이 끝난 func3, func2, func1은 순차적으로 호출 스택에서 제거된다.
  7. 이벤트 루프는 호출 스택에 작업 중인 태스크가 없는 것을 확인하고 이벤트 큐에 있는 callback함수를 호출 스택으로 올린다.
  8. 호출 스택에 올라간 callback함수가 실행되면서 console.log('func3')가 실행된다.

위 설명에서 주의 깊게 볼 것은 비동기 함수인 setTimeout함수에 세팅된 시간이 3초라면 3초 후에 콜백 함수를 실행시켜라가 아닌 3초 후에 콜백 함수를 이벤트 큐에 넣어라가 된다는 것이다.

즉, setTimeout 함수는 n초 뒤에 콜백을 단순히 큐에 집어넣는게 끝이다. 코드를 간단히 보자면 아래와 같다.

var eventLoop = []; var event; while (true) { // 틱! if (eventLoop.length > 0) { event = eventLoop.shift(); } try { event(); // 호출스택으로 밀어넣는다 } catch (err) { //... } }

이 큐에 이미 대기번호가 100개가 있다면 func3는 101번째 대기표를 받게 될 것이다. 따라서 setTimeout은 지정한 시간동안은 실행되지 않는 것은 보장할 수 있지만 지정한 시간에 실행되는것은 보장할 수 없다.

while (await messageQueue.nextMessage()) { let message = messageQueue.shift(); message.run(); }

결론적으로, 이벤트 루프는 메시지 큐에 메시지가 더 있는지 확인하는 루프이다.

메시지 큐에 메시지가 있으면 메시지 큐에서 다음 메시지를 제거하고 그 메시지와 연관된 기능을 호출 스택으로 보낸다. 그렇지 않으면 새 메시지가 메시지 대기열에 추가될 때까지 대기를 한다. 이벤트 루프가 자바스크립트에게 비동기를 허용하는 기본 모델이다.

ES6이후의 변화된 비동기 처리와 이벤트 루프

기본적으로 이벤트 루프는 위에서 설명한 내용이 큰 틀이다. 큐와 스택을 감시하며 스택의 작업이 없으면 큐의 작업을 스택에 올린다. 다만, ES6이후에는 몇 가지 비동기적 작업을 수행하는 API들이 추가되었고 이에 따라 약간의 추가된 내용이 있다. 하지만, 전체적인 실행 방식은 동일하며 각각의 비동기 처리에 수행 순서에 초점을 두고 살펴보자.

기존에 살펴보았던 이벤트 큐(Event Queue)를 좀 더 자세히 나눠보면 다음과 같다.

  1. Task Queue : 가장 사람들이 잘 알고 있는 비동기 작업인 setTimeout이 들어가는 큐
  2. Micro Task Queue : ES6에서 추가된 Promise와 ES8의 Async Await(Async Await도 결국 Promise)
  3. AnimationFrame: requestAnimationFrame(rAF)의 콜백 함수가 들어간다.
console.log("script start"); setTimeout(function() { console.log("setTimeout"); }, 0); Promise.resolve().then(function() { console.log("promise1"); }).then(function() { console.log("promise2"); }); requestAnimationFrame(function { console.log("requestAnimationFrame"); }) console.log("script end");

위의 코드를 실행하면 다음과 같은 결과가 출력된다.

script start script end promise1 promise2 requestAnimationFrame setTimeout

즉, 이벤트 큐에서 나눠지는 3가지 영역의 우선 순위는 다음과 같다.

Micro Task Queue => AnimationFrame => Task Queue

기존에 이벤트 루프에 대해서 이해가 된 상태라면 이 내용은 크게 어렵지 않다. 쉽게 보면 비동기 작업을 처리하는 방법이 추가되었고 이에 따라 이벤트 큐에서 내부적으로 처리하는 로직에 약간의 변화가 생겼을 뿐이다. 결국, 정리하면 다음과 같다.

  1. 비동기 작업으로 등록되는 작업은 TaskMicro Task, 그리고 AnimationFrame으로 구분된다.

  2. Micro TaskTask보다 먼저 처리된다.

  3. Micro Task가 처리된 이후 requestAnimationFrame이 호출되고 이후 브라우저 랜더링이 발생한다.


🙏 Reference

이벤트 루프(Event Loop)

자바스크립트는 싱글쓰레드 기반이다.

자바스크립트를 공부해본 개발자라면 한 번쯤은 자바스크립트는 싱글 쓰레드 기반의 언어다.라는 말을 들어봤을 것이다. 하지만 우리는 실제 웹 애플리케이션에서 여러 개의 작업이 동시에 처리되는 것처럼(비동기적) 느끼는 일이 더 많다. 싱글 쓰레드 기반의 언어에서 즉, 한 번에 하나의 작업만 처리가능한 환경에서 어떻게 많은 작업이 동시에 처리되는 것처럼 느낄 수 있을까? 그 답은 이벤트 루프에 있다.

브라우저 환경을 간단히 표현하면 다음 이미지와 같다.

eventLoop

우선, 위의 그림에서 보여지는 각각에 대해서 살펴본 후, 전체적으로 이벤트 루프가 동작하는 방식을 살펴보도록 하자.

자바스크립트 엔진

Heap

동적으로 생성된 객체 인스턴스는 Heap에 할당이 된다. Heap은 메모리에서 대부분 구조화되지 않은 영역을 나타낸다.

Call Stack(호출 스택)

호출 스택은 이름 그대로 Stack이며 LIFO(Last-In-First-Out)구조를 갖는다. 함수를 호출하면(작업을 요청하면) 작업은 순차적으로 호출 스택에 쌓이고 실행된다. 자바스크립트 엔진은 하나의 스택만 가지고 있기 때문에 하나의 작업이 끝나기 전까지 다른 작업을 수행할 수 없다.

Web APIs

흔히 WebAPI라 불리는 API들은 실행환경에 내장되어 있다.

이것은 자바스크립트에 포함되는 것이 아니다. 즉, 우리는 Web API의 내부는 조작할 수 없으며 호출만 가능하다. 또한 자바스크립트 언어를 사용하는데 있어 강력한 성능을 제공한다.

Web API의 종류는 다음을 참조하면 알 수 있다.

콜백함수

자바스크립트의 싱글 쓰레드 구조에서 비동기성의 이벤트 기반 실행(대표적으로 setTimeout)이나 ajax요청이 필요하다면, 콜백 함수를 큐로 보내고 큐에서는 호출 스택으로 보내 해결하게 된다.

자바스크립트에서는 쓰레드를 통해 병렬처리가 안되기 때문에 콜백 함수의 사용은 필수불가결하게 되는 것이다.

Event Queue(이벤트 큐)

이벤트 큐는 말 그대로 콜백 함수들이 대기하는 Queue이며 FIFO(First-In-First-Out)의 구조를 갖는다. 이벤트 루프는 호출 스택이 비워질 때마다 큐에서 콜백 함수를 호출 스택에 넣어주는 역할을 해준다.

이벤트 루프를 통한 비동기적 처리

이벤트 루프의 역할은 생각보다 단순한다. 호출 스택에 실행 중인 작업이 있는지, 이벤트 큐에 대기 중인 작업이 있는지 반복해서 확인한다. 만약 호출 스택이 비어있다면 이벤트 큐에 있는 작업을 호출 스택으로 옮긴다. 그리고 이 작업을 수행하는 것은 결국 호출 스택이다.

function func1() { console.log("func1"); func2(); } function func2() { setTimeout(function() { console.log("func2"); }, 0); func3(); } function func3() { console.log("func3"); } func1();

위 예제는 이벤트 루프를 설명할 때 가장 많이 사용되는 예제다. 만약, 이벤트 루프가 수행하는 과정이 없고 순차적으로 호출 스택에만 쌓이게 된다면 func1, func2, func3의 순서로 출력될 것이다. 하지만 실제로 위 코드를 실행해보면 func1, func3, func2의 순서로 출력되는 것을 볼 수 있을 것이다. 이런 결과가 나오는 이유는 위에서 설명한 것 처럼 이벤트 큐와 이벤트 루프를 통해 비동기 처리를 수행하는 setTimeout함수가 다른 함수들과 다르게 동작하기 때문이다.

아래 이미지는 위 코드가 실행되는 과정을 보여준다.

event_loop_gif

이미지 출처: https://poiemaweb.com/js-event


위 과정을 순차적으로 정리하면 다음과 같다.

  1. func1함수가 호출되고 이는 호출 스택에 올라가고 console.log('func1')이 실행된다.
  2. func2함수가 호출 스택에 올라가고 setTimout함수를 호출한다.
  3. 호출된 setTimeout함수의 수행은 비동기적 처리를 수행하는 Web API에 넘어간다.
  4. func3함수가 호출 스택에 올라가고 console.log('func3')이 실행된다.
  5. Web API에서 setTimout함수에서 지정한 시간이 지나면 callback함수를 이벤트 큐로 넘긴다.
  6. 작업이 끝난 func3, func2, func1은 순차적으로 호출 스택에서 제거된다.
  7. 이벤트 루프는 호출 스택에 작업 중인 태스크가 없는 것을 확인하고 이벤트 큐에 있는 callback함수를 호출 스택으로 올린다.
  8. 호출 스택에 올라간 callback함수가 실행되면서 console.log('func3')가 실행된다.

위 설명에서 주의 깊게 볼 것은 비동기 함수인 setTimeout함수에 세팅된 시간이 3초라면 3초 후에 콜백 함수를 실행시켜라가 아닌 3초 후에 콜백 함수를 이벤트 큐에 넣어라가 된다는 것이다.

즉, setTimeout 함수는 n초 뒤에 콜백을 단순히 큐에 집어넣는게 끝이다. 코드를 간단히 보자면 아래와 같다.

var eventLoop = []; var event; while (true) { // 틱! if (eventLoop.length > 0) { event = eventLoop.shift(); } try { event(); // 호출스택으로 밀어넣는다 } catch (err) { //... } }

이 큐에 이미 대기번호가 100개가 있다면 func3는 101번째 대기표를 받게 될 것이다. 따라서 setTimeout은 지정한 시간동안은 실행되지 않는 것은 보장할 수 있지만 지정한 시간에 실행되는것은 보장할 수 없다.

while (await messageQueue.nextMessage()) { let message = messageQueue.shift(); message.run(); }

결론적으로, 이벤트 루프는 메시지 큐에 메시지가 더 있는지 확인하는 루프이다.

메시지 큐에 메시지가 있으면 메시지 큐에서 다음 메시지를 제거하고 그 메시지와 연관된 기능을 호출 스택으로 보낸다. 그렇지 않으면 새 메시지가 메시지 대기열에 추가될 때까지 대기를 한다. 이벤트 루프가 자바스크립트에게 비동기를 허용하는 기본 모델이다.

ES6이후의 변화된 비동기 처리와 이벤트 루프

기본적으로 이벤트 루프는 위에서 설명한 내용이 큰 틀이다. 큐와 스택을 감시하며 스택의 작업이 없으면 큐의 작업을 스택에 올린다. 다만, ES6이후에는 몇 가지 비동기적 작업을 수행하는 API들이 추가되었고 이에 따라 약간의 추가된 내용이 있다. 하지만, 전체적인 실행 방식은 동일하며 각각의 비동기 처리에 수행 순서에 초점을 두고 살펴보자.

기존에 살펴보았던 이벤트 큐(Event Queue)를 좀 더 자세히 나눠보면 다음과 같다.

  1. Task Queue : 가장 사람들이 잘 알고 있는 비동기 작업인 setTimeout이 들어가는 큐
  2. Micro Task Queue : ES6에서 추가된 Promise와 ES8의 Async Await(Async Await도 결국 Promise)
  3. AnimationFrame: requestAnimationFrame(rAF)의 콜백 함수가 들어간다.
console.log("script start"); setTimeout(function() { console.log("setTimeout"); }, 0); Promise.resolve().then(function() { console.log("promise1"); }).then(function() { console.log("promise2"); }); requestAnimationFrame(function { console.log("requestAnimationFrame"); }) console.log("script end");

위의 코드를 실행하면 다음과 같은 결과가 출력된다.

script start script end promise1 promise2 requestAnimationFrame setTimeout

즉, 이벤트 큐에서 나눠지는 3가지 영역의 우선 순위는 다음과 같다.

Micro Task Queue => AnimationFrame => Task Queue

기존에 이벤트 루프에 대해서 이해가 된 상태라면 이 내용은 크게 어렵지 않다. 쉽게 보면 비동기 작업을 처리하는 방법이 추가되었고 이에 따라 이벤트 큐에서 내부적으로 처리하는 로직에 약간의 변화가 생겼을 뿐이다. 결국, 정리하면 다음과 같다.

  1. 비동기 작업으로 등록되는 작업은 TaskMicro Task, 그리고 AnimationFrame으로 구분된다.

  2. Micro TaskTask보다 먼저 처리된다.

  3. Micro Task가 처리된 이후 requestAnimationFrame이 호출되고 이후 브라우저 랜더링이 발생한다.


🙏 Reference

repaint와 reflow

rendering

위의 그림과 같이 브라우저는 화면을 rendering하는 과정에서 배치(flow)그리기(paint) 의 과정을 거친다.

생성된 DOM 노드의 레이아웃이 변경될 떄, 변경 후 영향을 받는 모든 노드를 다시 계산하고 렌더 트리를 재생성 한다. 이러한 과정을 reflow라 하고 reflow가 일어난 후, repaint가 일어난다.

즉, DOM의 노드가 변경될 때 마다 DOM tree라는 자료구조에 접근해야 하기 때문에 DOM의 레이아웃을 변경하는 코드를 작성할 때는 이를 최적화하기 위한 고민이 필요하다.

reflow

function reFlow() { var container = document.getElementById('container'); container.appendChild(document.createTextNode('hello')); }

위의 코드를 보면 conatiner라는 엘리먼트에 hello라는 TextNode를 추가했다. 이로 인해 DOM 노드의 레이아웃이 바뀌며 reflowrepaint가 일어날 것이다.

repaint

function repaint() { var container = document.getElementById('container'); container.style.backgroundColor = 'black'; container.style.color = 'white'; }

위의 코드에서는 이전의 코드와 다르게 엘리먼트의 style만 변경했다. 이러한 경우 DOM 노드의 레이아웃은 변경되지 않았고 style속성만 변경되었기 때문에 reflow는 일어나지 않고 repaint만 일어나게 된다.

reflowrepaint가 많아질수록 애플리케이션의 렌더링 성능은 느려지게 되기 때문에 이를 줄일 수록 성능을 높일 수 있다.


🙏 Reference

네임스페이스 패턴(Namespace Pattern)과 IIFE

전역 변수를 많이 쓰면 안좋다고 흔히들 말한다. 그 이유를 알 수 있는 간단한 예를 보자.

var x = 100; function test() { x = 10; console.log('10이 나오겠지?', x); } test(); console.log('100을 출력해볼까 ?', x); /* 10이 나오겠지? 10 100을 출력해볼까 ? 10 */

지역 스코프에서 전역변수를 참조할 수 있으므로 전역변수의 값도 변경할 수 있다. 내부 함수의 경우에는, 전역변수는 물론 상위 함수에서 선언한 변수에 접근/변경이 가능하다.

프로젝트가 클수록, 협업이 이루어질수록 전역 변수가 많아지면 원하는 결과가 아닌 다른 결과가 나타날 수 있다.

네임스페이스 패턴(Namespace Pattern)

네임스페이스 패턴은 말 그대로 이름 공간을 선언하여 다른 공간과 구분하는 패턴이라고 보면 된다.

var APP_GLOBAL = { name : 'BKJang', age : '25', getInfo : function() { console.log('name :', this.name, 'age :', this.age); } } console.log(APP_GLOBAL); //{name: "BKJang", age: "25", getInfo: ƒ} console.log(APP_GLOBAL.name, APP_GLOBAL.age); //BKJang 25 APP_GLOBAL.getInfo(); //name : BKJang age : 25

이처럼 전역 변수 사용을 위해 전역 객체 하나를 만들어 사용하는 것이다.

즉시 실행 함수 표현식(IIFE, Immediately-Invoked Function Expression)

즉시 실행 함수를 사용하면 함수가 실행되고 전역에서 사라진다. 이 방법으로 라이브러리를 많이 만들곤 한다.

(function moduleFunction() { var a = 3; function helloWorld(){ console.log('Hello'); } helloWorld(); //Hello })(); helloWorld(); //Uncaught ReferenceError: helloWorld is not defined

즉시실행함수가 실행되고 전역에서 사라지기 때문에 그 밖에선 출력 값이 에러가 발생하는 것을 볼 수 있다.

var singleton = (function () { var a = 3; function helloWorld(){ console.log('Hello'); } return { a : a, sayHello: helloWorld } })(); singleton.sayHello(); //Hello console.log(singleton.a); //3

위와 같이 반환 값을 변수에 담아 전역 변수를 한 번 만 사용하여 전역 변수의 사용을 줄일 수도 있다.


🙏 Reference

모듈 패턴(Module Pattern)

var Developer = function(arg) { var lang = arg ? arg : ''; return { getLang : function() { return lang; }, setLang : function(arg) { lang = arg; } } }; var bkjang = new Developer('javascript'); console.log(bkjang.getLang()); //javascript bkjang.setLang('java'); console.log(bkjang.getLang()); //java

위의 코드를 보면 Developer 생성자 함수에서 this가 아닌 var lang = arg ? arg : '';으로 선언하면 자바스크립트는 함수형 스코프를 따르기 때문에 private해진다.

그리고 getLang()setLang() 이라는 함수는 클로저이기 때문에 외부에서는 lang이라는 변수의 값에 접근할 수 있는 인터페이스가 된다.

위와 같이 getLang()setLang()과 같은 public 메서드를 인터페이스로 제공하고 lang과 같은 private한 변수에 인터페이스를 통해서만 접근하도록 하는 것이 모듈 패턴이다.

그렇다면 private 멤버 변수가 객체나 배열일 경우는 어떻게 될까?

var Developer = function (obj) { var developerInfo = obj; return { getDeveloperInfo: function() { return developerInfo; } }; }; var developer = new Developer({ name: 'BKJang', lang: 'javascript' }); var bkJang = developer.getDeveloperInfo(); console.log('bkJang: ', bkJang); // bkJang: {name: "BKJang", lang: "javascript"} bkJang.lang = 'java'; //인터페이스가 아닌 직접 변경 bkJang = developer.getDeveloperInfo(); console.log('bkJang: ', bkJang); // bkJang: {name: "BKJang", lang: "java"} console.log(Developer.prototype === bkJang.__proto__); //false

일반 변수가 아닌 객체나 배열을 멤버 변수로 가지고 이를 그대로 반환할 경우, 외부에서 이 멤버를 변경할 수 있다.

왜냐하면, 객체나 배열을 반환하는 경우는 얕은 복사(shallow copy)로 private 멤버의 참조값을 반환하게 된다.

따라서, 반환할 객체나 배열의 정보를 담은 새로운 객체를 만들어 깊은 복사(deep copy)를 거친 후 반환해야 한다.

또한, 위처럼 일반 객체를 반환하면 프로토타입 객체는 Object.prototype 객체가 되기 때문에 상속을 구현할 수 없다. 따라서 함수를 반환해야 한다.

var Developer = (function() { var lang; //생성자 정의 function Developer(arg) { lang = arg ? arg : ''; } Developer.prototype = { getLang : function() { return lang; }, setLang : function(arg) { lang = arg; } } return Developer; }()); var bkJang = new Developer('javscript'); console.log(bkJang.getLang()); //javscript bkJang.lang = 'java'; //인터페이스를 통해서가 아닌 직접 변경 console.log(bkJang.getLang()); //javscript bkJang.setLang('java'); console.log(bkJang.getLang()); //java console.log(Developer.prototype === bkJang.__proto__); //true

마지막 출력 값을 보면 인스턴스인 bkJang의 프로토타입 객체가 Developer.prototype 객체임을 알 수 있고 이는 상속을 구현할 수 있음을 의미한다.


🙏 Reference

렉시컬 스코프(Lexical Scope)

자바스크립트는 렉시컬 스코프를 지원한다.

우선적으로, 자바스크립트 엔진에서 코드를 컴파일 하는 과정을 보면 다음과 같다.

  • 토크나이징/렉싱 - 코드를 잘게 나누어 토큰으로 만든다.
  • 파싱 - 나눈 토큰을 의미있게 AST(Abstract Syntax Tree)라는 트리로 만든다.
  • 코드생성 - AST를 기계어로 만든다.

렉시컬 스코프란 1단계에서 발생하는 즉, 렉싱 과정에서 정의되는 스코프를 말한다. 프로그래머가 변수와 스코프 블록을 어떻게 구성하는냐에 따라 렉싱 타임에서 정의되는 스코프를 렉시컬 스코프라고 한다.

var x = 'global' function test1() { var x = 'local'; test2(); } function test2() { console.log(x); } test1(); //global test2(); //global

위의 코드에서 test2()함수를 어디서 호출하는지가 아닌 어디에 선언되어있냐에 집중할 필요가 있다.

즉, test2()함수의 상위 스코프는 test1()과 전역이 아닌 전역이다. 이에 따라 test2()에서 출력한 값은 global이 나올 것이다.

쉽게 말하면, 렉시컬 스코프는 함수를 어디서 호출하는지가 아닌 어디서 선언했는지에 따라 결정된다. 이러한 특성때문에 정적 스코프(Static Scope) 라고도 한다.


🙏 Reference

Deep Copy

use JSON.parse , JSON.stringify

function changeAgePure(person) { var newPersonObj = JSON.parse(JSON.stringify(person)); newPersonObj.age = 25; return newPersonObj; } var alex = { name: 'Alex', age: 30 }; var alexChanged = changeAgePure(alex); console.log(alex); // -> { name: 'Alex', age: 30 } console.log(alexChanged); // -> { name: 'Alex', age: 25 }

or use $.extend()

function changeAgePure(person) { var newPersonObj = $.extend(true, {}, person); newPersonObj.age = 25; return newPersonObj } var alex = { name: 'Alex', age: 30 }; var alexChanged = changeAgePure(alex); console.log(alex); // -> { name: 'Alex', age: 30 } console.log(alexChanged); // -> { name: 'Alex', age: 25 }

or use destructuring (ES6)

function changeAgePure(person) { var newPersonObj = { ...person }; newPersonObj.age = 25; return newPersonObj } var alex = { name: 'Alex', age: 30 }; var alexChanged = changeAgePure(alex); console.log(alex); // -> { name: 'Alex', age: 30 } console.log(alexChanged); // -> { name: 'Alex', age: 25 }

🙏 Reference

Deep Copy

use JSON.parse , JSON.stringify

function changeAgePure(person) { var newPersonObj = JSON.parse(JSON.stringify(person)); newPersonObj.age = 25; return newPersonObj; } var alex = { name: 'Alex', age: 30 }; var alexChanged = changeAgePure(alex); console.log(alex); // -> { name: 'Alex', age: 30 } console.log(alexChanged); // -> { name: 'Alex', age: 25 }

or use $.extend()

function changeAgePure(person) { var newPersonObj = $.extend(true, {}, person); newPersonObj.age = 25; return newPersonObj } var alex = { name: 'Alex', age: 30 }; var alexChanged = changeAgePure(alex); console.log(alex); // -> { name: 'Alex', age: 30 } console.log(alexChanged); // -> { name: 'Alex', age: 25 }

or use destructuring (ES6)

function changeAgePure(person) { var newPersonObj = { ...person }; newPersonObj.age = 25; return newPersonObj } var alex = { name: 'Alex', age: 30 }; var alexChanged = changeAgePure(alex); console.log(alex); // -> { name: 'Alex', age: 30 } console.log(alexChanged); // -> { name: 'Alex', age: 25 }

🙏 Reference

이벤트 위임(Event Delegation)

이벤트 위임의 이해에는 이벤트 버블링이벤트 캡쳐에 대한 이해가 수반된다.

우선 그림으로 보면, 이벤트 버블링과 이벤트 캡쳐의 개념은 다음과 같다.

event_delegation

이벤트 버블링(Event Bubbling)

<body> <div class="el1"> <div class="el2"> <div class="el3"> </div> </div> </div> </body>
var divs = document.querySelectorAll('div'); divs.forEach(function(div) { div.addEventListener('click', bubbleEvent); }); function bubbleEvent(e) { console.log(e.target.className); } /* el3 el2 el1 */

위의 코드에서는 class 명이 el3인 element(<div class="el3"></div>)을 클릭했을 때, 이벤트가 발생하는 요소인 <div class="el3"></div>에서 상위에 있는 요소까지 이벤트를 전파시키고 있다. 이에 따라 결과 값은 el3만 나오는 것이 아닌 el1까지 콘솔에 출력되고 있다.

이처럼 이벤트가 발생한 요소로부터 상위요소로 전파시키는 이벤트 전파 방식을 이벤트 버블링이라고 한다.

이벤트 캡쳐(Event Capture)

<body> <div class="el1"> <div class="el2"> <div class="el3"> </div> </div> </div> </body>
var divs = document.querySelectorAll('div'); divs.forEach(function(div) { div.addEventListener('click', captureEvent, { capture : true }); }); function captureEvent(e) { console.log(e.target.className); } /* el1 el2 el3 */

이벤트 캡쳐(Event Capture)를 테스트하기 위해 addEventListener()함수에 capture : true라는 옵션을 추가했다.

위의 코드에서는 마찬가지로 class 명이 el3인 element(<div class="el3"></div>)을 클릭했을 때, 이벤트 버블링과 다르게 상위 요소에서 하위 요소로 즉, 이벤트 버블링과 반대 방향으로 이벤트를 전파하고 있다.

이에 따라 결과 값은 el3부터 el1까지 상위 요소부터 콘솔에 출력되고 있다.

이처럼, 이벤트가 발생했을 때, 상위 요소부터 하위요소로 전파시키는 이벤트 전파 방식을 이벤트 캡쳐라고 한다.

이벤트 위임(Event Delegation)

이벤트 위임은 하위 요소 각각에 이벤트를 구현하지 않고 상위 요소에서 하위요소의 이벤트를 제어하는 방식이다.

<body> <ul class="list"> <li> <input type="checkbox" id="one"/> </li> <li> <input type="checkbox" id="two"/> </li> </ul> </body>
var items = document.querySelectorAll('input'); items.forEach(function(item) { item.addEventListener('click', function(e) { console.log(e.target.id); }); });

위의 코드를 보면 각각의 input 요소를 클릭했을 때, 해당 요소의 id 값을 콘솔에 출력하도록 코드가 짜여있다. 하지만 이런 방식으로, 이벤트를 줄 요소를 for문을 돌려 이벤트를 구현할 시, 동적으로 추가되는 요소는 이벤트가 적용되지 않는다는 치명적인 단점이 있다.

쉽게 말해, 위의 코드에서 li를 특정 버튼을 눌러 추가한 이후, 그 요소를 클릭하면 위의 js코드가 작동하지 않는다는 것이다. 이러한 문제를 해결할 수 있는 방법이 이벤트 위임(Event Delegation)을 적용하는 것이다.

/* var items = document.querySelectorAll('input'); items.forEach(function(item) { item.addEventListener('click', function(e) { console.log(e.target.id); }); }); */ //이벤트 위임 방식으로 코드 변경 var items = document.querySelector('.list'); items.addEventListener('click', function(e) { console.log(e.target.id); });

위의 수정된 코드를 보면, 이벤트 줄 요소를 해당 요소가 아닌 그 상위 요소인 <ul class="list"></ul>을 지정하고 있다. 이벤트를 줄 요소가 아닌 그 요소의 부모 요소를 지정하여 이벤트 리스너를 달고, 하위에서 발생한 클릭 이벤틀를 감지하도록 한다. 이렇게 코드를 짜면, 동적으로 추가된 요소에 대해서도 이벤트가 동작하도록 할 수 있다.

이벤트 위임(Event Delegation)의 장점

  • 상위 요소에서 이벤트 리스너를 관리하기 때문에 하위 요소에는 자유롭게 요소를 추가할 수 있다. 즉, 동적인 element를 관리하기에 수월하다.

  • 이벤트 핸들러를 한 곳에서 관리하기 때문에 관리하기 수월하다.

  • 동적으로 추가되는 요소에 대한 이벤트가 없기 때문에 메모리의 사용이 줄어든다.

  • 이벤트 핸들러가 줄어들기 때문에 메모리 누수의 가능성도 줄어든다.

🙏 Reference

DocumentFragment

DocumentFragment를 활용하는 것은 reflow를 줄이기 위한 방법 중 하나다.

<body> <select id="timer"> </select> </body>
function addElements() { var target = document.getElementById('timer'); for (var i = 0; i < 24; i++) { var option = document.createElement('option'); option.innerText = i; target.appendChild(option); } }

위 코드는 0시부터 23시까지의 option엘리먼트를 셀렉트 박스에 추가하는 예제이다.

timer셀렉트박스에 0부터 23까지 반복을 돌려 매번 셀렉트 박스에 엘리먼트를 추가하고 있다. 24번의 DOM 레이아웃 변경이 일어나게 되기 때문에 24번의 reflowrepaint가 각각 일어나게 된다.

DocumentFragment를 활용했을 때의 가장 큰 차이는 DocumentFragment객체는 활성화된 DOM트리의 일부가 아니기 때문에 DocumentFragment객체에서 일어나는 변경사항은 DOM에 영향을 주지 않는다. 즉, reflow를 일으키지 않으며 성능에 큰 영향을 미치지 않게 된다.

function addElements() { var target = document.getElementById('timer'); var docFrag = document.createDocumentFragment(); for (var i = 0; i < 24; i++) { var option = document.createElement('option'); option.innerText = i; docFrag.appendChild(option); } target.appendChild(docFrag.cloneNode(true)); }

위의 코드를 보면 DOM레이아웃을 변경시키는 경우는 timer셀렉트 박스 엘리먼트에 추가할 때 발생한다. 즉, DocumentFragMent객체를 셀렉트 박스 엘리먼트에 추가할 때 1번만 DOM 레이아웃이 변경된다. 따라서 각각 24번의 reflowrepaint가 일어나던 것을 1번씩만 일어나도록 줄일 수 있게 된다.

최신 브라우저의 경우에는 reflow가 발생하지 않도록 엔진을 최적화하기 때문에 DocumentFragment를 통한 성능 향상을 체감할 수 없는 경우가 많다. 그러나 DOM 객체에 대한 다수의 접근을 필요로하는 작업을 수행해야하는 상황에서는 충분한 성능 향상을 체감할 수 있다.


🙏 Reference

repaint와 reflow 최적화

RepaintReflow가 많아질수록 애플리케이션의 렌더링 성능은 느려지게 된다. 즉, 이를 줄일수록 성능을 높일 수 있다.

DOM객체의 캐싱

//Before for(var i=0; i<100; i++) { document.getElementById('container').style.padding = i + 'px'; } //After var container = document.getElementById('container'); for(var i=0; i<100; i++) { container.style.padding = i + 'px'; }

class명과 cssText사용

//Before var container = document.getElementById('container'); container.style.padding = "20px"; container.style.border = "10px solid red"; container.style.color = "blue"; //After cssText container.style.cssText = 'padding:20px;border:10px solid red;color:blue;'; //After class container.className = 'test';

애니메이션이 들어간 노드는 가급적 position:fixed 또는 position:absolute로 지정

<div id="animation" style="background:blue;position:absolute;"></div>

프레임에 따라 reflow비용이 많은 애니메이션 효과의 경우엔 노드의 positionabsolutefixed로 주면 전체 노드에서 분리된다. 이 경우엔, 전체 노드에 걸쳐 Reflow 비용이 들지 않으며 해당 노드의 Repaint 비용만 들어가게 된다.

테이블 레이아웃을 피한다.

테이블로 구성된 페이지 레이아웃의 경우, 점진적 페이지 렌더링이 일어나지 않고 모든 계산이 완료된 후, 화면에 렌더링이 되기 때문에 피하는게 좋다.

Virtual DOM의 사용

Virtual DOM은 React나 Angular와 같은 UI/UX기반의 라이브러리 혹은 프레임워크에서의 컨셈이 되는 개념이다.

기존에 javascript나 jQuery에서 사용되던 DOM 직접접근 방식의 문제는 reflow와 repaint의 연관성도 빼놓을 수 없다.

DOM은 정적이다. DOM 요소에 접근하여 동적으로 이벤트를 주어 layout을 바꾸게 되면 reflow와 repaint가 일어나게 된다.

이 과정에서 규모가 큰 애플리케이션일수록 recalculate할 양이 늘어나고 이는 성능에 큰 영향을 미친다.

  1. 데이터가 업데이트되면, 전체 UI 를 Virtual DOM 에 리렌더링.
  2. 이전 Virtual DOM 에 있던 내용과 현재의 내용을 비교.
  3. 바뀐 부분만 실제 DOM 에 적용.

즉, Virtual DOM을 사용함으로써 바뀐 부분(Component)만 rerendering하기 때문에 컴포넌트가 업데이트 될 때, 레이아웃 계산이 한 번만 일어나게 된다.


🙏Reference

쓰로틀링 vs 디바운싱

함수를 호출할 때 호출이 너무 많이 되어 과부화 됨을 방지하기 위한 기술이다.

함수 호출이 잦은 예로는 브라우저의 이벤트가 있다. onscroll 이나 onchange 와 같은 이벤트의 콜백으로 함수를 호출하는 경우 굉장히 많은 호출이 발생할 수 있다.

infinite scroll 이나 자동완성 기능의 경우 사용자의 특정 이벤트에 따라 비동기 콜백을 호출하는 방식이다. 이 경우 이벤트가 매우 빈번하게 일어나며 많은 호출을 제어하지 않으면 브라우저가 버티지 못할 것이다. 이 때 사용하는 것이 쓰로틀링과 디바운싱이다.

쓰로틀링

Throttle 은 정해진 시간동안 특정 행위를 한 번만 호출하도록 하는 것이다. 예를 들어 스크롤 행위가 1 초에 500 번이 발생한다면 이를 0.2 초에 한 번만 실행하게 만들 수 있다.

Throttle 처리가 되지 않은 경우 콜백이 500 번 발생한다. 하지만 Throttle 처리가 된다면 5 번만 실행되게 만드는 기술이다.

스크롤 이벤트의 경우 작은 움직임에도 엄청나게 많은 이벤트가 발생한다. 따라서 1 초 미만으로 쓰로틀링을 하여 같은 동작의 여러번 호출을 1 번으로 제어하는 것이 좋다.

var timer; document.querySelector('#input').addEventListener('input', function (e) { // 1. timer 값이 undefined니까 if문 실행 if (!timer) { // 2. timer 에 time함수 설정 timer = setTimeout(function () { // 3. 설정시간에 맞춰 timer 초기화 및 함수 실행 timer = null; console.log('비동기 요청', e.target.value); }, 2000); } });

디바운싱

Debounce 는 한 행위를 여러 번 반복하는 경우, 마지막 행위 이후 특정 시간이 흐른 뒤에 콜백을 호출하도록 하는 방식이다. 자동완성 즉 autocomplete 를 떠올리면 편하다.

input 의 onChange가 일어나면 callback 으로 AJAX 를 이용해 관련 데이터를 긁어온다. 그런데 사용자의 모든 입력에 AJAX 호출을 한다면 브라우저가 견디지 못할 것이다. 그래서 일정시간동안 Timer 를 만든다. 이 타이머의 시간동안 입력이 발생해 변경이 일어나면 Timer 를 초기화 한다. 입력이 멈추어 Timer 가 다 되면 AJAX 를 호출한다.

var timer; document.querySelector('#input').addEventListener('input', function (e) { // 1. timer 값이 undefined니까 if문 실행 if (timer) { // 3-1. setTimeout이 끝나기 전에 다시 이벤트 실행 시 time함수 클리어 (이벤트 호출 시 마다 반복) clearTimeout(timer); } // 2. 함수 할당 // 3-2. 함수 할당 (이벤트 호출 시 마다 반복) timer = setTimeout(function () { console.log('비동기 요청', e.target.value); }, 2000); // 4. 설정 시간 지나면 함수 종료 });

LodashUnderscore에는 해당 기능들이 구현되어 있다.


🙏Reference

스크롤 이벤트 최적화 (rAF)

Throttling(쓰로틀링)

Throttling은 스크롤 이벤트에서 주로 사용되는 기술로 지나치게 많은 이벤트가 발생하는 것을 몇 초에 한 번, 또는 몇 밀리초에 한 번씩만 실행되게 제한을 두는 것이다.

var timer; document.querySelector('#input2').addEventListener('input', function (e) { if (!timer) { timer = setTimeout(function() { timer = null; console.log('ajax 요청', e.target.value); }, 2000); } });

위의 코드에서 보다시피 setTimeout을 이용하여 2초 동안에 한 번만 ajax요청을 하도록 하고 있다. 하지만 setTimeout은 지정된 시간 뒤에 무조건 실행되는 것을 보장할 수 없다. 위의 코드는 2초 뒤에 콜백 함수를 실행하는 것이 아니라 2초 뒤에 Task Queue에 넣는 것을 의미한다.
Task Queue에 들어간 함수는 순차적으로 Call Stack에 옮겨지고 실행되는데 만약 Call Stack이 비워져 있지 않다면, 지정한 시간 이후에 실행되는 것을 보장할 수 없다.

requestAnimationFrame(rAF)

rAF는 브라우저의 최적화 상태를 고려하여 이벤트를 실행한다. 즉, setTimeout처럼 무조건 지정된 시간에 한 번씩 이벤트를 트리거하지 않아도 된다. Jbee님 포스팅 중 스크롤 이벤트 최적화를 보면 rAF를 이용하여 스크롤 이벤트를 구현하는 방식을 살펴 볼 수 있는데 간단히 보면 다음과 같다.

function rAFScroll(callback) { let tick = false return function trigger() { if (tick) { return; } tick = true return requestAnimationFrame(function task() { tick = false return callback(); }) } }

trigger함수를 보면 tick의 값이 false일 경우에만 requestAnimationFrame의 콜백이 실행된다. 즉, 콜백 함수(callback)가 rAF에 의해 브라우저가 최적화된 상태에서만 tick의 값이 false가 되고 실행된다. 순차적으로 정리하면 다음과 같다.

  1. rAF의 콜백 함수인 task함수가 animation frame에 들어간다.
  2. tick의 값이 true라면 trigger함수가 호출되어도 콜백 함수가 실행되지 않는다.
  3. task함수가 실행되면서 tickfalse가 되면 다시 콜백 함수가 실행될 수 있는 환경이 된다.

이처럼 쓰로틀링을 사용하지 않고 rAF을 사용하여 이벤트 최적화가 가능하며 rAF는 애니메이션의 최적화에도 많이 사용되는 API이기 때문에 잘 알아두는게 좋다.

🙏Reference

값 비교하기 (Array, String, Object ...)

  • react-router-dom에서 사용 중인 value-equal 라이브러리
function valueOf(obj) { return obj.valueOf ? obj.valueOf() : Object.prototype.valueOf.call(obj); } function valueEqual(a, b) { // Test for strict equality first. if (a === b) return true; // Otherwise, if either of them == null they are not equal. if (a == null || b == null) return false; if (Array.isArray(a)) { return ( Array.isArray(b) && a.length === b.length && a.every(function(item, index) { return valueEqual(item, b[index]); }) ); } if (typeof a === 'object' || typeof b === 'object') { var aValue = valueOf(a); var bValue = valueOf(b); if (aValue !== a || bValue !== b) return valueEqual(aValue, bValue); return Object.keys(Object.assign({}, a, b)).every(function(key) { return valueEqual(a[key], b[key]); }); } return false; }

🙏 Reference

배열 내장함수

자바스크립트의 배열에서는 기본적으로 많은 종류의 내장 함수를 제공하고 있고 이를 이용하면 보다 간단한 코드로 다양한 기능을 구현할 수 있다.

forEach

forEach는 배열을 반복하여 기존 값을 가져오는데 주로 쓰인다.

const arr = [1, 2, 3, 4, 5]; arr.forEach(item => { console.log(item); //1 2 3 4 5 });

map

mapforEach의 차이는 배열을 반복하면서 각각의 원소에 대하여 특정 로직을 수행한 뒤, 새로운 배열을 반환하고 싶을 때 주로 사용한다.

const arr = [1, 2, 3, 4, 5]; const newArr = arr.map(item => { return item * 2; }); console.log(arr); //[1, 2, 3, 4, 5] console.log(newArr); //[2, 4, 6, 8, 10]

위에서 볼 수 있듯이 map을 통해 반환한 배열은 깊은 복사를 사용하기 때문에 기존 배열을 영향을 주지 않고 새로운 배열을 반환한다.

filter

filter함수의 기능은 이름 그대로 배열을 반복하며 각각의 원소들 중 특정 조건에 해당하는 원소들만 뽑아내어 새로운 배열을 반환한다. filter또한 map과 마찬가지로 기존 배열에 영향을 주지 않는다.

const arr = [1, 2, 3, 4, 5]; const newArr = arr.filter(item => { return item >= 3; }) console.log(arr); //[1, 2, 3, 4, 5] console.log(newArr); //[3, 4, 5]

indexOf

indexOf함수는 배열에서 찾고자 하는 원소가 있다면 그 원소의 index값을 반환한다.

const arr = ['a', 'b', 'c', 'd']; console.log(arr.indexOf('c')); //2

findIndex

findIndex함수도 indexOf함수와 마찬가지로 찾고자 하는 원소의 index값을 반환한다. 하지만 원소가 객체로 되어있거나 배열로 되어있을 때 indexOf함수로는 index값을 찾을 수 없지만 findIndex 함수는 조건 처리를 통하여 index값을 찾을 수 있다.

const arr = [ { id: 1, name: 'BKJang', age: 27, }, { id: 2, name: 'JHKim', age: 25, } ] console.log(arr.findIndex(item => item.id === 2)); //1

find

find함수는 findIndex함수와 사용법은 동일하지만 반환하는 값이 index값이 아닌 찾아낸 값 자체를 반환한다.

const arr = [ { id: 1, name: 'BKJang', age: 27, }, { id: 2, name: 'JHKim', age: 25, } ] console.log(arr.find(item => item.id === 2)); // {id: 2, name: "JHKim", age: 25}

splice

splice함수의 첫번째 파라미터는 지우기 시작할 원소의 index, 두번째 파라미터는 지울 원소의 갯수를 넘긴다.

const arr = ['a', 'b', 'c', 'd', 'e']; const arr2 = arr.splice(2, 3); console.log(arr); //["a", "b"] console.log(arr2); //["c", "d", "e"]

slice

slice함수와 splice함수의 가장 큰 차이점은 slice함수는 기존 배열에 영향을 주지 않고 새로운 배열을 반환한다는 것이다.
또 다른 차이점은 slice함수는 두번째 파라미터로 보낸 원소의 index값 전까지 원소를 제거한다.

const arr = ['a', 'b', 'c', 'd', 'e']; const arr2 = arr.slice(1, 3); console.log(arr); //["a", "b", "c", "d", "e"] console.log(arr2); //["b", "c"]

shift

shift함수는 기존 배열의 원소를 앞에서부터 하나씩 제거한다.

const arr = [1, 2, 3, 4, 5]; arr.shift(); console.log(arr); //[2, 3, 4, 5] arr.shift(); console.log(arr); //[3, 4, 5]

unshift

unshift함수는 기존 배열에 새로운 원소를 앞에 추가한다.

const arr = [1, 2, 3, 4, 5]; arr.unshift(0); console.log(arr); //[0, 1, 2, 3, 4, 5] arr.unshift(-1); console.log(arr); //[-1, 0, 1, 2, 3, 4, 5]

push

push함수는 기존 배열에 새로운 원소를 뒤에 추가한다.

const arr = [1, 2, 3, 4, 5]; arr.push(6); console.log(arr); //[1, 2, 3, 4, 5, 6] arr.push(7); console.log(arr); //[1, 2, 3, 4, 5, 6, 7]

pop

pop함수는 기존 배열의 원소를 뒤에서부터 하나씩 제거한다.

const arr = [1, 2, 3, 4, 5]; arr.pop(); console.log(arr); //[1, 2, 3, 4] arr.pop(); console.log(arr); //[1, 2, 3]

concat

concat은 여러 개의 배열을 하나로 합쳐주는 함수다. 기존 배열들에는 영향을 끼치지 않고 새로운 배열을 반환한다. ES6에서는 concat대신 Spread Operator를 많이 사용한다.

//concat const arr1 = ['a', 'b']; const arr2 = ['c', 'd', 'e']; const newArr = arr1.concat(arr2); console.log(arr1); //["a", "b"] console.log(arr2); //["c", "d", "e"] console.log(newArr); //["a", "b", "c", "d", "e"]
//ES6 Spread Operator const arr1 = ['a', 'b']; const arr2 = ['c', 'd', 'e']; const newArr = [...arr1, ...arr2]; console.log(arr1); //["a", "b"] console.log(arr2); //["c", "d", "e"] console.log(newArr); //["a", "b", "c", "d", "e"]

join

join함수는 배열의 원소들을 합쳐 문자열 형태로 반환한다.

const arr = ['a', 'b', 'c', 'd', 'e']; console.log(arr.join(', ')); //a, b, c, d, e console.log(arr.join('')); //abcde

every

every함수는 특정 조건에 대하여 배열의 모든 원소가 통과해야만 true를 반환하며 빈 배열에 대해서는 무조건 true를 반환한다.

const arr = [1, 2, 3, 4, 5]; const result1 = arr.every(item => item <= 5); const result2 = arr.every(item => item < 3); console.log(result1, result2); //true false

some

some함수는 특정 조건에 대하여 배열의 어떤 한 요소라도 통과하면 true를 반환하며 빈 배열에 대해서는 무조건 false를 반환한다.

const arr = [1, 2, 3, 4, 5]; const result1 = arr.some(item => item <= 5); const result2 = arr.some(item => item < 3); console.log(result1, result2); //true true

sort

const fruit = ['orange', 'apple', 'banana']; fruit.sort(); console.log(fruit); //["apple", "banana", "orange"]
const imD = [ { name : "BKJang", age : 27}, { name : "JHKim", age : 25}, { name : "SHJo", age : 28}, { name : "DHJung", age : 29}, { name : "JSKang", age : 23} ] /* 이름순 */ imD.sort((a, b) => { // 오름차순 return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; }); /** 0: {name: "BKJang", age: 27} 1: {name: "DHJung", age: 29} 2: {name: "JHKim", age: 25} 3: {name: "JSKang", age: 23} 4: {name: "SHJo", age: 28} */ imD.sort((a, b) => { // 내림차순 return a.name > b.name ? -1 : a.name < b.name ? 1 : 0; }); /** 0: {name: "SHJo", age: 28} 1: {name: "JSKang", age: 23} 2: {name: "JHKim", age: 25} 3: {name: "DHJung", age: 29} 4: {name: "BKJang", age: 27} */ /* 나이순 */ const sortingField = 'age'; imD.sort((a, b) => { // 오름차순 return a[sortingField] - b[sortingField]; }); /** 0: {name: "JSKang", age: 23} 1: {name: "JHKim", age: 25} 2: {name: "BKJang", age: 27} 3: {name: "SHJo", age: 28} 4: {name: "DHJung", age: 29} */ imD.sort((a, b) => { // 내림차순 return b[sortingField] - a[sortingField]; }); /** 0: {name: "DHJung", age: 29} 1: {name: "SHJo", age: 28} 2: {name: "BKJang", age: 27} 3: {name: "JHKim", age: 25} 4: {name: "JSKang", age: 23} */

reduce

reduce함수는 누적 값과 현재 값을 이용하여 여러 가지 기능을 구현할 수 있는 매우 유용한 함수다. 사실, reduce만 잘 사용할 줄 알아도 filter함수의 기능까지도 구현할 수 있다.

var arr = [1, 2, 3, 4, 5]; var sum = arr.reduce((acc, cur) => { return acc + cur; }); console.log(sum); // 15
var arr = [1, 2, 3, 4, 5]; var reducer = (acc, cur) => { return acc + cur; }; var sum = arr.reduce(reducer); console.log(sum); // 15
var arr = ['Kim', 'Kim', 'Kang', 'Jang', 'Jo', 'Kang', 'Jo']; var reducer = (acc, cur, index) => { if (!acc[cur]) { acc[cur] = 1; } else { acc[cur] = acc[cur] + 1; } return acc; // 여기 }; var sorting = arr.reduce(reducer, {}); console.log(sorting); // { Kim: 2, Kang: 2, Jang: 1, Jo: 2 }
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 15, 16, 20]; var reducer = (acc, cur, index) => { if (cur % 2 === 0) acc.push(cur * 2); return acc; }; var double = arr.reduce(reducer, []); console.log(double);
const input = [ { title: '슈퍼맨', year: '2005', cast: ['장동건', '권상우', '이동욱', '차승원'] }, { title: '스타워즈', year: '2013', cast: ['차승원', '신해균', '장동건', '김수현'] }, { title: '고질라', year: '1997', cast: [] } ]; const key = 'cast'; const flatMapReducer = (acc, cur) => { cur[key].reduce((acc2, cur2) => { if (acc2.indexOf(cur2) === -1) acc2.push(cur2); return acc2; }, acc); return acc; }; const flattenCastArray = input.reduce(flatMapReducer, []); // ['장동건', '권상우', '이동욱', '차승원', '신해균', '김수현'] console.log(flattenCastArray);

reduceRight

reduceRight함수는 기존의 reduce와 원리는 동일하지만 current에 마지막 index원소부터 첫번째 원소 순서로 들어간다. 아래는 배열을 nested object로 만드는 예시다.

const makeNestedObjWithArray = (arr) => { return arr.reduceRight( (accumulator, item) => { const newAccumulator = {}; newAccumulator[item] = Object.assign( {}, accumulator ); return newAccumulator; }, {} ); }; const list = ["a", "b", "c"]; const obj = makeNestedObjWithArray(list); console.log(obj); /** a : { b: { c: {} } } */

🙏 Reference

Object.keys(), Object.values(), Object.entries() 그리고 for...in

Object.keys(), Object.values(), Object.entries()

const developer = { name : 'BKJang', age : 26, lang: 'Korean' } console.log(Object.keys(developer)); // ["name", "age", "lang"] console.log(Object.values(developer)); // ["BKJang", 26, "Korean"] console.log(Object.entries(developer)); /** 0: ["name", "BKJang"] 1: ["age", 26] 2: ["lang", "Korean"] */

for...in

const developer = { name : 'BKJang', age : 26, lang: 'Korean' } for(let key in developer) { console.log(`${key} : ${developer[key]}`); } /** name : BKJang age : 26 lang: 'Korean' */