Map, Set, WeakMap, WeakSet 지원

Map

키 : 값 쌍

  • 생성법 : const m = new Map();
  • 메소드
    • set / get / has / delete / size / clear
    • keys : 키 배열 반환
    • values : 값 배열 반환
    • entries : <키:값> 배열 반환

Set

중복 허용 X

  • 생성법 : const s = new Set();
  • 메소드
    • add / size / delete ...

WeakMap / Set

기존 맵/셋과의 차이점

  • 키 값 : 객체만 가능
  • 키로 사용된 객체를 참조하는 것이 아무것도 없다면 해당 객체는 메모리와 Weak Map/Set 에서 자동으로 삭제됨
  • 반복 작업과 키나 값 전체를 얻는게 불가능(size, keys(), values(), entries ... etc)
    • why ? 가비지 컬렉션 동작 시점이 불 분명

UseCase

추가 데이터 : 부차적인 데이터 저장시
외부 코드에 속한 객체를 가지고 작업을 한다. 이 객체에 데이터를 추가해줘야 하는 데, 추가해줄 데이터는 객체가 살아있는 동안에만 유효하다.
위크맵은 복잡한 데이터를 저장하고, 위크셋은 단순한 데이터(예/아니오)를 저장하기에 좋다.

  • 사용자의 방문횟수 세기
    • Map
// visitsCount.js
let visitsCountMap = new Map();// 맵에 사용자의 방문 횟수를 저장
// 방문 횟수 카운트
function conntUser(user) {
	let count = visitsCountMap.get(user) || 0;
	visitsCountMap.set(user, count + 1);
}
// main.js
// john 방문시
let john = { name: "John" };
// 방문 횟수 증가
countUser(john);
// john의 방문 횟수를 셀 필요가 없어지면 null로 덮어씁니다.
john = null;
    • WeakMap
// visitsCount.js
let visitsCountMap = new WeakMap();// 위크맵에 사용자의 방문 횟수를 저장
// 방문 횟수 카운트
function conntUser(user) {
	let count = visitsCountMap.get(user) || 0;
	visitsCountMap.set(user, count + 1);
}
// main.js
// john 방문시
let john = { name: "John" };
// 방문 횟수 증가
countUser(john);
//john 객체가 가비지컬렉션의 대상이되면, 자동으로 메모리에서 삭제됩니다.
  • 사용자의 사이트 방문 여부를 추적(WeakSet)
let visitSet = new WeakSet();
 
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
 
visitedSet.add(john); // John 방문
visitedSet.add(pete); // ㅖete 방문
visitedSet.add(john); // John 방문
 
// 방문 여부 확인
alert(visitedSet.has(john)); // true
alert(visitedSet.has(mary)); // false
 
john = null; // visitSet에서 john을 나타내는 객체가 자동으로 삭제

캐싱

  • 함수 연산 결과를 맵에 저장
    • Map
// cache.js
let cache = new Map();
 
// 연산을 수행하고 그 결과를 맵에 저장
function process(obj) {
	if(!cache.has(obj)) {
		let result = /* 연산 수행 */obj;
 
		cache.set(obj, result);
	}
	return cache.get(obj);
}
 
// main.js
// 함수 process() 호출\
let obj = {/* ... 객체 ... */};
 
let result1 = process(obj); // 연산 수행
let result2 = process(obj); // 연산 수행을 하지않고 저장된 결과 반환
 
// 객체가 쓸모없어지면 null로 덮어씀
obj = null;
 
alert(cache.size); // 1 (객체가 cache에 남아 있음. 메모리 낭비)
  •  
    • WeakMap
// cache.js
let cache = new WeakMap();
 
// 연산을 수행하고 그 결과를 맵에 저장
function process(obj) {
	if(!cache.has(obj)) {
		let result = /* 연산 수행 */obj;
 
		cache.set(obj, result);
	}
	return cache.get(obj);
}
 
// main.js
// 함수 process() 호출\
let obj = {/* ... 객체 ... */};
 
let result1 = process(obj); // 연산 수행
let result2 = process(obj); // 연산 수행을 하지않고 저장된 결과 반환
 
// 객체가 쓸모없어지면 null로 덮어씀
obj = null;
 
// size를 사용할 수 없지만, obj가 키로 캐싱된 데티어는 메모리에서 삭제됨

 

Promise

패턴

