-인자를 n개 받는 함수에 m개의 인자만 넘겨 기억시켰다가 나중에 나머지 (n-m)개의 인자를 넘기면 비로소 함수의 실행 결과를 얻을 수 있게끔 하는 함수
-예시(bind 메서드)
var add = function () {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = add.bind(null, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10)); //55
→addPartial 함수는 인자 5개를 미리 적용(첫번째 인자 null은 this에 대한 것)
→추후 나머지 인자들을 전달하면 모든 인자를 모아 원래의 함수가 실행
→add 함수는 this를 사용하지 않아 bind 메서드만으로 구현되었지만 this 값을 변경할 수 밖에 없어, 메서드에 사용하려면 this에 관여하지 않도록 만들어야 함
-예시(부분 적용 함수 구현 - 1)
var partial = function () {
var originalPartialArgs = arguments;
var func = originalPartialArgs[0];
if (typeof func !== 'function') {
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function () {
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
var restArgs = Array.prototype.slice.call(arguments);
return func.apply(this, partialArgs.concat(restArgs));
};
};
var add = function () {
var result = 0;
for (var i = 0; i <arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = partial(add, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10)); // 55
var dog = {
name: '강아지',
greet: partial(function(prefix, suffix) {
return prefix + this.name + suffix;
}, '왈왈, ')
};
dog.greet('입니다!'); // 왈왈, 강아지입니다!
→addPartial에 partial 함수에 적용할 원본 함수와 미리 적용할 인자들을 전달
→이후 addPartial에 나머지 인자들을 partial 함수가 반환하는 함수(부분 적용 함수)에 넘겨주어 이들을 한데 모아(concat) 원본 함수(add) 호출(apply)
→실행 시점의 this를 그대로 반영하여 this에는 아무런 영향을 주지 않음
-예시(부분 적용 함수 구현 - 2): 추후에 넘길 인자의 위치를 정하기
Object.defineProperty(window, '_', {
value: 'EMPTY_SPACE',
writable: false,
configurable: false,
enumerable: false
});
var partial2 = function () {
var originalPartialArgs = arguments;
var func = originalPartialArgs[0];
if (typeof func !== 'function') {
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function () {
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
var restArgs = Array.prototype.slice.call(arguments);
for (var i = 0; i < partialArgs.length; i++) {
if(partialArgs[i] === _) {
partialArgs[i] = restArgs.shift();
}
}
return func.apply(this, partialArgs.concat(restArgs));
};
};
var add = function () {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = partial2(add, 1, 2, _, 4, 5, _, _, 8, 9);
console.log(addPartial(3, 6, 7, 10)); // 55
var dog = {
name: '강아지',
greet: partial2(function(prefix, suffix) {
return prefix + this.name + suffix;
}, '왈왈, ')
};
dog.greet(' 배고파요!'); // 왈왈, 강아지 배고파요!
→전역객체에서 Object.defineProperty를 이용해 '_'를 비워놓음의 의미로 준비(삭제, 변경 등 접근에 대한 방어 차원에서 여러 프로퍼티 설정 해둠)
→또, partial2 함수 내에서 if 문을 통해 인자가 _인 부분을 골라내어 restArgs의 자리인 것을 명시적으로 나타냄
※ '_'문자를 '비워놓음'의 의미로 바꿀 때, Object.defineProperty 외에 Symbol.for을 사용해도 됨
(function () {
var EmptySpace = Symbol.for('EMPTY_SPACE'); // 기존 전역 심볼공간에 'EMPTY_SPACE'라는 심볼을 새로 생성
console.log(EmptySpace);
})();
(function () {
console.log(Symbol.for(EmptySpace)); // 기존 전역 심볼공간에 'EMPTY_SPACE'라는 심볼이 있으므로 해당 값을 참조
})();
return function () {
// ...
for (var i = 0; i < partialArgs.length; i++)
if(partialArgs[i] === Symbol.for('EMPTH_SPACE')) { // 바뀐 부분
partialArgs[i] = restArgs.shift();
}
}
// ...
};
// ...
var _ = Symbol.for('EMPTY_SPACE'); // 추가된 부분, '_'의 심볼을 'EMPTY_SPACE'로 설정
var addPartial = partial(add, 1, 2, _, 4, 5, _, _, 8, 9);
console.log(addPartial(3, 6, 7, 10));
-디바운스: 짧은 시간동안 동일한 이벤트가 많이 발생할 경우 모두 처리하지 않고 가장 처음 또는 마지막 이벤트에 대해 한 번만 처리
→scroll, wheel, mousemove, resize 등에 적용
→실무에서 부분 적용 함수 사용에 적합
-예시(디바운스)
var debounce = function (eventName, func, wait) {
var timeoutId = null;
return function (event) {
var self = this;
console.log(eventName, 'event 발생');
clearTimeout(timeoutId);
timeoutId = setTimeout(func.bind(self, event), wait);
};
};
var moveHandler = function (e) {
console.log('move event 처리');
};
var wheelHandler = function (e) {
console.log('wheel event 처리');
};
document.body.addEventListener('mousemove', debounce('move', moveHandler, 500));
document.body.addEventListener('mousewheel', debounce('wheel', wheelHandler, 700));
→디바운스 함수는 어떤 이벤트가 발생했는지 출력하기 위한 eventName, 이벤트에 따라 실행할 함수인 func, 마지막으로 발생한 이벤트인지 판단하기 위한 대기시간인 wait(ms)를 인자로 받음
→timeoutID의 초기값은 null로 주며 선언
→EventListener에 의해 호출될 함수를 return
→self에 this 객체를 미리 저장해둠
→함수 내부에서는 어떤 이벤트가 발생했는지 eventName을 출력
→clearTimeout()을 사용하여 대기큐를 무조건 초기화해주는데, 처음 이벤트 발생 후 함수를 실행시킬 대기 시간 이전에 반복해서 이벤크가 발생한다면, 여기서 대기큐를 초기화시켜 다시 대기 시간을 계산할 수 있게 해줌
→마지막으로 setTimeout()을 사용하여 대기시간(wait)만큼 지연시킨 후 func 호출, 대기시간 이전에 동일한 이벤트 발생 시 반환할 함수가 다시 실행되며 윗 줄에서 clearTimeout되고 다시 대기시간 계산
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
fruits.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function () {
alert('your choice is' + fruit);
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
-ul 생성, ul 내부에 fruits 목록에 있는 과일을 하나씩 li로 추가
→li에 fruits 목록의 값을 하나씩 추가하는 forEach 함수는 외부 변수를 사용하지 않으므로 클로저가 없음
-li를 클릭하면 "'your choice is' + fruit" 출력, 이때, fruit는 addEventListener의 콜백 함수 function 내부에서 외부의 변수를 참조하여 사용
→따라서, addEventListener의 콜백 함수에는 클로저가 있음
-예시 2)
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var alertFruit = function(fruit) {
alert('your choice is' + fruit);
};
fruits.forEach(function(fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit);
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
-공통적으로 사용되는 함수인 alertFruit를 콜백 함수 외부로 꺼냄
-마지막 코드인 alertFruit(fruits[1])에서 fruits[1]은 정상적으로 'banana'를 불러와 수행
-하지만 클릭 시 수행되는 alertFruit는 대상 과일명이 아닌 [object PointerEvent]가 출력됨
→콜백 함수의 인자에 대한 제어권이 addEventListener한테 있는 상태
→addEventListener는 콜백 함수 호출 시 첫번째 인자에 이벤트 객체를 넣기 때문에 alertFruit에 이벤트 객체 [Object PointerEvent]가 들어감
→bind 메서드로 해결 가능
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var alertFruit = function(fruit) {
alert('your choice is' + fruit);
};
fruits.forEach(function(fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit.bind(null,fruit));
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
→bind 메서드를 사용하면 첫번째 인자는 바인딩할 this인데 이 값은 생략할 수 없으므로 null로 정의해야하고, 이 때문에 원래의 this를 유지할 수 없는 경우가 많음
→두번째 인자에는 이벤트 객체가 넘어옴
...
var alertFruit = function(fruit, s) {
alert('your choice is' + fruit + ' ' + s);
};
...
→다음과 같이 임의의 변수 s를 지정해주면 bind의 인자로 null로 정의한 this와 fruit 외에 암시적으로 이벤트 객체를 넘겨줬음을 알 수 있음
→해결을 위해 고차함수를 활용하면 됨
...
var alertFruitBuilder = function (fruit) {
return function () {
alert('your choice is ' + fruit);
};
};
fruits.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruitBuilder(fruit));
$ul.appendChild($li);
});
...
→alertFruitBuilder로 정의한 함수에서 익명 함수를 반환하는데 이 익명함수가 기존의 alertFruit 함수임
→addEventListener의 콜백 함수로 alertFruitBuilder를 지정하면 alertFruitBuilder는 fruit값을 받아 alert를 띄우는 함수를 반환함
→결국, fruit를 받아 alert를 띄우는 함수가 addEventListener의 콜백 함수가 되고, 클릭이 발생했을 때, alert('your choice is ' + fruit)가 바로 fruit를 참조함
→alertFruitBuilder가 반환하는 함수에는 클로저가 존재함
2) 접근 권한 제어(정보 은닉)
-정보 은닉: 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화
→모듈 간의 결합도 낮추고 유연성 높임
-접근 권한의 종류
→public: 외부에서 접근이 가능
→private: 내부에서만 사용하며 외부에 노출되지 않는 것
→protected: private이지만 동일 패키지의 클래스나 하위 클래스 관계에서 접근 가능, 거의 사용 안함
-함수 내에서 return을 사용하여 선택적으로 일부 변수에만 접근 권한을 부여할 수 있음
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());
→outer라는 변수를 통해 outer 함수를 실행할 수는 있지만, outer 함수 내부에는 개입 불가
→외부에서는 오직 outer함수가 return한 정보(inner)에만 접근 가능
→return한 변수들은 공개 멤버, return하지 않은 정보는 비공개 멤버
-예시(자동차 경주 게임)
→규칙 1) 각 턴마다 주사위를 굴려 나온 숫자(km)만큼 이동
→규칙 2) 차량별 연료(fuel), 연비(power)는 무작위
→규칙 3) 남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동 못함
→규칙 4) 모든 유저가 이동할 수 없는 턴에 게임 종료
→규칙 5) 게임 종료 시점에 가장 멀리 이동해 있는 사람이 승리
// 규칙에 따라 생성한 자동차 객체
var car = {
fuel: Math.ceil(Math.random() * 10 + 10),
power: Math.ceil(Math.random() * 3 + 2),
moved: 0,
run: function () {
var km = Math.ceil(Math.random() * 6);
var wasteFuel = km / this.power;
if (this.fuel < wasteFuel) {
console.log('이동불가');
return;
}
this.fuel -= wasteFuel;
this.moved += km;
console.log(km + 'km 이동 (총 ' + this.moved + 'km)');
}
};
→랜덤으로 설정되는 fuel, power와 이동거리인 moved에 접근이 가능하여 다음과 같이 사용자가 조작 가능
→문제 해결을 위해 자동차를 객체가 아닌 함수로 만들고, 필요한 멤버만 return하면 됨
// 규칙에 따라 생성한 자동차 함수
var createCar = function () {
var fuel = Math.ceil(Math.random() * 10 + 10);
var power = Math.ceil(Math.random() * 3 + 2);
var moved = 0;
return {
get moved () {
return moved;
},
run = function () {
var km = Math.ceil(Math.random() * 6);
var wasteFuel = km / power;
if (fuel < wasteFuel) {
console.log('이동불가');
return;
}
fuel -= wasteFuel;
moved += km;
console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel);
}
};
};
var car = createCar();
→fuel과 power는 비공개 멤버로 함수 외부에서 접근할 수 없고, moved는 getter만 부여하여 읽기 전용
→외부에서는 moved를 읽는 것과 run 메서드를 실행하는 것만 가능
console.log(car.moved) // 지금까지 총 이동거리 출력
consoel.log(car.fuel) // undefined
consoel.log(car.power) // undefined
car.fuel = 1000; // car.fuel에 1000을 대입할 수 있고
console.log(car.fuel); // 출력하면 대입한 값인 1000이 나오지만
car.run(); // run함수에까지 영향을 미치지는 못함, power와 moved도 마찬가지
→여전히 run 메서드를 다른 내용으로 덮어씌우는 어뷰징은 가능한 상태이고 이것까지 막으려면 freeze 등의 메서드 사용
→따라서, outer2()를 실행할 때마다 ++a, 즉 a에 1씩 더해져 반환되고 반복해서 실행하면 계속 1씩 더해진 값을 가질 수 있음
→outer 함수의 실행 종료 시점에서 inner 함수를 반환하고 inner 함수를 반환하고 나면 outer 함수는 종료된 상태
→이 때, inner 함수는 outer2에 저장되어 outer2를 실행하면 언제든 호출될 가능성이 열린 것이므로 inner 함수가 참조해야하는 outer 함수의 LexicalEnvironment는 가비지 콜렉터의 수집대상에서 제외됨
-예제 3)을 통해 알 수 있는 클로저의 정의
→어떤 함수 A(outer)에서 선언한 변수(a)를 참조하는 내부함수 B(inner)를 외부로 전달(outer2에 inner 함수 자체를 반환)할 경우, A의 실행 컨텍스트 종료 이후에도 변수가 사라지지 않는 현상
→이때, 외부로 전달이 return만 의미하는 것은 아님
-예시 4)
( function () {
var a = 0;
var intervalId = null;
var inner = function () {
if (++a >=10 ) {
clearInterval(intervalId);
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
→setInterval에 전달할 콜백 함수 inner는 inner 외부에서 선언된 변수 a를 참조
-예시 5)
( function () {
var count = 0;
var button = document.createElement('button');
button.innerText = 'click';
button.addEventListener('click', function () {
console.log(++count, 'times clicked');
});
document.body.appendChild(button);
})();
→'click'이라고 쓰인 버튼을 생성하고, 버튼을 클릭할 때마다 count가 1씩 더해진 값과 'times clicked' 문자가 함께 출력
→이때, addEventListener에서 실행할 함수 내부에서 외부에 있는 변수 count를 참조함
-예제 5), 6) 모두 지역변수(a, count)를 참조하는 내부함수(inner, function)를 외부(setInterval, button)으로 전달했기 때문에 클로저
2. 클로저와 메모리 관리
-클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수가 메모리를 소모하도록(a가 사라지지 않고 계속 참조되도록 하는 등) 함으로써 발생
-필요성이 사라진 시점에서 메모리를 더 소모하지 않도록 해주면 됨
-참조 카운트를 0으로 만들어 더 이상 참조되지도 않고 쓰이지도 않게 된다면 메모리가 회수될 것
-이 때, 참조 카운트를 0으로 만드는 방법으로 null이나 undefined를 할당하면 됨
-예시 1)
var outer = (function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조를 끊음
→outer 함수를 null로 정의하며 outer 함수 내부에서 변수 a를 참조하던 inner 함수를 없애며 a를 참조하는 컨텍스트가 아무것도 없도록 만듦
-예시 2)
( function () {
var a = 0;
var intervalId = null;
var inner = function () {
if (++a >=10) {
clearInterval(intervalId);
inner = null; // inner 식별자의 함수 참조를 끊음
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
→함수 내부에서 변수를 참조하던 내부함수 inner 자체를 null로 정의하면 함수 내부변수 a룰 참조하는 것이 없도록 만듦
-예시 3)
( function () {
var count = 0;
var button = dovument.createElement('button');
button.innerText = 'click';
var clickHandler = function () {
concole.log(++count, 'times clicked');
if (count >= 10) {
button.removeEventListener('click', clickHandler);
clickHandler = null; // clickHandler 식별자의 함수 참주 끊음
}
};
button.addEventListener('click', clickHandler);
document.body.appendChild(button);
})();
→count 변수를 계속 참조하며 button에 eventListener로 주는 함수 clickHandler를 null로 정의하며 count를 참조할 컨텍스트를 없앰
var coffeeList = '';
var addEspresso = function (name) {
coffeeList = name;
console.log(coffeeList);
setTimeout(addAmericano, 500, '아메리카노');
};
var addAmericano = function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
setTimeout(addMocha, 500, '카페모카');
};
var addMocha = function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
setTimeout(addLatte, 500, '카페라떼');
};
var addLatte = function (name) {
coffeeList += ', ' + name;
console.log(coffeeList);
};
setTimeout(addEspresso, 500, '에스프레소');
-가독성이 높아지고, 위에서 아래로 읽어내려가는대로 출력되어 어색함도 사라짐
-일회성 함수를 매번 변수에 할당하는 것이 복잡해 보일 수 있음
(2) promise
-방법 1
new Promise(function (resolve) {
setTimeout(function () {
var name = '에스프레소';
console.log(name);
resolve(name); // promise가 fufilled(성공)일 때, name 반환
}, 500);
}).then(function (prevName) { // promise.then에서 위의 resolve 결과를 받아 인자로 사용
return new Promise(function (resolve) {
setTimeout(function () {
var name = prevName + ', 아메리카노'; // 인자로 받은 초기 name에 '아메리카노' 추가
console.log(name);
resolve(name); // 아메리카노가 추가된 name을 다시 반환
}, 500);
});
}).then(function (prevName) { // 위에서 resolve로 넘겨준 아메리카노가 추가된 name을 인자로 사용
return new Promise(function (resolve) { // 이후 반복
setTimeout(function () {
var name = prevName + ', 카페모카';
console.log(name);
resolve(name);
}, 500);
});
}).then(function (prevName) {
return new Promise(function (resolve) {
setTimeout(function () {
var name = prevName + ', 카페라떼';
console.log(name);
resolve(name);
}, 500);
});
});
-resolve 또는 reject가 있으면 둘 중 하나가 실행되기 전까지 다음 then 또는 catch로 넘어가지 않으므로 비동기 작업의 동기적 표현이 가능
-방법 2
var addCoffee = function (name) {
return function (prevName) {
return new Promise(function (resolve) {
setTimeout(function () {
var newName = prevName ? (prevName + ', ' + name) : name;
console.log(newName);
resolve(newName);
}, 500);
});
};
};
addCoffee('에스프레소')()
.then(addCoffee('아메리카노'))
.then(addCoffee('카페모카'))
.then(addCoffee('카페라떼'));
-방법 1에서 반복적이던 내용을 함수화해서 짧게 표현
(3) Generator
var addCoffee = function(prevName, name) {
setTimeout(function () {
coffeeMaker.next(prevName ? prevName + ', ' +name : name);
}, 500);
};
var coffeeGenerator = function* () { // Generator 함수
var espresso = yield addCoffee('', '에스프레소');
console.log(espresso);
var americano = yield addCoffee(espresso, '아메리카노');
console.log(americano);
var mocha = yield addCoffee(americano, '카페모카');
console.log(mocha);
var latte = yield addCoffee(mocha, '카페라떼');
console.log(latte);
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();
-Generator 함수를 실행하면 Iterator가 반환되며
-Iterator는 next라는 메서드를 가짐
-next를 실행하면 Generator 함수 내 가장 첫번째 yield에서 함수의 실행을 멈춤
-이후 다시 next 메서드 호출하면 멈췄던 부분부터 시작해서 다음 yield에서 함수의 실행을 멈춤
-비동기 작업이 완료되는 시점(console에 name 출력)마다 next를 호출하면 Generator 함수가 위에서 아래로 순차적으로 진행됨
(4) promise + async/await
var addCoffee = function (name) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(name);
}, 500);
});
};
var coffeeMaker = async function () {
var coffeeList = '';
var _addCoffee = async function (name) {
coffeeList += (coffeeList ? ',' : '') + await addCoffee(name);
};
await _addCoffee('에스프레소');
console.log(coffeeList);
await _addCoffee('아메리카노');
console.log(coffeeList);
await _addCoffee('카페모카');
console.log(coffeeList);
await _addCoffee('카페라떼');
console.log(coffeeList);
};
coffeeMaker();
→map 메서드의 콜백 함수는 기본적으로 (요소, 인덱스, [, 배열], [, thisArg])의 순서로 인자를 받도록 되어 있음
→즉, map 메서드 내의 콜백 함수에서 (index, currentValue)로 (인덱스, 요소)의 순서로 이름을 붙였다고 해도 이름만 붙인 것일 뿐
실제로, 컴퓨터에서는 index라고 이름 붙은 요소, currentValue라고 이름 붙은 인덱스라고 인식함
→따라서, index, currentValue 순서로 출력을 명령하면 (index가 아닌 요소, currentValue가 아닌 인덱스)의 순서로 출력됨
→또한, +5가 더해지는 주체도 currnetValue의 이름이 붙은 인덱스이므로 0, 1, 2에 각각 5씩 더해져 새로운 배열에 저장됨
-콜백 함수의 제어권을 넘겨받은 코드(map)은 콜백 함수 인자의 순서(요소, 인덱스)에 대한 제어권을 가짐
3) this에 대한 제어권
-콜백 함수도 함수이기 때문에 별도로 this를 지정하지 않으면 this에는 기본적으로 전역객체가 설정됨
-예시
Array.prototype.map = function(callback, thisArg) {
var mappedArr = [];
for (var i = 0; i < this.length ; i++) {
var mappedValue = callback.call(thisArg || window, this[i], i, this);
mappedArr[i] = mappedValue;
}
return mappedArr;
};
-callback 함수의 제어권을 넘겨받는 코드(function())에서 this 객체를 thisArg에서 지정해준다면, 콜백 함수를 call할 때, 콜백 함수의 this를 function()에서 넘겨준 thisArg를 지정하고, 넘겨주지 않으면 기본적으로 설정된대로 전역객체를 지정함
-obj.logValues(1, 2): obj 객체의 logValues 메서드로서 정의되어 this는 obj 객체를 가리키고 1과 2는 각각 function의 인자 v와 i로 할당되어 출력됨
-[4, 5, 6].forEach(obj.logValues): obj,logValues를 forEach의 콜백 함수로 사용하여, obj 객체의 logValues 메서드가 아닌 obj.logValues가 담고 있는 함수 function만 전달됨
→obj 객체와의 직접적인 연관이 없어지고 forEach에서 별도의 this 객체를 지정하지 않아 this는 전역객체를 가리키게 됨
→객체의 메서드를 전달하더라도 콜백 함수로서 전달하면 메서드가 아닌 함수로 전달됨
4. 콜백 함수 내부의 this에 다른 값 바인딩
-위에서 봤듯이, 원래 객체의 메서드를 콜백 함수로 전달하면 해당 객체를 this로 설정할 수 없음
-이때, 콜백 함수 내부에서 메서드의 객체를 this로 설정하는 방법
-전통적인 방법: 메서드 내에서 임의의 변수(self 등)에 this를 담아 메서드를 담은 객체를 this로 설정
var obj1 = {
name: 'obj1',
func: function() {
var self = this;
return function() {
console.log(self.name);
};
}
};
var callback = obj1.func();
setTimeout(callback, 1000);
-setTimeout 함수의 콜백 함수로 obj1 객체의 func 메서드를 주어 함수로 변경되어 this가 전역 객체를 가리켜야 하지만, 메서드 내에서 self에 this를 할당했으므로 self.name은 obj1 객체의 name을 가리키게 됨
-따라서, 1000ms(1초) 뒤에 'obj1'이 출력됨
// self라는 임의의 변수 생성없이 하드코딩하는 방법도 있지만, this를 이용한 재활용을 할 수 없게 됨
var obj1 = {
name: 'obj1',
func: function() {
console.log(obj1.name);
}
};
setTimeout(obj1.func, 1000);
-var self = this로 지정한 것을 재활용하는 예시
var obj1 = {
name: 'obj1',
func: function() {
var self = this;
return function() {
console.log(self.name);
};
}
};
// obj1의 func를 복사한 것을 obj2내의 func로 사용
var obj2 = {
name: 'obj2',
func: obj1.func
};
var callback2 = obj2.func();
setTimeout(callback2, 1500);
// obj3은 name만 지정한 후, call을 사용하여 obj1의 func를 obj3에 적용
var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
→obj1 객체 내에서 func 메서드의 this를 self로 지정해놨으므로 obj2, obj3에 복사되어 이용될 때, self에는 obj1의 func를 호출할 때의 각 객체, obj2와 obj3이 각각 할당됨
→따라서, self.name에는 obj2.name, obj3.name이 설정되어 1.5초, 2초 뒤에 'obj2', 'obj3'이 각각 출력됨
-this.name을 obj1.name으로 설정하면 obj1 객체의 func를 콜백 함수로 사용할 때는 obj1 객체를 콜백함수의 this로 지정할 수 있지만, obj2, obj3 등에서 func를 콜백함수로써 재활용하지 못함
-하지만, this를 self 등의 변수에 넣어두면, 메서드를 호출하는 객체가 바뀔 때마다 메서드의 this를 해당 객체로 바꿔서 지정해줄 수 있으므로 재활용이 가능해짐
-전통적인 방법 외에 bind를 사용하는 방법
var obj1 = {
name: 'obj1',
func: function() {
console.log(this.name);
}
};
setTimeout(obj1.func.bind(obj1), 1000);
// obj1 객체의 func 메서드를 obj2에 bind하여 사용
var obj2 = { name: 'obj2'};
setTimeout(obj1.func.bind(obj2), 1500);
→setTimeout(obj1.func.bind(obj1), 1000): obj1 객체의 func 메서드가 콜백함수로 쓰여 함수의 형태가 되고, this 객체를 주지 않았으면 전역객체가 this로 할당되었겠지만, bind를 통해 obj1 객체를 bind해주어 this에 obj1객체가 할당됨.
2. home.ctrl.js 파일에서 process내에 register 함수를 보면 user클래스 내의 reigster가 실행되도록 되어 있음
// home.ctrl.js
const process = {
login: async (req, res) => {
const user = new User(req.body);
const response = await user.login();
return res.json(response);
},
register: (req, res) => {
const user = new User(req.body);
const response = user.register();
return res.json(response);
},
};
3. User.js 파일의 user 클래스에서 register()함수를 보면 save메서드를 UserStorage 파일에서 save 메서드를 호출하도록 되어 있음
// User.js에서 user클래스 내부
register(){
const client = this.body;
const response = UserStorage.save(client); // constructor에서 넘겨준 body를 그대로 저장
return response;
}
4. UserStorage.js에서 save메서드를 "/register"경로에서 입력받은 값을 파일에 저장하는 메서드로 구현
// UserStorage.js
// save 메서드를 실행할 때 users 데이터 전체를 넘겨주기 위해 코드 수정
static getUsers(isAll, ...fields) {
return fs
.readFile("./src/databases/users.json") // 해당 경로에서 파일 읽어오기
.then((data) => { // 파일 읽어오는 것을 성공하면
return this.#getUsers(data, isAll, fields); // 은닉화하여 생성한 함수 #getUsers 실행
})
.catch(console.error); // 파일 읽기에 실패하면 error 반환
}
// getUsers 메서드에서 파일 읽어오는 것에 성공했을 때 시행할 명령 정리
static #getUsers(data, isAll, fields) {
const users = JSON.parse(data);
if (isAll) return users; // isAll이 true이면 전체 데이터 바로 반환하기
const userUsers = fields.reduce((newUsers, field) => { // 각각의 id, pw, name 필드 각각 반환
if (users.hasOwnProperty(field)) {
newUsers[field] = users[field];
}
return newUsers;
}, {});
return userUsers;
}
// 데이터를 그냥 추가하면 원래의 데이터가 사라지고 덮어씌워지므로,
// 원래의 데이터를 불러와서 새로운 데이터를 추가하고,
// 새로운 데이터를 추가한 데이터 전체를 파일에 저장해야됨
// users 데이터를 모두 불러오는 메서드(위에 있는 getUsers) 사용하여 getUsers가 반환한 모든 파라미터 다 받아오기
// 데이터를 읽어오는데 시간이 걸리므로 다 읽어올 때 까지 기다리도록 async 함수로 선언하고 getUsers 메서드는 await로 처리
static async save(userInfo) {
const users = await this.getUsers(true);
if (users.id.includes(userInfo.id)) {
throw "이미 존재하는 아이디입니다."; // error를 통해 명령하면 object로 반환되므로 문자열 그대로 넘겨주기
}
users.id.push(userInfo.id);
users.name.push(userInfo.name);
users.pw.push(userInfo.pw);
fs.writeFile("./src/databases/users.json", JSON.stringify(users));
return { success: true };
}
5. 위에서 회원가입을 실행하기 위해 거치는 다른 파일들에도 asynx, awiat를 걸어 파일을 읽어오는 시간동안 기다리도록 명령
// User.js의 user 클래스 내부
async register(){ // async 함수로 변경
const client = this.body;
const response = await UserStorage.save(client); // awiat 처리
return response;
}
// home.ctrl.js
const process = {
login: async (req, res) => {
const user = new User(req.body);
const response = await user.login();
return res.json(response);
},
register: async (req, res) => { // async 함수로 변경
const user = new User(req.body);
const response = await user.register(); // awiat 처리
return res.json(response);
},
};
6. 또한, User.js 파일에서 register 메서드는 이미 존재하는 아이디에 대한 예외 처리 코드 추가
// User.js의 user 클래스 내부
async register() {
const client = this.body;
try {
const response = await UserStorage.save(client);
return response;
} catch (err) { // 에러 발생 시, UserStorage에서 throw해준 메세지, "이미 존재하는 아이디입니다."를 출력
const a = { success: false, msg: err };
console.log(a.msg);
return a;
}
}
7. 결과
-서버 실행 후 "/register" 경로에서 아이디, 이름, 비밀번호, 비밀번호 확인 입력 후 SIGN UP을 누르면, users.json파일에 해당 정보 저장
-원래 users.json
-회원가입
-회원가입 후 users.json(회원가입 창에서 새로 입력하고 SIGN UP을 누른 아이디, 이름, 비밀번호가 추가로 저장되어 있음)
4. User.js 파일에서 login()함수를 UserStorage함수가 바뀐 것에 따라 바꿔주기
// User.js
"use strict";
class User{
// constructor 생략
async login() {
const client = this.body
// UserStorage에서 getUserInfo를 실행할 때, json파일을 불러올 때 시간이 걸림
// javascript는 비동기 처리를 하므로 json파일을 다 불러올 때까지 기다리지 않고 이 id, pw 비교 작업을 수행
// 그에 따라 아직 불러오지 못한 json파일과 비교하며 undefined가 됨
// 이를 방지하기 위해 await를 써서 UserStorage.getUserInfo가 다 시행될 때까지 기다리는 명령을 부여
// 이때, await는 async 함수에서만 사용이 가능하므로 login() 함수를 async login()으로 변경
const {id, pw} = await UserStorage.getUserInfo(client.id);
if (id) {
if (id === client.id && pw === client.pw) {
return {success: true};
}
return {success: false, msg: "비밀번호가 틀렸습니다."};
}
return {success: false, msg: "존재하지 않는 아이디입니다."};
}
// register() 생략
}
module.exports = User;
-await를 사용하지 않고 console.log(fs)를 통해 promise로 설정된 fs를 출력하면 promise: {<pending>}이 출력
※ "promise: {}": 데이터를 전부 읽어오지 못했다는 의미
-또한, User.js파일에서 UserStorage.getUserInfo(client.id)를 console.log로 출력하면 아직 json파일을 불러오지 못해 id가 정의되지 않음
→undefined가 뜸
5. controller인 home.ctrl.js파일에서도 login 메서드를 실행할 때 login 메서드가 User.js 파일 내에서 다 실행될 때까지 기다리도록 await 적용
// home.ctrl.js
const process = {
login: async (req, res) => {
const user = new User(req.body);
const response = await user.login();
return res.json(response);
},
//register 생략
};