🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [26]일차
🚀26일차에는 자바스크립트에서 쓰이는 프로토타입의 개념과 활용방법, 단축평가로 다양한 연산자들을 알아보고, 중요한 개념인 비동기 처리를 실습할 수 있었다.
비동기 처리를 이해하기 위해 회고를 진행하면서 검색과 강의를 함께 찾아봐야겠다고 느꼈다.
비구조화 할당
- ( 객체.속성명 ) 으로 꺼내던 방식을 한꺼번에 꺼내서 쓸 수 있도록 변수에 값들을 바로 할당
➡️(즉 구조를 분해해서 필요한 값들을 자동으로 할당해주는 기능) - 많은 값을 가질 수 있고, 특히 DOM 구조에서 이 비구조화 할당이 많이 쓰임
ex. HTML 태그에서 <input> 태그 사용 시 ( name, value, class, id ) 같은 속성들을
비구조화 할당을 사용하면 원하는 속성값만 꺼내서 사용할 수 있다.
프로토타입 Prototype
- 객체 간 속성이나 함수를 공유할 수 있게 함
- 메모리를 절약하면서 공통된 동작을 구현할 수 있음 (코드 중복을 방지)
객체 생성자
- 함수를 통해서 새로운 객체를 만들고 그 안에 넣고 싶은 값 혹은 함수들을 구현할 수 있게 해줌
- 보통 함수의 이름을 대문자로 시작
- 새로운 객체를 만들때에는 new 키워드를 앞에 붙여서 생성
▶️실습 - 객체 생성자 (프로토타입 사용 전)
function Animal(type, name, sound) {
this.type = type;
this.name = name;
this.sound = sound;
this.say = () => console.log(this.sound);
}
const dog = new Animal("개", "멍멍이", "왈왈!");
dog.say(); // 왈왈
- 객체 생성자 함수를 사용하면, 특정 함수나 값을 재사용가능
dog.say = () => console.log("I don't care");
cat.say();
- dog.say() 처럼 함수를 호출하는 부분을
dog.say = () => console.log("I don't care"); 처럼 화살표함수로 출력을 변경하여도 - 함수도 객체로 취급되기때문에 new키워드를 사용하면 dog객체의 say()함수가 별도의 공간에서 가지게되어
이처럼 함수의 수행을 바꿔 출력할 수 있다. - 따라서 dog객체만의 say()함수 공간이 만들어지므로
cat.say()를 하면 기존에 프로토타입으로 정의했던 say()가 수행됨
- 정리하자면
➡️new 키워드를 통해 만들어지는 객체는 별도의 공간을 만들기때문에 say()라는 함수를 재정의하여도
기존의 프로토타입에서 정의된 say()함수 console.log(this.sound) 에는 영향이 없이
기존의 프로토타입이 수행될 수 있는 것이다. - 하지만 이러한 방식은 "계속해서 별도의 공간"을 생성해내기때문에 "공간 낭비"가 생길 수 있다.
➡️해결방법 : prototype 사용
▶️실습 - 객체 생성자 (프로토타입 사용)
Animal.prototype.say = () => console.log(this.sound);
Animal.prototype.sharedValue = 1;
// 함수 출력 바꿀때
Animal.prototype.say = () => console.log("바꿀 내용");
- 이를 해결하기 위해서기존 Animal객체에서 this.say 함수를 분리한 후
Animal객체의 프로토타입으로 함수를 생성해준다.
- Animal 생성자 함수의 "프로토타입 객체"에 sharedValue라는 속성을 추가하는 것이다.
이 결과 Animal로 생성된 모든 인스턴스 (dog, cat)은 이 속성을 공유하게 된다. - 이 속성은 공유되는 속성이기때문에 변경하게 되면 모든 인스턴스 (dog, cat)에도 적용이 된다.
- (dog.sharedValue = 1, cat.sharedValue = 1) :
➡️이 코드를 수행할때 dog, cat 인스턴스에 sharedValue라는 속성이 있는지 확인한 후
➡️그 속성이 없을 경우 Animal.prototype으로 가서 sharedValue라는 속성을 찾게 된다.
➡️Animal.prototype.sharedValue = 1; 처럼 설정했던 값이 존재하기 때문에 이 값이 반환되는 것이다.
▶️실습 - prototype 타입 객체에 새로운 속성 추가 (describe)
// 새로운 속성과 함수 추가
Animal.prototype.describe = function () {
console.log(`${this.name}는 ${this.type}입니다.`);
};
// dog, cat 인스턴스를 참조하여 프로토타입 함수 (describe()) 호출
dog.describe(); // "멍멍이는 강아지입니다."
cat.describe(); // "야옹이는 고양이입니다."
// dog 인스턴스에 속성을 직접 추가
dog.sharedValue = 100; // dog 인스턴스의 고유한 속성
console.log(dog.sharedValue); // 100 (자신(dog)의 속성 우선)
console.log(cat.sharedValue); // 1 (Animal.prototype.sharedValue = 프로토타입 값)
- sharedValue속성 말고도 describe라는 속성에 함수를 추가하여 이름과 타입을 출력할 수 있도록 한다.
- dog.describe():
➡️호출되었을때 해당 인스턴스(dog객체)에 describe라는 속성이 있는지 확인하는데 없기때문에
➡️Animal.prototype으로부터 describe를 찾게 된다.
➡️그 후에는 this.name, this.type을 통해 자기 자신을 가리키며 템플릿 리터럴로 출력하게 된다.
// 재정의하여 각자 다른 say()함수 공간을 가지도록함
console.log("\\n-----[프로토타입 재정의 함수]-----");
dog.sound = "강아지는 멍멍";
cat.sount = "고양이는 야옹";
dog.say();
cat.say();
cat.say = () => console.log("cat.say() 재정의 : 야옹야옹");
// cat객체만 재정의 후 출력 (cat 객체가 별도 공간으로 say()를 가지게됨)
console.log("\\n-----[cat객체 say() 재정의 출력]-----");
dog.say();
cat.say();
- dog.sound = "강아지는 멍멍";
➡️dog 인스턴스의 sound 속성을 재정의하여 "별도의 say()함수 공간을 가지게되고 자신의 sound 속성"을 출력 - cat.say = () => console.log("...");
➡️cat 인스턴스의 say() 함수를 재정의하여 "별도의 say() 함수 공간을 가지게 되고 자신의 say()"를 출력
▶️실습 - 프로토타입 상속받기 (prototype 상속 - ES5 이하 방식)
// 프로토타입 부여하기 (=객체 생성자 상속받기) (상속)
function Dog(name, sound) {
Animal.call(this, "개", name, sound);
}
Dog.prototype = Animal.prototype;
console.log("\\n-----[객체 생성자 상속받기]-----");
const dog2 = new Dog("허스키", "허스키:월월");
dog2.say();
- Animal.call(this, "개", name, sound);
➡️부모의 생성자 함수(Animal)을 호출하여 Dog 생성자 함수에서 "부모 생성자 함수의 초기화 코드를 실행하도록 함"
➡️부모 생성자 함수의 초기화 코드는 this.type = type; 처럼 받아온 값들을 this.type로 갱신시켜주는 과정을 하고 있음 - call() 함수 :
➡️자바스크립트의 모든 함수 객체에서 사용할 수 있는 내장함수.
➡️이 함수 사용으로 함수의 호출를 명시적으로 설정할 수 있도록 함
call(객체, arg1, arg2) 처럼 사용되는데, 이 경우 첫번째 인자로 객체를 전달하는데 이 코드에서는 this가 쓰였으므로 function의 Dog객체가 전달되는 것이다. - Dog.prototype = Animal.prototype;
➡️이렇게 연결하게되면 Dog의 인스턴스가 Animal의 프로토타입 함수 say()에 접근할 수 있게 된다.
따라서 dog2.say()를 호출하면 Animal.prototype.say 가 실행되는 것이다. - 이 코드는 각 프로토타입 객체가 완전히 공유되므로 한 쪽의 프로토타입이 변경되면 다른 쪽에도영향을 미칠 수 있다.
➡️해결방법 : 이 경우 Dog.prototype을 Animal.prototype의 복사본으로 설정한다. (Object.create() 사용)
❓extends 대신 이러한 상속방식을 사용한 이유
➡️ ES5 버전 이하에서도 사용되는 상속 방식이지만 프로토타입 객체와 생성자를 수동으로 설정해야하는 불편함이 존재➡️해결방법 : ES6에서 도입된 문법 extends 키워드는 더 간결한 상속 표현이 가능
▶️실습 - 프로토타입 상속받기 (extends 상속 - ES6 이후 방식)
- extends 키워드를 사용하여 상속을 더 명확하게 표현 가능
// 프로토타입 부여하기 (=객체 생성자 상속받기) (상속)
function Dog(name, sound) {
Animal.call(this, "개", name, sound);
}
class Dog extends Animal{
constructor(name, sound){
super("개", name, sound); // 부모생성자를 호출한다.
}
}
const dog2 = new Dog("허스키", "허스키:월월");
dog2.say();
❓call() 함수를 사용하는 이유
➡️생성자 함수로 상속받을때 부모 생성자의 초기화 코드를 "자식 생성자에서 재사용해야함"
이때 부모 생성자를 호출하여 초기화 코드를 통해 this로 자식 인스턴스를 명시적으로 가리키도록 하여
명확하게 자식 인스턴스와 연결하는 역할을 하는 것
이 call()함수를 사용하면 Animal 생성자의 초기화 코드들을 다시 작성할 필요없이
call()함수 호출만으로 코드 재사용이 가능
이렇게 상속하게되면 부모 생성자의 초기화작업을 "자식 생성자에서 수행"하게되어 상속 계층이 형성된다.
단축평가
- Short-Circuit Evaluation
- 단축평가를 이용하여 코드를 간단하게 바꿀 수 있음
논리연산자 (AND, OR)
▶️실습 - 단축평가 사용하기 전
// 샘플데이터
const food = { foodName: "치킨" };
// 1. 단축평가를 사용하지 않은 예
function getFoodName(food) {
// food가 없으면 food.foodName을 실행시키지 않음
if (!food) return "음식 이름이 없습니다.";
return food.foodName;
}
- if문장에서 food가 없으면 그 다음 return문 food.foodName을 실행하지 않을 것이다.
- 이 코드를 "단축평가"를 통해 더 간단하게 바꿀 수 있다.
▶️실습 - 단축평가 사용 (&& 연산자)
// 2. 단축평가를 사용한 예
function getFoodName(food) {
return food && food.foodName;
}
const food2 = null;
console.log(getFoodName(food2));
console.log(getFoodName());
- return food && food.foodName :
➡️food가 null, undefined, 혹은 값이 없다면 그 다음 문장을 실행하지 않음
➡️food가 애초에 없으면 && food.foodName의 문장을 실행하지 않게되는 것 - ex. console.log(true && "이전 문장은 true이므로 이 문장이 실행됩니다.")
➡️true로 인해 그 다음 문자열이 실행 - OR연산의 특징인 둘 중 하나의 문장이 true이면 true이므로 바로 반환되어
앞 문장이 true이면 뒤의 문장은 더 이상 수행안하는 특징을 이용한 것 - 반면 AND연산의 특징은 두 문장이 모두 true이어야하므로 반드시 뒤의 문장까지 실행
🚀실습 - 단축평가 응용
- 회원의 이름이 입력되지 않았을 경우 <GUEST>로 취급할 수 있음
const name = "Gordon";
const userName = name || "<GUEST>";
console.log(userName);
- "Gordon"이입력되었기때문에 “Gordon”이 출력
- ex. const name = null;
➡️userName의 논리연산자 부분에서 name이 없기때문에
나머지 문장이 true인지 판단하기 위해서 뒤 문장까지 실행하여 “<GUEST>”가 실행된다.
다양한 상황에서의 단축평가 (논리연산자 OR)
console.log("" || "Hello"); // JS에서 false판단 -> ""
console.log(0 || "Hello"); // JS에서 false 판단 -> 0
console.log(null || "Hello"); // JS에서 false 판단 -> null
console.log(undefined || "Hello"); // JS에서 false 판단 -> undefined
console.log("A" || "Hello"); // JS에서 값이 있기때문에 true 판단
console.log(1 || "Hello"); // JS에서 값이 있기때문에 true 판단
- 자바스크립트에서 (공백””, 0, null, undefined) 은 false로 판단
let a;
let b = null;
let c = undefined;
let d = 4;
let e = "test";
let result = a || b || c | d || e;
console.log(result);
- 출력 결과
➡️a, b, c 모두 false이므로 건너뛰다가 d에서 값을 만나 true가 되어
➡️뒤의 let e또한 값이 있음에도 이 문장이 true인 것으로 판단되어
OR연산의 특징처럼 “4”가 출력되고 수행이 끝난다.
// 샘플데이터
const person = { name: "", age: 0 };
// ||= 연산자
person.name ||= "이름없는 사람에게 이름을 부여"; // 1번 문장
person.name || (person.name = "이름없는 사람에게 이름을 부여"); // 1번문장과 동일한 기능을 수행
- person.name ||= "...";
➡️샘플데이터 person객체에서 name속성을 찾았지만 "공백"이므로 false
➡️다음 문장에서 true를 찾기 위해 수행. "이름 없는 사람에게 이름을 부여" 출력 - person.name || (person.name = "...");
➡️위의 할당 연산자와 같은 결과값을 가진다.
➡️||= 연산자를 통해 더 코드를 간단하게 작성할 수 있다.
?? 연산자 (널 병합 연산자)
- 널 병합 연산자를 의미
- null 또는 undefined인 경우에만 다음 문장을 실행 (=오른쪽 값을 반환)
- 이 연산자는 || 연산자 (OR연산자)와 비슷하지만
|| 연산자 (OR연산자) 는 [ false, 0, ""(공백) ] 등도 false로 간주해 왼쪽 피연산자를 무시
let value = null;
let result = value ?? "다음문장실행";
console.log(result); // "다음문장실행" 출력
value = null;
result = value ?? "다음문장실행";
console.log(result); // "다음문장실행" 출력
value = 0;
result = value ?? "다음문장실행";
console.log(result); // 0 출력
- value가 0일때를 제외하고는 "다음문장실행"이 수행됨을 알 수 있다.
- 0은 null이나 undefined가 아니기때문에
null이나 undefined일때만 다음 문장을 수행하는 ?? 연산자의 특징에 부합하지 않는다.
??= 연산자 (널 병합 할당 연산자)
- 널 병합 할당 연산자를 의미
- 왼쪽 변수가 null 또는 undefined일 경우에만 오른쪽의 값을 할당한다.
- 왼쪽 변수가 유효하다면(null, undefined 가 아니라면) 변경되지 않는다.
let age = null;
age ??= 21;
console.log(age);
- 변수 age 가 null이므로 변수 age에 21이라는 값을 할당한다.
const person = { name: "", age: 0 };
person.age ??= 21;
console.log(person);
- person.age 속성이 0의 값을 가지지만 0은 null이나 undefined가 아니므로 그대로 ➡️{ name: "", age: 0 } 출력
// 단축평가 - Object 테스트
function makeObj(obj) {
obj.name ??= "GUEST";
obj.age ??= 20;
return obj;
}
const person = { name: "", age: 0 };
person.name ||= "이름없는 사람에게 이름을 부여";
console.log(makeObj({})); // 이름, 나이 둘다 없음
console.log(makeObj({ name: "Barnes" })); // 이름은 있고 나이는 없음
console.log(makeObj(person));
- makeObj({}) : 이름과 나이가 둘다 없으므로 null로 처리되어 오른쪽의 값을 할당
- makeObj({name : "Barnes" }) : 이름은 있으므로 obj.name이 유효한 값이므로 변경되지 않고 "Barnes"로 출력
- makeObj(person); : name:""으로 공백이므로 person.name ||= "..."; 코드로 인해 "이름없는 사람에게 이름을 부여"가
속성 name의 속성값으로 할당된다.
➡️이름은 있으므로 변경되지 않고 "이름없는 사람에게 이름을 부여" 출력
➡️나이는 0 값을 가지지만 null이나 defined가 아니므로 그대로 변경되지 않고 "0" 출력
?. 연산자 (옵셔널 체이닝 연산자)
- Optional Chaining Operator (옵셔널 체이닝 연산자)
- ?. 연산자를 사용하면 조금 더 빠르게 객체의 속성으로 접근할 수 있고 조건 검사 코드 또한 간단해진다.
- 왼쪽 피연산자가 null 이나 undefined인 경우 수행을 멈추고 undefined를 반환하게된다.
➡️접근 대상이 null 이나 undefined일 경우 에러를 방지하고 안전하게 undefined를 반환하는 것 - 장점 : 간결하고 안전하게 조건에 해당하지 않는 속성을 찾아낼 수 있다.
▶️실습 - ?. 연산자 사용하기 전
// 샘플데이터
const person = {
name: "Muller",
job: {
title: "student",
manager: {
name: "Neuer",
},
},
};
// 1. ?. 연산자 사용 전 ( && 연산자 활용 )
function printManagerName(person) {
console.log(
person && person.job && person.job.manager && person.job.manager.name
);
}
printManagerName(person); // 1번 출력. person객체를 넣기
printManagerName({ name: "Min-Jae" }); // 2번 출력. 이름만 넣기
printManagerName({}); // 3번 출력. 아무 값도 넣지 않기
- printManagerName(person)
➡️모든 문장이 true인지 검사하기때문에 마지막 문장 person.job.manager.name에서 Neuer를 출력 - printManagerName( {name : "Min-Jae"} );
➡️person객체이므로 true이지만 person.job을 만나면 job 속성이 없으므로 false가 되어
undefined를 출력하고 실행을 멈춘다. - printManagerName({})
➡️빈 객체이므로 undefined를 출력하고 실행을 멈춘다.
▶️실습 - ?. 연산자 사용
// 2. ?. 연산자 사용 후 (위의 코드를 개선)
function printManagerName2(person) {
console.log(person?.job?.manager?.name);
}
printManagerName2(person); // person객체를 넣기
printManagerName2({ name: "Pavlovic" }); // 이름만 넣기
printManagerName2({}); // 아무 값도 넣지 않기
- ?. 연산자를 사용
- 코드를 더 간결하게 표현하여 조건에 해당하지 않는 속성을 찾아낼 수 있다.
?.() 함수
- 옵셔널 체이닝 연산자 '함수'
- 함수가 존재하면 함수를 수행, 없으면 함수를 수행하지 않는다.
// 3. ?.() - 함수 사용
console.log("\\n-----[?.() 함수]-----");
const isak = {
name: "isak",
admin() {
console.log("관리자");
},
};
const gordon = {
name: "gordon",
};
isak.admin?.(); // admin()함수가 있으면 실행하고 없으면 실행하지 않음
gordon.admin?.(); // admin()함수가 있으면 실행하고 없으면 실행하지 않음
- ?. 연산자를 활용한 함수또한 사용할 수 있다.
- 각 객체에서 admin() 함수가 존재하면 실행하고, 없으면 실행하지 않는 문구
?.[ ] 배열
- 옵셔널 체이닝 연산자 '배열'
- 배열의 속성이 존재하면 배열의 속성값을 꺼내고, 없으면 꺼내지 않는다.
// 4. ?.[] - 배열 사용
console.log("\\n-----[?.[] 배열]-----");
console.log(isak["age"]); // age 속성이 있을때만 값을 꺼내도록 함
const davies = {};
console.log(davies?.["name"]); // name 속성이 있을때만 값을 꺼내도록 함
- ?. 연산자를 활용한 배열 사용 방법이다.
- 각 객체에서 [ ] 안에 해당하는 속성이 존재하면 실행하고, 없으면 실행하지 않는 문구이다.
- ?.[”속성명”]; 처럼 사용
- 4가지 단축평가 연산의 출력 결과
자바스크립트의 비동기 처리
- 동기 방식 : 수행흐름이 한 작업의 수행이 끝난 후에야 다음 작업이 수행되는 순차적인 흐름을 가진다.
작업들을 동기적으로 처리하면 한 작업이 끝날때까지 다른 작업들은 중지 상태이므로 다른 작업을 수행할 수 없다. - 비동기 방식 : 동시에 작업을 처리할 수 있는 방식이며 작업흐름을 멈추지 않기 떄문에
동시에 여러작업을 처리할 수 있고 기다리는 과정에서도 다른 함수를 호출할 수 있다.
➡️💡자바에서 멀티스레드의 방식 (동시에 수행)
▶️실습 - 비동기 처리 전
// 1. 비동기 처리를 하지 않은 예제
function work() {
console.log("-----[작업시작]-----");
const start = Date.now();
for (let i = 0; i < 1000000000; i++) {}
const end = Date.now();
console.log(end - start + "ms"); // 걸린 시간 출력
console.log("-----[작업종료]-----");
}
work();
console.log("다음 작업");
- Date.now() : 현재 시간을 숫자 형태로 가져오는 자바스크립트의 내장 함수
- start에서 처음 시간을 저장해놓고, end에서 for문 수행이 끝난 시간을 저장해놓은 후
그 두 시간의 차이를 구하여 ‘ms’로 출력
- 이처럼 많은 연산량의 작업을 수행하는 중에도 다른 작업을 하고 싶다면
함수를 비동기 형태로 전환해주어야함. ➡️비동기 처리 : setTimeout 함수를 사용
▶️실습 - 비동기 처리 후
// 2. setTimeout 사용
function typing() {
console.log("타자치는 중...");
}
console.log("-----[wokring 작업시작]-----");
setTimeout(typing, 5000); // 5초 지연시간
console.log("-----[wokring 작업완료]-----");
- setTimeout(typing, 5000) :
➡️작업시작과 작업 종료 사이에 setTimeout() 함수를 통해 지연시간을 지정 - 출력은 작업시작 → 작업완료 → 타자치는중… 이 출력된다.
➡️setTimeout() 함수를 수행함으로써 비동기처리가 되어 이 함수는 "백그라운드"에서 동시에 작업하고 있는 중이다.
➡️하지만 setTimeout() 함수가 모두 끝나도 "작업완료"가 모두 출력된 후에야 출력이 된다.
⭐그 이유는 setTimeout() 함수가 Stack과 WebAPI를 거쳐 Callback Queue에 있다가 프로그램의 모든 작업이 끝내야
Event Loop가 다시 Stack으로 복귀시켜 setTimeout()함수의 작업을 완료시키기 때문이다.
setTimeout() 함수의 과정
- 1. 프로그램을 수행하면 Stack에 자바스크립트의 코드들이 담긴다.
- 2. setTimeout()가 포함된 함수를 만나면 WebAPI에 있는 setTimeout 파트로 보내고 함수를 처리한다.
- 3. setTimeout에서 지정된 지연시간 후 다시 Stack으로 넘어오는 것이아니라 Callback Queue로 보낸다.
- 4. Callback Queue에서는 받아온 작업들의 대기줄을 세운다.
➡️버스정류장처럼 Event Loop가 다시 데려가기 전까지 기다리는 것 - 5. 브라우저가 Stack 작업들을 끝냈거나 대기하는 중에서만 이 Callback queue에서 꺼내서 사용하게 된다.
➡️버스 역할의 Event Loop 가 승객 역할의 대기중인 Callback Queue 작업들을 Stack에 보내준다. - ❓"자바스크립트의 스택이 비워진다"는 의미
➡️브라우저가 Stack의 모든 작업들을 끝냈음을 의미
➡️모든 코드가 수행 후 Callback queue에서 대기했던 작업들을 다시 event loop가 스택에 올려보내 수행 - setTimeout()을 통해서 비동기 처리를 해주었기때문에 다른 문장들이 모두 수행되며
setTimeout() 함수 안에 사용자가 정의한 작업들이 백그라운드에서 수행되기떄문에
기존의 코드흐름을 막지 않고 동시에 다른 작업들을 진행할 수 있다. - 비동기로 처리하는 작업들의 예
➡️Ajax Web API 요청 : 서버쪽 데이터를 받아올때, 요청 후 서버의 응답까지 기다리지 않고
다른 작업을 수행하기 위해 이 작업을 “비동기적으로 처리”한다.
➡️파일 업로드/다운로드 : 사용자가 다른 작업을 하더라도 업로드/다운로드를 요청하면 백그라운드에서는 계속 업로드/다운로드 되고 있도록 비동기 처리를 할 수 있다.
➡️그 외로 암호화/복호화, 작업 예약 등을 비동기적으로 처리 가능하다.
setInterval() 함수
- setTimeout() 함수와 비슷하게 지연시간과 관련하여 명령 수행
setInterval( () => {
console.log("작업수행중...");
}, 1000);
- 1초마다 “작업수행중…” 문구가 출력되는 인터벌 형태
- setTimeout()은 지연시간 후 실행해달라는 명령이라면
- setInterval()은 지연시간 마다 실행해달라는 명령
➡️실습 - setTimeout() + callback함수 사용
// 비동기 처리 예제
// 1. 비동기 처리 완료 ('초' 표시)
function work(callback) {
setTimeout(() => {
console.log("작업 완료...");
callback();
}, 3000);
}
console.log("-----<'작업명령' 시작>-----");
work(() => {
console.log("Callback 함수 실행 후 : [관리자:작업완료 승인합니다.]");
});
console.log("-----<'다음명령' 탐색>-----");
console.log("-----<'작업명령' 종료>-----");
- 모든 작업들이 수행되고 마친 다음에서야 ➡️작업완료…, Callback 함수 실행 후 …. 등의 출력이 발생
setTimeout() 함수로 인해 비동기 처리가 된 것 - 1. work()함수 실행 ➡️3초 대기(setTimeout) 후 "작업 완료..." 메시지 출력
- 2. setTimeout의 비동기 작업이 완료되면 "전달된 callback 함수 수행"
❓전달된 callback 함수는 ➡️work( () => { console.log("Callback 함수 실행 후 : ..."); }); 을 의미한다. - 3. 따라서 이 callback 함수는 setTimeout의 비동기 작업을 완료한 이후에 할 "추가 작업"
ex.이 코드에서는 [ Callback 함수 실행 후 ...] 라는 추가 작업을 수행 - ⭐정리하자면
➡️work()함수 : "세탁기가 세탁을 한다" 라는 작업 - 시간이 걸림
➡️setTimeout() 함수 : 세탁기 내부에서 "실제로 세탁이 이루어지는 과정"
➡️callback함수 : 세탁이 완료되면 울리는 알림. 세탁이 끝난 뒤 알림을 듣고나서야 추가 작업이 가능해짐
Callback()함수
- 다른 함수의 실행이 완료된 후에 호출되는 함수
- 즉 특정 작업이 끝났을 때 다음 작업을 수행하기 위해 미리 준비해 둔 함수
- 비동기 처리를 할 work() 함수에 setTimeout()를 지정하고 callback() 함수를 통해
callback함수 외로도 비동기 작업 다룰 때 사용하는 문법
➡️Promise와 async/await
➡️Promise : ES6에 도입된 비동기 작업을 좀 더 편히 처리할 수 있도록 도와주는 기능
즉 비동기 작업이 많아질수록 복잡한 코드를 가지게되는 callback() 함수 사용을 개선하는 기능을 가짐
Promise
- Promise는 비동기 작업의 성공(resolv) 혹은 실패(reject)를 나타내는 객체
- resolve : 작업성공 시 결과를 반환하는 함수
- reject : 작업실패 시 오류를 반환하는 함수
- then : 작업이 끝나고 나서 또 다른 작업을 수행해야할때 Promise뒤에 .then() 을 사용
- catch : reject로 인한 오류가 발생 시 수행할 작업을 지정
- Promise() 함수 사용으로 비동기 작업의 결과를 쉽게 관리하고 이 후 작업을 이어나갈 수 있게됨
▶️실습 - Promise() 함수 사용하기 전 (기존 callback()함수 사용)
// 비동기 처리 예제
// Promise() 함수 사용 전 (기존 callback()함수 사용)
function increaseAndPrint(n, callback) {
setTimeout(() => {
const increased = n + 1;
console.log(increased);
if (callback) {
callback(increased);
}
}, 1000);
}
// 초기값은 0을 넣고 자기 자신인 (n)함수를 callback의 부분 인자로 넣어줌
increaseAndPrint(0, (n) => {
increaseAndPrint(n, (n) => {
increaseAndPrint(n, (n) => {
// ... 이렇게 계속 반복되며 복잡한 코드를 보이는 callback함수
console.log("callback함수 수행 끝!");
});
});
});
- if(callback){ callback(increased); }
➡️다음번 callback이 호출될때 이 increased 값을 가지고 호출되도록함
➡️즉 재귀함수 호출 방식처럼 갱신된 increased 값을 가지고 호출하는 것 - 깊이가 깊어지며 계속 increaseAndPrint() 함수를 호출하는 부분에서 복잡한 코드를 보이게 된다
➡️해결방법 : Promise() 함수 사용
▶️실습 - Promise() 함수 사용 - resolve (작업 성공)
// 1. Promise() 함수 사용 - resolve (작업 성공)
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1); // 1초 뒤 성공하도록 설정
}, 1000);
});
myPromise.then((n) => {
console.log(n);
});
- new Promise() :
➡️Promise 객체가 생성되고 내부에 비동기작업(setTimeout())이 정의됨 - 1초 뒤 실행할 수 있도록 설정 - resolve(1) :
➡️1초가 지난 후 resolve(1)이 호출되어 "작업이 성공했음을 알리고, 결과로 1을 전달" - then :
➡️myPromise가 성공되어서 resolve가 호출되면 then 블록 안의 함수가 실행된다.
➡️결과로 전달된 값 n = 1을 받아 console.log(n)을 출력하는 것 - ⭐정리하자면
➡️Promise 생성 (상품 주문) : 상품을 주문하고 기다리게된다.
➡️resolve (배송 완료) : 1초 뒤 상품도착완료 (비동기 작업 성공 = resolve)
➡️then (상품 사용) : 상품을 사용하는 과정 (숫자 1) - 깊이가 깊어지며 코드가 복잡해졌던 callback()함수의 단점을 then 체인을 이용하여
비동기 작업들의 순서를 쉽게 읽을 수 있게됨
▶️실습 - Promise() 함수 사용 - reject (작업 실패)
// 2. Promise() 함수 사용 - reject (에러 발생)
const errorPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error());
}, 1000);
});
errorPromise
.then((n) => { // 작업 성공 시 출력
console.log(n);
})
.catch((error) => { // 작업 실패 시 출력
console.log(error);
});
- catch를 통해 비동기 작업 중 발생한 오류를 처리할 수 있음
▶️실습 - Promise() 함수 사용 - 1. then의 반복사용
// 3-1. Promise() 함수 사용 (then 안에서 Promise 다시 리턴 - 개선코드)
function increaseAndPrint2(n) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const value = n + 1;
if (value === 5) {
// value 값이 5가 되면 실패를 발생시키도록 함
const error2 = new Error();
error.name = "값이 5이므로 오류 발생";
reject(error2);
return; // 오류 발생 후 종료시킴
}
// 성공했을 경우의 로직
console.log(value);
resolve(value);
}, 1000); // 1초 후 출력하도록 함
});
}
increaseAndPrint2(0) // 이 문구를 통해 Promise 객체가 리턴되었을 것
.then((n) => {
// 리턴된 Promise 객체를 넣어줌
return increaseAndPrint2(n);
})
.then((n) => {
return increaseAndPrint2(n);
})
.then((n) => {
return increaseAndPrint2(n);
})
.catch((e) => {
// 에러 발생된 객체를 받아서 처리하도록 함
console.error(e);
});
- increaseAndPrint2() 함수가 호출이되면 Promise객체라 새로 생성되어 리턴
- callback()함수처럼 호출이 될 때마다 값을 가진채 값이 넘어갔을 것이고,
다시 돌아와서 수행하고 돌아가는 과정을 거치게 되는 것 - 이 과정은 매번 수행할때마다 매번 다른 스택에서 동작하게 된다.
- ⭐정리하자면
➡️계속해서 increaseAndPrint2(n) 호출 후 리턴된 결과값을 기반으로 다시 return 하여 호출하고 있는 것이다.
▶️실습 - Promise() 함수 사용 - 2. then의 반복사용을 개선 (return 제거)
// 3-2. Promise() 함수 사용 (then 안에서 Promise 다시 리턴 - 개선코드)
// 이렇게 위의 코드를 더 간단히 표현할 수 있다.
increaseAndPrint2(0)
.then(increaseAndPrint2)
.then(increaseAndPrint2)
.then(increaseAndPrint2)
.catch((e) => {
console.error(e);
});
- 위의 리턴하고 값을 보내주는 이 코드를 간단하게 개선할 수 있다.
- return하지않고 함수 참조만 전달하게되어 반복적이었던 return구문을 제거할 수 있음
❓ Promise() 함수 사용의 장/단점
➡️장점 : callback함수와 달리 비동기 작업의 개수가 많아져도 코드 깊이가 깊어지지 않는다.
➡️단점 : 에러 발생 시 몇번째 에러인지 찾아내기가 어렵고 조건 분기를 나누는 작업도 어렵다.
또한 특정 값을 공유해가면서 작업을 처리하기도 까다롭다. ➡️해결방법 : async/await 문법 사용
async / await
- ES8에서 등장한 문법으로 Promise() 함수를 더 쉽게 사용하도록 도와줌
- async 키워드 : async키워드로 정의된 함수는 항상 Promise를 반환한다. (ex. return Promise)
- await 키워드 : Promise가 완료될때까지 실행을 “일시중단”하며 결과값을 반환
➡️async 함수 내에서만 사용이 가능하다.
➡️비동기적인 로직을 동기적인 로직으로 작성할 수 있도록 (기다릴 수 있도록) 함
➡️await가 있는 작업은 이전 await 작업이 완료된 후에야 실행된다.
➡️따라서 비동기 작업들이 “동시에 실행되지 않고” “순서대로 처리”된다.
▶️실습 - async / await 사용하기 전
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function process() {
console.log("안녕하세요");
sleep(3000).then(() => {
// sleep()함수를 이용하여 3초를 쉬고 진행
console.log("반갑습니다");
});
}
console.log("[Process 호출 전]");
process(); // 호출 후 "안녕하세요" 출력, sleep(3000)
console.log("[Process 호출 후]");
- 내부적으로 new Promise에서 setTimeout이 동작하면서 WebAPI파트의 setTimeout에 넘어간다
- 수행 후에는 callback queue에 담기고 모든 작업이 수행되고 나면 스택이 비워진다.
- 그 이후로 resolve가 수행된 결과가 반환되고 then() 코드가 그 결과값을 받는다.
- then() 코드의 결과로 console.log의 "반갑습니다"가 출력
▶️실습 - async / await 사용 후
// 2. async/await 사용 후
async function process() {
console.log("안녕하세요.");
await sleep(3000); // 1. 비동기 처리
console.log("반갑습니다."); // 2. 비동기 처리와 함께 전달되어야하는 후처리 코드
}
console.log("[Process 호출 전]");
process(() => {
console.log("프로세스가 종료되었습니다. [작업 종료]");
});
console.log("[Process 호출 후]");
- 장점 : 함수 안에 함수를 넣어서 계쏙 Callback 함수로 값을 넘겼던 과정들을
async/await가 대신해줌으로써 간단한 코드를 작성 가능 - 함수 선언 시 함수 앞부분에 async를 붙여주고, 실제 비동기 작업을 호출하는 부분에는 await를 붙여준다.
➡️async function process() …;
➡️await sleep(3000); - 비동기를 호출하는 순간(setTimeout 등…) 비동기 작업이 끝난 후 처리해야하는 코드가 있다면
그 “후처리 코드”들 또한 비동기 작업 코드와 함께 WebAPI로 보내야한다. - 그 예로 A함수의 “데이터를 가져오는 코드”와 “바뀐 데이터로 화면을 바꾸는 코드”가
1. 프로그램 실행 시 스택에 담긴다.
2. 만약 데이터를 가져오는 코드만 Web API 쪽으로 보낸다면 (=비동기로 보낸다면)
화면을 바꾸는 코드는 스택에 남겨져있다.
3. “데이터를 가져오는 코드”가 데이터를 가져오지 않고 비동기 처리 한 후
4. callback queue에서 모든 스택 작업이 끝나길 기다리는 상태임에도 “화면을 바꾸는 코드”가 수행된다.
⚠️적합하지 않은 상황
5. 만약 “데이터를 가져오는 코드”와 “바뀐 데이터로 화면을 바꾸는 코드”가 모두 함께
Web API쪽으로 비동기 처리된다면
6. callback queue에서 대기하다가 추후 stack에 있는 모든 작업들이 끝난 후에
Event Loop에 의해 다시 올라와서 함께 수행되어야한다
➡️적합한 상황
▶️실습 - async / await (에러발생)
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// 1-1. 에러발생 async 함수
async function makeError() {
await sleep(1000);
const error = new Error();
throw error; // 에러를 던져줌 (발생시킴)
}
// makeError를 사용하는 async 함수
async function process() {
try {
await makeError();
} catch (e) {
console.log(e);
}
}
console.log("[process 호출 전]");
process(); // process호출로 makeError()함수를 호출
console.log("[process 호출 후]");
- 자바스크립트의 경우 타입이 명시되지 않았기때문에 catch블록을 여러개 사용할 수 없음
- async / await가 없었다면 계속해서 함수를 감싸서 호출하는 방식이었을 것
- process 함수에서는 makeError()함수로부터 에러가 발생할 것을 알고있으므로
➡️makeError()함수로부터 던져진 에러를 처리할 try-catch 블록 선언
▶️실습 - async / await (수행할 문장)
// 1-2. 에러발생를 발생시키지 않고 해야할 일 구현 async 함수
async function makeError() {
await sleep(1000);
console.log("1초 기다렸다가 해야할 일을 구현");
}
- makeError()함수를 수정한 후 error 발생 대신 console.log()로써 해야할 일을 구현해볼 수도 있다.
▶️실습 - async / await (Promise.all 사용 전)
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const getDog = async () => {
await sleep(1000);
return "멍멍이";
};
const getRabbit = async () => {
await sleep(500);
return "토끼";
};
const getTurtle = async () => {
await sleep(3000);
return "거북이";
};
// 1-1번 방법
async function process() {
const dog = await getDog();
console.log(dog);
const rabbit = await getRabbit();
console.log(rabbit);
const turtle = await getTurtle();
console.log(turtle);
}
process();
- const dog = await getDog();
➡️ getDog() 함수를 호출하여 Promise가 완료될때까지 실행을 일시 중단
➡️getDog() 함수는 async로 정의된 함수이므로 항상 Promise를 반환
➡️내부적 코드로 sleep(1000)이 호출되는데 1초를 대기하게 된다. - sleep(1000) :
➡️1초 대기 후 return new Promise()에 따라 resolve를 호출하는 Promise를 반환 - process()안에서 getDog()를 호출함으로써 밑의 getTurtle() 등도 같이 감싸서 보내서 기다리게함
이는 process함수에서 한 작업이 수행될때 다른 작업은 순차적으로 기다리게 됨으로써
각각 1초 0.5초 3초의 시간을 합쳐 4.5초의 시간이 걸린다. - 순서대로 실행이되어 하나가 끝나야 다음 작업이 시작하는 이러한 방식을 동시에 작업하도록 바꿔줄 수 있다.
➡️해결방법 : Promise.all 사용
▶️실습 - async / await (Promise.all 사용 후)
- Promise가 가진 all()함수를 통해 실제 동작시키고자 하는 작업들을 한꺼번에 보내줌
async function process() {
const results = await Promise.all([getDog(), getRabbit(), getTurtle()]);
}
process();
- 동시에 보내서 처리함으로써 모든 작업이 순차적으로 수행되어 4.5초걸리던 작업을
➡️동시에 모든 작업이 시작되므로 가장 오래걸리는 작업인 3초 안에 모든 작업이 수행되도록 작업시간 개선 가능
▶️실습 - async / await (Promise.all 사용과 비구조화 할당 적용)
// 1-3번 방법 (Promise.all() + 비구조화 할당)
async function process() {
const [dog, rabbit, turtle] = await Promise.all([getDog(), getRabbit(), getTurtle()]);
console.log(dog);
console.log(rabbit);
console.log(turtle);
}
process;
- Promise.all()로부터 전달된 것들을 [dog, rabbit, turtle] 처럼 비구조화 할당을 이용하여
각각의 객체를 출력해볼 수 있다.
🚀 자바스크립트에서 비동기처리를 다루는 부분에서 복잡한 개념이 많았지만 이전 코드들을 점점 새로운 문법으로
개선해나갈 수 있다는 점이 이해하는데 큰 도움이 되었다.
회고를 진행하면서 수업에서 이해하지 못하고 지나쳤던 부분들을 추가 강의와 검색을 통해 배울 수 있었다.
회고가 길어졌지만 머릿 속에서 이해한 과정을 풀어내듯 썼다보니 다음에 다시 이 회고를 보더라도 금방 생각이 날 것 같다.
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_28일차_"리액트 React" (1) | 2025.01.10 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_27일차_"자바스크립트 이벤트" (0) | 2025.01.09 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_25일차_"자바스크립트 함수와 배열" (0) | 2025.01.07 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_24일차_"자바스크립트" (1) | 2025.01.06 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_23일차_"CSS 선택자와 위치 속성" (0) | 2025.01.03 |