대표적인 패턴

  • all([...]) : 여러 프라미스를 동시에 편성(복수의 병렬/동시 작업)하여 모두 이루어진다는 전제로 동작
  • race([...]) : 결승선을 통과한 최초의 프라미스만 인정하고 나머지는 무시
    • 하나라도 이루어진 프라미스가 있을 경우에 이루어지고 하나라도 버려지는 프라미스가 있으면 버려진다.

all과 race를 변형한 패턴 중에 자주 쓰이는 것들이 있다.

  • none([]) : all과 비슷하지만 이룸/버림이 정반대다. 모든 프라미스는 버려져야 하며, 버림이 이룸값이 되고 이룸이 버림값이 된다.
  • any([]) : all과 유사하나 버림은 모두 무시하며, 전부가 아닌 하나만 이루어지면 된다.
  • first([]) : race와 비슷하다. 최초로 프라미스가 이루어지고 난 이후엔 다른 이룸/버림은 간단히 무시한다.
  • last([]) : first와 거의 같고 최후의 이룸 프라미스 하나만 승자가 된다는 것만 다르다.
var getTimePromise = (sec) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(sec);
            resolve(`Wow! ${sec}second`);
        }
        , sec * 1000);
    });
}
var one = getTimePromise(1);
var two = getTimePromise(2);
// 여러 프라미스를 동시에 편성(복수의 병렬/동시 작업)하여 모두 이루어진다는 전제로 동작
/*Promise.all([one, two]) // all에 전달하는 배열은 프라미스,thenable, 즉시값 모두 가능
.then((msgs) => {
    // all이 반환한 메인 프라미스는 자신의 하위 프라미스들이 모두 이루어져야 이루어질 수 있다.
    console.log(msgs);
})*/
// 결승선을 통과한 최초의 프라미스만 인정하고 나머지는 무시
// 하나라도 이루어진 프라미스가 있을 경우에 이루어지고 하나라도 버려지는 프라미스가 있으면 버려진다
Promise.first([one, two])
.then((msgs) => {
    console.log(msgs);
})
/* Promise.race([])를 이용한 프라미스 타임아웃 패턴
추가 설명 : then() 메서드는 Promise (en-US)를 리턴하고 두 개의 콜백 함수를 인수로 받습니다. 하나는 Promise가 이행했을 때, 다른 하나는 거부했을 때를 위한 콜백 함수입니다.
Promise.race([one, timeoutPromise( 3000 )])
.then(() => {
    function(){
        // `foo(..)`가 제때 이루어졌다.
    },
    function(err){
        // `foo()`가 버려졌거나 제때 마치지 못했으니
        // `err`를 조사하여 원인을 찾는다.
    }
})*/

async & await

목적 : 좀 더 코드를 짜기 쉽게

  • 프로미스를 간결하고 간편하게 좀 더 동기적으로 실행되는 것처럼 보여준다.
  • 프로미스 체이닝을 하면 코드가 난잡해질 수 있는 데 이 방법을 사용하면 동기식으로 코드를 순서대로 작성하는 것처럼 간편하게 할수잇게 도와준다.
  • 기존에 존재하는 것을 감싸서 좀더 간편하게 사용할 수 잇는 api를 제공하는 것은 syntactic sugar라고 합니다.
  • 무조건 async & await를 사용하라는 건 아니다. async&await / promise 중 패턴에 따라 편한 걸로 사용하면 된다.

정의

async

promise를 리턴하는 함수를 좀 더 간편하게 정의 가능

  • 프로미스가 리턴되기 때문에 .then() 사용 가능
async function myAsync() {
    return 'async'; 
}

const res = myAsync();
console.log(res); // Promise{<resolve>: "async"}
res.then((result) => {
// then의 리턴값 : result
    console.log(result); // "async"
});

await

프로미스가 resolve되서 결과 값이 넘어올 때까지 기다린다.

  • await는 async함수 내부에만 사용 가능
  • 그냥 함수면 일반 함수처럼 리턴된다.

예외 처리

예외 발생 : throw

예외 처리 : catch

  • async함수내 await 되는 프로미스 함수가 reject되면 자동으로 throw 발생 : try-catch / .catch
async function wait(ms) {
	// return new Promise (resolve => setTimeout(resolve, ms));
    throw 'error';
}

console.log(wait(3)); // Promise{<rejected>: "error"}

async function useTryCatch() {
    console.log(`in useTryCatch : ${new Date()}`);
    try {
        await wait(3); // throw error!
    } catch(e) {
        console.error(e); // 에러 잡기
    }
    console.log(`in useTryCatch : ${new Date()}`);
}

