📢 자바스크립트는 함수를 다룰 때 탁월한 유연성을 제공한다. 함수는 이곳저곳 전달될 수도 있고, 객체로도 사용될수 있다.
코드 변경 없이 캐싱 기능 추가하기
◾️ CPU를 많이 잡아먹지만 결과는 안정적(x 가 같으면 호출결과도 같다)인 함수 slow(x)
◾️ slow(x)가 자주 호출된다면, 결과를 어딘가에 저장(캐싱)해 재연산에 걸리는 시간을 줄일 수 있다.
◾️ 래퍼 함수를 만들어 캐싱 기능을 추가한다.
function slow(x) {
// CPU 집약적인 작업
console.log(`slow(${x})을 호출`);
return x;
}
function cachingDecorator (func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // cache에 해당 키가 있으면
return cache.get(x); // 대응하는 값을 cache에서 읽어온다.
}
let result = func(x); // 그렇지 않은 경우엔 func를 호출하고
cache.set(x, result); // 그 결과를 캐싱(저장) 한다.
return result;
};
}
slow = cachingDecorator(slow);
console.log( show(1) ); // show(1)가 저장됨
console.log( '다시 호출: ' + slow(1) ); // 동일한 결과
console.log( show(2) ); // slow(2)가 저장됨
console.log( '다시 호출: ' + slow(2) ); // 동일한 결과
◾️ cachingDecorator같이 인수로 받은 함수의 행동을 변경시켜주는 함수를 데코레이터(decorator) 라고 한다.
◾️ 모든 함수를 대상으로 cachingDecorator를 호출 할 수 있는데, 이때 반환되는 것은 캐싱 래퍼이다.
◾️ 함수에 cachingDecorator를 적용하기만 하면 캐싱이 가능한 함수를 원하는 만큼 구현할 수 있다.
◾️ 캐싱 관련 코드를 함수 코드와 분리할 수 있기 때문에 함수의 코드가 간결해진다.
◾️ cachingDecorator(func)를 호출하면 래퍼(wrapper), function(x)가 반환된다.
◾️ 래퍼 function(x)는 func(x)의 호출 결과를 캐싱 로직으로 감싼다. (wrapping)
◾️ 함수 slow는 래퍼로 감싼 이전이나 이후나 동일한 일을 수행한다. 행동 양식에 캐싱 기능이 추가된 것뿐
◾️ slow 본문을 수정하는 것 보다 독립된 래퍼 함수 cachingDecorator를 사용할 때 장점
- cachingDecorator를 재사용 할 수 있다. 원하는 함수 어디에든 cachingDecorator를 적용할 수 있다.
- 캐싱 로직이 분리되어 slow 자체의 복잡성이 증가하지 않는다.
- 필요하다면 여러 개의 데코레이터를 조합해서 사용할 수도 있다. (추가 데코레이터는 cachingDecorator뒤를 따른다.)
func.call를 사용해 컨텍스트 지정하기
◾️ 객체 메서드 worker.slow()는 데코레이터 적용 후 제대로 동작하지 않는다.
◾️ worker.slow()에 캐싱 기능 추가
let worker = {
someMethod() {
return 1;
},
slow(x) {
// CPU 집약적인 작업
console.log(`slow(${x})을/를 호출`);
return x * this.someMethod(); // (*)
}
};
// 이전과 동일한 코드
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func(x); // (**)
cache.set(x, result);
return result;
};
}
console.log( worker.slow(1) );
worker.slow = cachingDecorator(worker.slow); // 캐싱 데코레이터 적용
console.log( worker.slow(2) ); // Error: Cannot read property 'someMethod' of undefined
◾️ (*)로 표시한 줄에서 this.someMethod 접근에 실패했기 때문에 에러가 발생
◾️ 원인은 (**)로 표시한 줄에서 래퍼가 기존 함수 func(x)를 호출하면 this가 undefined가 되기 때문이다.
◾️ func.call(context, ...args) : this를 명시적으로 고정해 함수를 호출할 수 있게 해주는 특별한 내장 함수 메서드
func.call(context, arg1, arg2, ...);
◾️ 메서드를 호출하면 메서드의 첫 번째 인수가 this, 이어지는 인수가 func의 인수가 된 후, func가 호출된다.
◾️ 둘다 인수로 1, 2, 3을 받지만 func.call에선 this가 obj로 고정된다.
func(1, 2, 3);
func.call(obj, 1, 2, 3)
◾️ 다른 컨텍스트(객체) 하에 sayHi를 호출
◾️ sayHi.call(user)를 호출하면 sayHi의 컨텍스트가 this=user로, sayHi.call(admin)을 호출하면 컨텍스트가 this=admin로 설정
function sayHi() {
console.log(this.name);
}
let user = { name: 'suzu' };
let admin = { name: 'Admin' };
// call을 사용해 원하는 객체가 this가 되도록 한다.
sayHi.call( user ); // this = suzu
sayHi.call( admin ); // this = Admin
◾️ 컨텍스트와 phrase에 원하는 값을 지정
function say(phrase) {
console.log(this.name + ': ' + phrase);
}
let user = { name: 'suzu' }
// this=user
say.call( user, 'Hello' ); // suzu: Hello
◾️ 래퍼 안에서 call을 사용해 컨텍스트 원본 함수로 전달하면 에러가 발생하지 않는다.
let worker = {
someMethod() {
return 1;
},
slow(x) {
// CPU 집약적인 작업
console.log(`slow(${x})을/를 호출`);
return x * this.someMethod(); // (*)
}
};
// 이전과 동일한 코드
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // this가 제대로 전달된다
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow); // 캐싱 데코레이터 적용
console.log( worker.slow(2) ); // 동작 OK
console.log( worker.slow(2) ); // 동작 OK -> 원본 함수 호출 X, 캐시된 값이 출력
◾️ this가 전달되는 과정
- 데코레이터를 적용한 후에 worker.slow는 래퍼 function (x) { ... }이 된다.
- worker.slow(2)를 실행하면 래퍼는 2를 인수로 받고, this=worker가 된다.(점 앞의 객체)
- 결과가 캐시되지 않은 상황이라면 func.call(this. x)에서 현재 this (=worker)와 인수(=2)를 원본 메서드에 전달한다.
여러 인수 전달하기
◾️ 복수 인수를 가진 메서드, worker.slow 캐싱
let worker = {
slow(min, max) {
return min + max; // CPU를 아주 많이 쓰는 작업이라고 가정
}
};
// 동일한 인수를 전달했을 때 호출 결과를 기억할 수 있어야 한다.
worker.slow = cachingDecorator(worker.slow);
◾️ 해결방법
- 복수 키를 지원하는 맵과 유사한 자료 구조 구현하기(서드 파티 라이브러리 등을 사용해 됨)
- 중첩 맵을 사용하기. (max, result) 쌍 저장은 cache.set(min)으로, result는 cache.get(min).get(max)를 사용해 얻는다.
- 두 값을 하나로 합치기. Map의 키로 문자열 min, max를 사용한다. 여러 값을 하나로 합치는 코드는 해싱 함수(hashing function)에 구현해 유연성을 높인다.
◾️ func.call(this. x)를 func.call(this, ...arguments)로 교체해, 래퍼 함수로 감싼 함수가 호출할 때 복수 인수로 넘길 수 있다.
let worker = {
slow(min, max) {
console.log(`slow(${min},${max})을 호출함`);
return min + max;
}
};
function cachingDecorator(func, hash) {
let cache = new Map();
return function () {
let key = hash(arguments); // (*)
if(cache.has(key)) {
return cache.get(key);
}
let result = func.call(this, ...arguments); // (**)
cache.set(key, result);
return result;
}
}
function hash(args) {
return args[0] + ',' + args[1];
}
worker.slow = cachingDecorator(worker.slow, hash);
console.log( worker.slow(3, 5) ); 8
console.log( '다시 호출: ' + worker.slow(3, 5) ); // 8캐시된 결과
◾️ (*)로 표시한 줄에서 hash가 호출되면서 arguments를 사용한 단일 키가 만들어진다. (좀 더 복잡한 경우라면 또 다른 해싱 함수가 필요할 수 있다. )
◾️ (**)로 표시한 줄에선 func.call(this, ...arguments)를 사용해 컨텍스트(this)와 래퍼가 가진 인수 전부(...arguments)를 기존 함수에 전달하였다.
func.apply
◾️ func.call(this, ...arguments) 대신, func.apply(this, arguments)를 사용해도 된다.
◾️ func.apply문법
func.apply(context, args);
◾️ apply는 func의 this를 context로 고정해주고, 유사 배열 객체인 args를 인수로 사용할 수 있게 해준다.
◾️ call과 apply의 문법적 차이는 call이 복수 인수를 따로따로 받는 대신 apply는 인수를 유사 배열 객체로 받는다.
◾️ 전개 문법을 사용해 인수가 담긴 배열을 전달하는 것과 call을 사용하는 것은 동일하다.
◾️ 전개 문법 ...은 이터러블 args를 분해 해 call에 전달할 수 있도록 해준다.
◾️ apply는 오직 유사 배열 형태의 args만 받는다.
func.call(context, ...args);
func.apply(context, args);
◾️ 인수가 이터러블 형태라면 call, 유사 배열 형태라면 apply를 사용하면 된다.
◾️ 배열 같이 이터러블이면서 유사 배열인 객체엔 둘 다를 사용할 수 있는데, 대부분의 자바스크립트 엔진은 내부에서 apply를 최적화 하기 때문에 apply를 사용하는게 좀 더 빠르다.
◾️ 콜 포워딩(call forwarding) : 컨텍스트와 함께 인수 전체를 다른 함수에 전달하는 것
let wrapper = function () {
return func.apply(this, arguments);
};
◾️ 외부에서 wrapper를 호출하면, 기존 함수인 func를 호출하는 것과 명확하게 구분할 수 있다.
메서드 빌리기
◾️ 해싱 함수 개선
function hash(args) {
return args[0] + ',' + args[1];
}
◾️ args의 요소 개수에 상관없이 요소를 합치기 → 배열 메서드 arr.join을 사용
function hash(args) {
return args.join(); // Error: arguments.join is not a function
}
◾️ hash(arguments)를 호출할 때 인수로 넘겨주는 arguments는 진짜 배열이 아니고 이터러블 객체나 유사 배열 객체이기 때문에 에러가 발생한다.
◾️ 메서드를 빌려와서 문제 해결 → method borrowing
function hash() {
console.log([].join.call(arguments)); // 1, 2
}
hash(1, 2);
◾️ 일반 배열에서 join 메서드를 빌려오고([].join), [].join.call를 사용해 arguments를 컨텍스트로 고정한 후 join메서드를 호출한다.
◾️ arr.join(glue)의 내부 알고리즘
1. glue가 첫 번째 인수가 되도록 한다. 인수가 없으면 ','가 첫 번째 인수가 된다.
2. result는 빈 문자열이 되도록 초기화 한다.
3. this[0]을 result에 덧붙인다.
4. glue와 this[1]를 result에 덧붙인다.
5. glue와 this[2]를 result에 덧붙인다.
6. this.length개의 항목이 모두 추가될 때까지 이 일을 반복한다.
7. result를 반환한다.
◾️ 기존에 call을 사용했던 방식처럼 this를 받고, this[0], this[1] 등이 합쳐진다. 이렇게 내부 알고리즘이 구현되어있기 때문에 어떤 유사 배열이던 this가 될 수 있다.
데코레이터와 함수 프로퍼티
◾️함수 또는 메서드를 데코레이터로 감싸 대체하는 것은 대체적으로 안전하다.
◾️ 그런데 원본 함수에 func.calledCount 등의 프로퍼티가 있으면 데코레이터를 적용한 함수에선 프로퍼티를 사용할 수 없으므로 안전하지 않다. 함수에 프로퍼티가 있는 경우엔 데코레이터 사용에 주의해야 한다.
◾️ 예시에서 함수 slow에 프로퍼티가 있었다면, cachingDecorator(slow) 호출 결과인 래퍼엔 프로퍼티가 없다.
◾️ 몇몇 데코레이터는 자신만이 프로퍼티를 갖기도 한다. 데코레이터는 함수가 얼마나 많이 호출되었는지 세거나 호출 시 얼마나 많은 시간이 소모되었는지 등의 정보를 래퍼의 프로퍼티에 저장할 수 있다.
◾️ 함수 프로퍼티에 접근할 수 있게 해주는 데코레이터를 만드는 방법도 있다. → Proxy 사용
📝 요약
◽️ 데코레이터는 함수를 감싸는 래퍼로 함수의 행동을 변화시킨다. 주요 작업은 여전히 함수에서 처리한다.
◽️ 데코레이터는 함수에 추가된 기능정도로 보면 된다. 하나 혹은 여러 개의 데코레이터를 추가해도 함수의 코드는 변경되지 않는다.
◽️ func.call(context, arg1, arg2...) - 주어진 컨텍스트와 인수를 사용해 func를 호출한다.
◽️ func.apply(context, args) - this에 context가 할당되고, 유사 배열 args가 인수로 전달되어 func가 호출된다.
◽️ 콜 포워딩(call forwarding)은 대개 apply를 사용해 구현한다.
◽️ 메서드 빌리기 : 특정 객체에서 메서드를 가져오고, 다른 객체를 컨텍스트로 고정한 후 함수를 호출(call)하는 형태
◽️ 메서드 빌리기는 배열 메서드를 빌려서 이를 arguments에 적용할 때 흔히 사용된다.
◽️ 나머지 매개변수와 배열을 함께 사용하면 유사한 기능을 구현할 수 있다.
📝 오늘 새롭게 배운 내용 📝
✅ 너무 새로운 내용이다.
✅ 다시 보고, 과제까지 풀어봐야할 거 같다.
📌 [JAVASCRIPT_INFO call/apply와 데코레이터, 포워딩]
call/apply와 데코레이터, 포워딩
ko.javascript.info
📌 [MDN call()]
Function.prototype.call()
call() 메소드는 주어진 this 값 및 각각 전달된 인수와 함께 함수를 호출합니다.
developer.mozilla.org
📌 [MDN apply()]
Function.prototype.apply()
apply() 메서드는 주어진 this 값과 배열 (또는 유사 배열 객체) 로 제공되는 arguments 로 함수를 호출합니다.
developer.mozilla.org
'모던 자바스크립트' 카테고리의 다른 글
6.10 화살표 함수 다시 살펴보기 (0) | 2020.09.02 |
---|---|
6.10 함수 바인딩 (0) | 2020.09.01 |
6.8 setTimeout과 setInterval을 이용한 호출 스케줄링 (0) | 2020.08.26 |
6.7 new Function 문법 (0) | 2020.08.25 |
6.6 객체로서의 함수와 기명 함수 표현식 (0) | 2020.08.24 |