[JS] JavaScript·Python 핵심 — ES2015+·프로토타입·클로저·비동기
목차
[JavaScript] ES2015+ 요약 정리
ES2015+의 등장
기존의 자바스크립트 문법에 다른 언어의 장점들을 더한 편리한 기능들이 많이 추가되었다. 이 중에 활용도가 높은 부분에 대해서 알아보자.
1. const, let
자바스크립트에서 변수를 선언할 때 var를 이용해왔다. 하지만 이제 var는 const 와 let 으로 대체할 것이다.
const와 let은 함수 스코프를 가지는 var와는 달리 블록 스코프를 갖는다.
블록 스코프는 if, while, for, function 등에서 사용하는 중괄호에 속하는 범위를 뜻한다. 따라서 const와 let을 이 중괄호 안에서 사용하면 그 스코프 범위 안에서만 접근이 가능하다. 이를 통해 호이스팅 관련 문제를 자연스럽게 해결할 수 있다.
const와 let의 차이
| 키워드 | 재할당 | 초기화 필수 |
|---|---|---|
let | 가능 | 불필요 |
const | 불가 | 필수 |
const a = 0;
a = 1; // error
let b = 0;
b = 1; // 1
const c; // error
2. 템플릿 문자열
백틱(`) 을 이용해 새로운 문자열을 만들 수 있다.
백틱은 키보드에서
tab키위에 있다
백틱을 활용하면 문자열 안에 변수를 직접 삽입할 수 있다. 기존에는 +로 연결했지만 이제 한 줄로 작성이 가능해졌다.
var string = num1 + ' + ' + num2 + ' = ' + result;
const string = `${num1} + ${num2} = ${result}`;
아래가 훨씬 가독성이 좋다. 또한 백틱 안에 따옴표를 함께 작성하는 것도 가능하다.
3. 객체 리터럴
다음 코드는 oldObject 객체에 동적으로 속성을 추가하는 상황이다.
기존 코드
var sayNode = function() {
console.log('Node');
};
var es = 'ES';
var oldObject = {
sayJS: function(){
console.log('JS');
},
sayNode: sayNode,
};
oldObject[es + 6] = 'Fantastic';
oldObject.sayNode();
oldObject.sayJS();
console.log(oldObject.ES6);
이제 위와 같은 코드를 아래처럼 수정할 수 있다.
var sayNode = function() {
console.log('Node');
};
var es = 'ES';
const newObject = {
sayJS() {
console.log('JS');
},
sayNode,
[es+6]: 'Fantastic',
};
newObject.sayNode();
newObject.sayJS();
console.log(newObject.ES6);
oldObject와 newObject를 비교해보자.
- 객체의 메서드에 함수를 연결할 때
:와function키워드를 생략할 수 있다. sayNode : sayNode처럼 중복되는 이름의 변수는 간단히sayNode하나만 작성하면 된다.- 객체의 속성명을 동적으로 생성할 수 있다. 이전에는 객체 리터럴 바깥에서
[es+6]으로 만들었지만, 이제 내부에서 만들 수 있다.
코드의 양을 줄이고 편리하니 익숙해지면 좋다.
4. 화살표 함수
기존의 function {} 도 이전처럼 사용이 가능하지만, ES2015 이후로 화살표 함수가 많이 사용되고 있다.
function add1(x, y) {
return x+y;
}
const add2 = (x, y) => x + y;
두 가지 모두 똑같은 기능을 하는 함수다. 화살표 함수에서는 function 대신 => 기호로 선언한다. return 문을 줄일 수 있는 장점이 있다.
또한 화살표 함수는 function과 this 바인드 방식에서 차이점이 존재한다.
기존 코드 (this 문제)
var relationship1 = {
name: 'kim',
friends: ['a', 'b', 'c'],
logFriends: function() {
var that = this; // relationship1을 가리키는 this를 that에 저장
this.friends.forEach(function(friend){
console.log(that.name, friend);
});
},
};
relationship1.logFriends();
forEach 안에 function 선언문을 사용하면 각자 다른 함수 스코프의 this를 가지므로, that이라는 변수를 만들어 this 값을 미리 저장해두어야 한다.
화살표 함수로 개선
const relationship2 = {
name: 'kim',
friends: ['a', 'b', 'c'],
logFriends() {
this.friends.forEach(friend => {
console.log(this.name, friend);
});
},
};
relationship2.logFriends();
화살표 함수는 바로 바깥 스코프인 logFriends()의 this를 그대로 사용할 수 있다. 따로 바깥 스코프의 this를 저장해놓을 필요가 없어 편리하다.
5. 비구조화 할당
객체나 배열에서 속성 혹은 요소를 꺼내올 때 사용한다.
기존 코드
var candyMachine = {
status: {
name: 'node',
count: 5,
},
getCandy: function(){
return "Hi";
}
};
var getCandy = candyMachine.getCandy;
var count = candyMachine.status.count;
기존에는 객체에서 속성을 가져올 때 이처럼 작성했다.
const candyMachine1 = {
status: {
name: 'node',
count: 5,
},
getCandy1() {
return "Hi";
}
};
const { getCandy1, status: { count } } = candyMachine1;
console.log(getCandy1()) // Hi
console.log(count) // 5
이처럼 한 줄로 나타내는 것이 가능해졌다. 여러 단계 안의 속성도 count를 가져오는 것처럼 작성이 가능하다.
배열에서도 마찬가지로 적용이 가능하다.
var array = ['nodejs', {}, 10, true];
var node = array[0];
var obj = array[1];
var bool = array[array.length - 1];
const array1 = ['nodejs', {}, 10, true];
const [node, obj, , bool] = array1;
bool은 true를 가져오기 위해 배열의 마지막 부분에 작성한 걸 볼 수 있다. 이처럼 작성하면 맨 끝이라고 자동으로 인식해주니 상당히 편한 장점이 있다.
비구조화 할당을 이용하면 배열이 위치마다 변수를 넣어 똑같은 역할을 하도록 만들 수 있다. 특히 Node.js에서는 모듈을 사용하기 때문에 이런 방식이 많이 사용된다.
6. 프로미스 (Promise)
자바스크립트와 Node는 비동기 프로그래밍으로 이벤트 주도 방식을 활용하면서 콜백 함수를 많이 사용하게 된다. ES2015부터는 콜백 대신 프로미스 기반으로 API가 재구성되고 있다.
Promise 객체 구조
const condition = true;
const promise = new Promise((resolve, reject) => {
if (condition){
resolve('성공');
} else {
reject('실패');
}
});
promise
.then((message) => {
console.log(message);
})
.catch((error) => {
console.log(error);
});
resolve가 호출되면 then이 실행되고, reject가 호출되면 catch가 실행된다.
콜백 → 프로미스 변환
콜백 3중 중첩 예시:
function findAndSaveUser(Users) {
Users.findOne({}, (err, user) => { // 첫번째 콜백
if(err) {
return console.error(err);
}
user.name = 'kim';
user.save((err) => { // 두번째 콜백
if(err) {
return console.error(err);
}
Users.findOne({gender: 'm'}, (err, user) => { // 세번째 콜백
// 생략
});
});
});
}
프로미스로 개선:
function findAndSaveUser1(Users) {
Users.findOne({})
.then((user) => {
user.name = 'kim';
return user.save();
})
.then((user) => {
return Users.findOne({gender: 'm'});
})
.then((user) => {
// 생략
})
.catch(err => {
console.error(err);
});
}
then을 활용해 코드가 깊어지지 않도록 만들었다. 에러는 마지막 catch를 통해 한번에 처리가 가능하다.
지원하지 않는 콜백 함수는
util.promisify를 통해 가능하다.
Promise.all
const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
Promise.all([promise1, promise2])
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(err);
});
Promise.all에 해당하는 모든 프로미스가 resolve 상태여야 then으로 넘어간다. 하나라도 reject가 있다면 catch문으로 넘어간다.
7. async/await
ES2017에 추가된 최신 기능이며, 비동기 프로그래밍을 할 때 유용하게 사용되고, 프로미스를 더 깔끔하게 만들어주는 문법이다.
function findAndSaveUser1(Users) {
Users.findOne({})
.then((user) => {
user.name = 'kim';
return user.save();
})
.then((user) => {
return Users.findOne({gender: 'm'});
})
.then((user) => {
// 생략
})
.catch(err => {
console.error(err);
});
}
콜백의 깊이 문제를 해결하기는 했지만, 여전히 코드가 길긴 하다. 여기에 async/await 문법을 사용하면 아래와 같이 바꿀 수 있다.
async function findAndSaveUser(Users) {
try{
let user = await Users.findOne({});
user.name = 'kim';
user = await user.save();
user = await Users.findOne({gender: 'm'});
// 생략
} catch(err) {
console.error(err);
}
}
function 앞에 async를 붙이고, 프로미스 앞에 await을 붙여주면 된다. await을 붙인 프로미스가 resolve될 때까지 기다린 후 다음 로직으로 넘어가는 방식이다.
화살표 함수로 나타내면 아래와 같다.
const findAndSaveUser = async (Users) => {
try{
let user = await Users.findOne({});
user.name = 'kim';
user = await user.save();
user = await user.findOne({gender: 'm'});
} catch(err){
console.error(err);
}
}
[참고 자료]
데이터 타입
자바스크립트의 데이터 타입은 크게 Primitive Type, Structural Type, Structural Root Primitive 로 나눌 수 있다.
| 분류 | 타입 | typeof 결과 |
|---|---|---|
| Primitive | undefined | 'undefined' |
| Primitive | Boolean | 'boolean' |
| Primitive | Number | 'number' |
| Primitive | String | 'string' |
| Primitive | BigInt | 'bigint' |
| Primitive | Symbol | 'symbol' |
| Structural | Object | 'object' |
| Structural | Function | 'function' |
| Structural Root | null | 'object' |
기본적인 것은 설명하지 않으며, 놓칠 수 있는 부분만 설명하겠다.
Number Type
ECMAScript Specification을 참조하면 number 타입은 double-precision 64-bit binary 형식을 따른다.
console.log(1 === 1.0); // true
즉, number 타입은 모두 실수로 처리된다.
BigInt Type
BigInt 타입은 number 타입의 범위를 넘어가는 숫자를 안전하게 저장하고 실행할 수 있게 해준다. 숫자 뒤에 n을 붙여 할당할 수 있다.
const x = 2n ** 53n;
9007199254740992n
Symbol Type
Symbol 타입은 unique 하고 immutable 하다. 이런 특성 때문에 주로 이름이 충돌할 위험이 없는 객체의 유일한 property key를 만들기 위해 사용된다.
var key = Symbol('key');
var obj = {};
obj[key] = 'test';
데이터 타입의 필요성
var score = 100;
위 코드가 실행되면 자바스크립트 엔진은 아래와 같이 동작한다.
score는 특정 주소addr1을 가리키며 그 값은undefined이다.- 자바스크립트 엔진은
100이number타입인 것을 해석하여addr1와는 다른 주소addr2에 8바이트의 메모리 공간을 확보하고 값100을 저장하며score는addr2를 가리킨다. (할당)
만약 값을 참조하려 할 때도 한 번에 읽어야 할 메모리 공간의 크기(바이트 수)를 알아야 한다. 자바스크립트 엔진은 number 타입의 값이 할당되어 있음을 알기 때문에 8바이트만큼 읽게 된다.
정리하면 데이터 타입이 필요한 이유는 다음과 같다.
- 값을 저장할 때 확보해야 하는 메모리 공간의 크기를 결정하기 위해
- 값을 참조할 때 한 번에 읽어 들여야 할 메모리 공간의 크기를 결정하기 위해
- 메모리에서 읽어 들인 2진수를 어떻게 해석할지 결정하기 위해
Object Prototype
Prototype 은 JavaScript 객체가 다른 객체에서 상속하는 메커니즘이다.
Prototype 기반 언어란?
JavaScript는 종종 prototype-based language 로 설명된다. 객체는 prototype object 를 갖는다. prototype object는 메서드와 프로퍼티를 상속하는 템플릿 객체 같은 것이다.
객체의 prototype object 또한 prototype object를 가지고 있으며, 이것을 prototype chain 이라고 부른다.
JavaScript에서 연결은 object instance와 prototype(__proto__ 속성 또는 constructor의 prototype 속성) 사이에 만들어진다.
Understanding prototype objects
function Person(first, last, age, gender, interests) {
// property and method definitions
this.name = {
'first': first,
'last' : last
};
this.age = age;
this.gender = gender;
//...see link in summary above for full definition
}
object instance는 아래와 같이 만들 수 있다.
let person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
person1에 있는 메서드를 부른다면 어떤 일이 발생할까?
person1.valueOf()
valueOf()를 호출하면
- 브라우저는
person1객체가valueOf()메서드를 가졌는지 확인한다. 즉, 생성자인Person()에 정의되어 있는지 확인한다. - 그렇지 않다면
person1의 prototype object를 확인한다. prototype object에 메서드가 없다면 prototype object의 prototype object를 확인하며 prototype object가null이 될 때까지 탐색한다.
[JavaScript] Closure
closure는 주변 state(lexical environment)에 대한 참조와 함께 묶인 함수의 조합이다. 다시 말해서, closure는 inner function이 outer function의 scope를 접근할 수 있게 해준다. JavaScript에서 closure는 함수 생성 시간에 함수가 생성될 때마다 만들어진다.
Lexical Scoping
function init() {
var name = 'Mozilla'; // name is a local variable created by init
function displayName() { // displayName() is the inner function, a closure
alert(name); // use variable declared in the parent function
}
displayName();
}
init();
closure는 inner function이 outer function의 scope에 접근할 수 있기 때문에 위의 예제에서 displayName()이 init()의 지역 변수 name을 참조하고 있다.
lexical scoping 은 nested 함수에서 변수 이름이 확인되는 방식을 정의한다. inner function은 parent function이 return된 이후에도 parent function의 scope를 유지한다.
/* lexical scope (also called static scope)*/
function func() {
var x = 5;
function func2() {
console.log(x);
}
}
func() // print 5
/* dynamic scope */
function func() {
console.log(x)
}
function dummy1() {
x = 5;
func();
}
function dummy2() {
x = 10;
func();
}
dummy1() // print 5
dummy2() // print 10
첫 번째 예제는 compile-time에 추론할 수 있기 때문에 static이며, 두 번째 예제는 outer scope가 dynamic하고 function의 chain call에 의존하기 때문에 dynamic이라고 불린다.
Closure
function makeFunc() {
var name = 'Mozilla';
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
위의 예제는 처음의 init() 함수와 같은 효과를 가진다. 차이점은 inner function인 displayName()이 outer function이 실행되기 이전에 return되었다는 것이다.
다른 언어에서는 함수의 local variable은 함수가 실행되는 동안에만 존재한다. 그러나 JavaScript에서는 함수가 closure 를 형성하기 때문에 makeFunc()가 실행된 이후에도 name 변수에 접근할 수 있다.
closure란 함수와 lexical environment의 조합이다. 이 environment는 closure가 생성될 때 scope 내에 있던 모든 local 변수로 구성된다.
closure는 data와 함수를 연결시켜 주기 때문에 매우 유용하며, OOP(object-oriented programming)의 객체처럼 활용할 수 있다.
Emulating private methods with closures
Java와 다르게 JavaScript는 private를 구현하기 위한 native 방법을 제공하지 않는다. 그러나 closure를 통해서 private를 구현할 수 있다.
아래 예제는 Module Design Pattern을 따른다.
var counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
};
})();
console.log(counter.value()); // 0.
counter.increment();
counter.increment();
console.log(counter.value()); // 2.
counter.decrement();
console.log(counter.value()); // 1.
counter.increment, counter.decrement, counter.value는 같은 lexical environment를 공유하고 있다.
공유된 lexical environment는 즉시 실행되는 anonymous function(IIFE)의 body에 생성되어 있다. lexical environment는 private 변수와 함수를 가지고 있어 anonymous function의 외부에서 접근할 수 없다.
아래는 anonymous function 대신 일반 function을 사용한 예제이다.
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var counter1 = makeCounter();
var counter2 = makeCounter();
alert(counter1.value()); // 0.
counter1.increment();
counter1.increment();
alert(counter1.value()); // 2.
counter1.decrement();
alert(counter1.value()); // 1.
alert(counter2.value()); // 0.
위의 예제는 closure 보다는 object를 사용하는 것을 추천한다. makeCounter()가 호출될 때마다 increment, decrement, value 함수들이 새로 assign되어 오버헤드가 발생한다. 즉, object의 prototype에 함수들을 선언하고 object를 운용하는 것이 더 효율적이다.
function makeCounter() {
this.publicCounter = 0;
}
makeCounter.prototype = {
changeBy : function(val) {
this.publicCounter += val;
},
increment : function() {
this.changeBy(1);
},
decrement : function() {
this.changeBy(-1);
},
value : function() {
return this.publicCounter;
}
}
var counter1 = new makeCounter();
var counter2 = new makeCounter();
alert(counter1.value()); // 0.
counter1.increment();
counter1.increment();
alert(counter1.value()); // 2.
counter1.decrement();
alert(counter1.value()); // 1.
alert(counter2.value()); // 0.
Closure Scope Chain
모든 closure는 3가지 scope를 가지고 있다.
- Local Scope(Own scope)
- Outer Functions Scope
- Global Scope
// global scope
var e = 10;
function sum(a){
return function(b){
return function(c){
// outer functions scope
return function(d){
// local scope
return a + b + c + d + e;
}
}
}
}
console.log(sum(1)(2)(3)(4)); // log 20
// You can also write without anonymous functions:
// global scope
var e = 10;
function sum(a){
return function sum2(b){
return function sum3(c){
// outer functions scope
return function sum4(d){
// local scope
return a + b + c + d + e;
}
}
}
}
var s = sum(1);
var s1 = s(2);
var s2 = s1(3);
var s3 = s2(4);
console.log(s3) //log 20
위의 예제를 통해서 closure는 모든 outer function scope를 가진다는 것을 알 수 있다.
Creating closures in loops: A common mistake
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
document.getElementById('help').textContent = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
위의 코드는 정상적으로 동작하지 않는다. 모든 element에서 age의 help text가 보일 것이다. 그 이유는 onfocus가 closure이기 때문이다. item은 var로 선언되어 있어 호이스팅이 일어난다. item.help는 onfocus 함수가 실행될 때 결정되므로 항상 age의 help text가 전달된다.
아래는 해결방법이다.
방법 1: 별도 함수로 lexical environment 분리
function showHelp(help) {
document.getElementById('help').textContent = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
setupHelp();
방법 2: IIFE(즉시 실행 함수) 사용
function showHelp(help) {
document.getElementById('help').textContent = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})(); // Immediate event listener attachment with the current value of item (preserved until iteration).
}
}
setupHelp();
방법 3: let 키워드 사용
function showHelp(help) {
document.getElementById('help').textContent = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (let i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
Performance consideration
closure가 필요하지 않을 때 closure를 만드는 것은 메모리와 속도에 악영향을 끼친다.
예를 들어, 새로운 object/class를 만들 때 메서드는 생성자 대신에 object의 prototype에 선언하는 것이 좋다. 생성자가 호출될 때마다 메서드는 reassign되기 때문이다.
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
위의 예제에서 getName과 getMessage는 생성자가 호출될 때마다 reassign된다.
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
prototype 전부를 다시 재선언하는 것은 추천하지 않는다.
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
파이썬 매크로
설치
pip install pyautogui
import pyautogui as pag
마우스 명령
마우스 커서 위치 좌표 추출
x, y = pag.position()
print(x, y)
pos = pag.position()
print(pos) # Point(x=?, y=?)
마우스 위치 이동 (좌측 상단 0,0 기준)
pag.moveTo(0,0)
현재 마우스 커서 위치 기준 이동
pag.moveRel(1,0) # x방향으로 1픽셀만큼 움직임
마우스 클릭
pag.click((100,100))
pag.click(x=100,y=100) # (100,100) 클릭
pag.rightClick() # 우클릭
pag.doubleClick() # 더블클릭
마우스 드래그
pag.dragTo(x=100, y=100, duration=2)
# 현재 커서 위치에서 좌표(100,100)까지 2초간 드래그하겠다
duration 값이 없으면 드래그가 잘 안되는 경우도 있으므로 설정하기
키보드 명령
글자 타이핑
pag.typewrite("ABC", interval=1)
# interval은 천천히 글자를 입력할때 사용하기
글자 아닌 다른 키보드 누르기
pag.press('enter') # 엔터키
press 키 네임 모음 : 링크
보조키 누른 상태 유지 & 떼기
pag.keyDown('shift') # shift 누른 상태 유지
pag.keyUp('shift') # 누르고 있는 shift 떼기
많이 쓰는 명령어 함수 사용
pag.hotkey('ctrl', 'c') # ctrl+c