async function useCatchFun() {
    console.log(`in useCatchFun : ${new Date()}`);
    const result = await wait(3).catch(e => {
        console.error(e);
    }); // catch를 통해 리턴된 프라미스를 await : 정상적이면 resolve값 반환 / 예외 발생 catch에서 리턴한 값. 이렇게 없으면 undefined
    console.log(`in useCatchFun : ${new Date()}`);
}

async function all() { // 순차 처리
    await useTryCatch();
    await useCatchFun();
}

all();

패턴

위의 프로미스 패턴 사용 가능(all, race)

  • 병렬 처리
async function wait(ms) {
	return new Promise (resolve => setTimeout(resolve, ms));
}
 
async function getApple() {
	await wait(1000);
	return 'apple';
}
 
async function getBanana() {
	await wait(1000);
	return 'banana';
}
 
async function parallel() { // 순차 처리
	return Promise.all([getApple(), getBanana()]).then(fruits => {
		console.log(fruits);
		return fruits.join(` + `);
	});
}
parallel().then((res) => {
	console.log(res);
});

참고

Generator

목적 : Promise + Generator => async & await

개념

문법

function *foo(x) {
    var y = x * (yield "y value:");
    return y;
}

var it = foo( 6 );

// start `foo(..)`
it.next(); // {value:"y value:", done: false}

var res = it.next( 7 ); // {value:42, done: true}

Interleaving

이터레이터를 생성할 때마다 이터레이터가 제어할 제너레이터의 인스턴스를 암시적으로 생성한다.

동일한 제너레이터의 여러 인스턴스를 동시에 실행 가능하며 상호 작용할수도 있다.

function *foo() {
    var x = yield 2;
    z++;
    var y = yield (x * z);
    console.log( x, y, z );
}

var z = 1;

var it1 = foo();
var it2 = foo();

var val1 = it1.next().value;            // 2 <-- yield 2
var val2 = it2.next().value;            // 2 <-- yield 2

val1 = it1.next( val2 * 10 ).value;        // 40  <-- x:20,  z:2
val2 = it2.next( val1 * 5 ).value;        // 600 <-- x:200, z:3

it1.next( val2 / 2 );                    // y:300
                                        // 20 300 3
it2.next( val1 / 4 );                    // y:10
                                        // 200 10 3

다른 두 제너레이터 함수의 반복자를 각각 어떤 순서로 부르느냐에 따라 다른 결과를 생성할 수 있다.

iterable, iterator와의 관계

제너레이터는 이터레이터를 좀 더 쉽게 사용가능하게 한다.

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  return;
}

let generator = foo();

for(let value of generator) {
  alert(value); // 1, 2, 3가 출력됨
}

Generator Delegation(위임)

  • yield * [반복 가능한 객체]
  • 재귀, 비동기, 예외 등 대부분의 동작에 대한 위임이 가능하다.
function *foo(val) {
  console.log(val);
  if (val > 1) {
    // generator recursion
    val = yield *foo( val - 1 );
  }
}

const gen = foo(3);
gen.next();

예제

1, 4, 9, 16, ... n^2 값을 반환하는 함수 : 제너레이터에 변수를 넣으면 함수를 계속실행해도 그 값이 유지됩니다.

function *getSquaredNumber(max) {
  let n = 0;  // 다음 메소드를 계속 호출하면서 그 가치를 유지하고 있습니다.
  while(n < max) {
    n++;
    yield n * n;
  }
}
// usecase 1
const res = getSquaredNumber(3);
console.log(res.next().value);
console.log(res.next().value);
console.log(res.next().value);
console.log(res.next().value);
// usecase 2
for(const n of getSquaredNumber(3)) {
  console.log(n);
}

배열에서 중복 없이 무작위로 이름을 선택하는 함수(한번 선택되면 그 후에는 선택되지 않음)

function *getUniqueRandomVal(array) {
  const available = array;
  while(available.length !== 0) {
    const randomIdx = Math.floor(Math.random() * available.length);
    const val = available[randomIdx];
  
    // remove the used value from the list of available values
    available.splice(randomIdx, 1);
    console.log(available);
  
    yield val;
  }
}

const names = ["aa", "bb", "cc", "dd"];
for(const name of getUniqueRandomVal(names)) {
  console.log(name);
}

메모리를 적게쓰면서 간단한게 사용하는 법(내장 함수 reverse(), filter()...)

const arr = [1, 2, 3, 4];
// case 1 : 메모리 사용량 = 원본 배열 X 2
console.log("[case 1]");
for(const val of arr.reverse()) { // 이 함수는 이 함수를 호출한 배열을 거꾸로 뒤집고, 그 배열을 가리키는 참조값을 반환합니다. 따라서, 이 함수를 실행시키면 원본 배열이 변형됩니다.
  console.log(val);
}
// case 2 : 메모리 사용량 = 원본 배열
const arr2 = [1, 2, 3, 4];
console.log("[case 2]");
for(let i = arr2.length - 1; i >= 0; i--) {
  console.log(arr2[i]);
}
// case 3 : 메모리 사용량 = 원본 배열, use generator
console.log("[case 3]");
function *reverse(array) {
  for(let i = array.length - 1; i >= 0; i--) {
    yield array[i];
  }
}
for(const val of reverse(arr2)) {
  console.log(val);
}

 async & await와 Promise + Generator 비교

참조

 

 

'JS' 카테고리의 다른 글

Class 내 arrow function은 사용하지 않아야한다.  (0) 2022.07.22

크게 메모리 문제, method override 문제를 야기시킨다.

 

원인

arrow function의 this는 상위 스코프의 this를 바인딩

  • 다른 함수들은 어떻게 호출되었는지에 따라 this에 바인딩할 객체가 동적으로 결정됨

따라서 class 내에서 arrow function을 정의하면 constructor 내부로 이동되기 때문에 프로토타입으로 공유되어야할 메소드가 각 인스턴스 내부로 이동한다.

 

발생되는 문제

메모리 문제

JS에서 클래스 메서드를 normal function으로 정의시, 해당 메소드는 모든 인스턴스들이 단 하나의 프로토타입 메소드를 공유한다.

그러나 화살표 함수로 정의하면 각 인스턴스마다 메소드가 정의된다. 따라서 100개의 인스턴스를 만들면 그 100개의 인스턴스 내부에 서로 다른 각각 100개의 함수가 중복으로 만들어지게 된다.

예제

class ClassFun {
    constructor(name) {
        this.name = name;
    }

    method() {
        console.log(`${this.name} method`);
    }
};

class ClassArrowFun {
    constructor(name) {
        this.name = name;
    }

    method = () => {
        console.log(`${this.name} arrow method`);
    }
};


const classFunList = [new ClassFun(1), new ClassFun(2)];
classFunList.forEach((item) => {
    item.method();
});

const classArrowFunList = [new ClassArrowFun(1), new ClassArrowFun(2)];
classArrowFunList.forEach((item) => {
    item.method();
});

console.log(classFunList[0]);   // ClassFun {name: 1}
console.log(classFunList[1]);   // ClassFun {name: 2}

console.log(classArrowFunList[0]);  // ClassArrowFun {name: 1, method: ƒ}
console.log(classArrowFunList[1]);  // ClassArrowFun {name: 2, method: ƒ}

console.log(classFunList[0].method === classFunList[1].method); // true
console.log(classArrowFunList[0].method === classArrowFunList[1].method);   // false

method override 문제

아래의 출력결과를 보자.

class ParentClass {
    constructor(name) {
        this.name = name;
    }

    method() {
        console.log(`parent - ${this.name} method`);
    }

    arrow_method = () => {
        console.log(`parent - ${this.name} arrow method`);
    }
};

class ChildClass extends ParentClass {
    constructor(name) {
        super(name);
    }

    method() {
        console.log(`child - ${this.name} method`);
    }

    arrow_method() {
        console.log(`child - ${this.name} arrow method`);
    }
}

const parent = new ParentClass("parent");
parent.method();        // parent - parent method
parent.arrow_method();  // parent - parent arrow method

const child = new ChildClass("child");
child.method();         // child - child method
child.arrow_method();   // parent - child arrow method (**)

child.arrow_method()는 ChildClass 에 정의된 메소드를 사용하는 것이 아니라 ParentClass 에 정의된 메소드를 사용한다.

이는 화살표 함수로 정의된 메서드는 constructor 내부로 이동되어 각 인스턴스 내부에 정의되기 때문에 발생한다.

바벨 트렌스파일링을 해보면 더 잘알 수 있다. 

 

TODO : 스킨에서 class내 arrow function을 쓰고 있는데 괜찮나..? 트랜스파일링된 것을 한번봐야할듯

'JS' 카테고리의 다른 글

ES6  (0) 2022.07.22

+ Recent posts