[JavaScript] callback, callback hell, and Promise

Soozynn·2022년 7월 6일
0

JavaScript

목록 보기
4/6

8개월이라는 시간동안 계속해서 정확한 비동기의 개념과 callback함수 그리고 Promise, async/await에 대해 완벽히 이해를 하지 못하고 있었는데, 면접 질문에서 단골 질문으로 나오는 부분이기에 제대로 정리 및 이해하고자 글을 쓰게 되었다.

코딩이 익숙해진 지금에서야 비로소 비동기가 정확히 어떤 느낌(?)이고 개념인지 파악할 수 있었다. 천천히여도 좋으니 베이스에 있어서 정말 탄탄한 개발자로 나아가고싶다. 아자잦!!


동기와 비동기


  • ⭐️ JavaScript is synchronous. 자바스크립트는 동기적이다. ⭐️
  • ⭐️ Execute the code block in order after hoisting.
    호이스팅이 된 이후부터 코드는 우리가 작성한 순서에 맞춰서 하나하나씩 동기적으로 실행된다.
    ⭐️
  • ⭐️ hoisting: var, function declaration
    변수 var 와 함수 선언들이 자동적으로 제일 위로 올라가는 것.
    ⭐️

그렇다면, 비동기 프로그램(asynchronous)이란?

👉 언제 코드가 실행될 지 예측할 수 없는 것.
(대표적인 예시로 setTimeout이 있음)

자바스크립트에서는 Promise, async/await 문법이 나오기 이전에는 callback 을 사용하여 비동직 작업을 처리해주었다. 그러다 보니 많은 불편함이 있었는데 그 중 하나가 바로 callback 지옥이다.

예를 들어서, 코드를 작성할 때 해당 비동기 작업 내부에서 또 다른 비동기 작업,, 또 다른 비동기 작업,, 또 다른 작업,, 이런 식으로 코드를 작성하다보면 연속되는 들여쓰기에 의해 callback지옥이 탄생하게 된다..ㅎ

이런한 코드 작성은 가독성에 굉장히 좋지 않아 코드를 읽거나 해석할 때 눈이 돌아간다.
때문에 해당 불편한 사항을 개선하기 위해 나온 문법이 바로 Promise이다.
그 전에 callback이란 그럼 무엇일까?


Callback

call-back 말 그대로 다시 불러주라는 의미처럼, 함수를 인자로 넘겨주는 부분을 뜻한다.

아래 예시에서 콜백함수는

// 지금 당장 실행하지 않고 1초가 지난 다음에 내 함수를 실행해줘 라는 의미 -> call back
setTimeout(function () {
	console.log("콜백 함수 부분 체크");
}, 1000);

// 아래와 같이 arrow function을 사용하여 더 간단하게 축약하여 작성 가능
setTimeout(() => {
	console.log("콜백 함수 부분 체크");
}, 1000);

파라미터로 넘겨진 익명함수 function () { ~ } 지점까지가 callback 함수인 것.
setTimeout 문법을 mdn에서 확인해보면 setTimeout(code, delay)와 같이 적혀있는데, 위에서 delay는 1000에 해당 하고 하나의 파라미터로 전달한 code가 바로 우리가 전달한 callback 함수(익명함수 영역)인 것이다.

(콜백에 더 자세한 쓰임은 여기서 확인해보면 좋을 거 같다.)

그럼, callback은 항상 비동기일때만 쓰일까?

👉 답은, NO! 이다

callback도 2가지의 경우로 나뉘어진다.

  • 동기적 콜백 Synchronous callback
  • 비동기적 콜백 Asynchronous callback
// 🚀 Synchronous callback
// ❗️ 자바스크립트는 type이 아니라서 어떤 type의 callback 함수인지 예측을 할 수 없음..
function printImmediately(print) {
	print();
}

printImmediately(() => console.log("동기적 콜백"));
// 🚀 Asynchronous callback
function printWithDelay(print, timeout) {
	setTimeout(print, timeout);
}

printWithDelay(() => console.log("비동기적 콜백"), 1000);

간단한 예시는 이제 뒤로하고, callback만을 사용하여 작업을 수행하게 되면 어떻게 될까?..ㅎㅎ

앞서 말했던 콜백지옥이 펼쳐지게 된다. 콜백함수 안에서 다른 콜백함수를 부르고 또 부르고 반복하다보면,, 정말 가독성이 최악인 코드가 된다. (콜백 속 콜백 속 콜백 속 콜백...)

이제 콜백지옥 예제를 통해 직접 콜백지옥을 느껴보자.

Callback hell

// 사용자의 데이터를 백엔드에서 받아오는 작업을 코드로 작성해보는 예제

class UserStorage {
// 2가지의 API가 있다고 가정 -> 1. 사용자 로그인 2. 역할 가져오기

	loginUser(id, password, onSuccess, onError) {
    	// 사용자의 id 와 password가 일치하여 로그인 성공 시 onSuccess, 로그인 실패 시 onError 호출하는 예제
        
        setTimeout(() => {
        	if (id === "ellie" && password === "dream") || (id === "coder" && password === "academy")) {
				onSuccess(id); // ⭐️ callback
           } else {
           	onError(new Error("not found")); // ⭐️ callback
           }
        }, 2000);
    }
    
    getRoles(user, onSuccess, onError) {
    	// 사용자의 데이터를 받아서 사용자마다 가지는 admin이나 게스트라던지 그러한 역할을 서버에서 다시 받아오는 예제
        // ❗️ 원래는 사용자가 로그인 하면 로그인과 관련된 정보를 백에서 한 번에 넘겨받지만 callback hell을 작성하기 위한 예시로 사용..
        
        setTimeout(() => {
			if (user === "ellie") {
				onSuccess({ name: "ellie", role: "admin" }); // ⭐️ callback
			} else {
              onError(new Error("no access")); // ⭐️ callback
			}
        }, 1000);
    }
    
    
    
// 1. 클래스를 이용하여 사용자에게 id와 password 입력 받아오기
// 2. 로그인 시도
// 3. 성공적으로 된다면 onSuccess(id)를 통해 id를 받아오는데 받아온 id를 이용해서 Roles 역할을 다시 요청하여 받아올 것임
// 4. Roles를 통해 역할을 성공적으로 받아오면 우리에게는 사용자의 이름과 역할이 들어있는 객체를 받을 수 있음. ->  { name: "ellie", role: "adming" }

const userstorage = new UserStorage(); // class니까 new라는 키워드를 사용하여 만들기
const id = prompt("enter your id"); // class를 이용하여 백엔드와 통신, 사용자에게 id와 password 받아오기
const password = prompt("enter your password");

// 🚀
userstorage.loginUser(
id,
password,
(user) => {
	userStorage.getRoles(
    user,
    (userWithRole) => {
    	alert(`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`);
    },
    error => {
    	console.error(error);
    }
  );
},
(error) => console.error(error));

위와 같이 작성하게 되면, 문제점이 무엇일까?

바로, 콜백 체인의 문제점..!

1. 가독성이 너무 많이 떨어짐.
어디서 어떤 식으로 연결되어있는지 한 눈에 가눈하기 어렵고, 비즈니스 로직을 한 눈에 이해하기 힘듦.
2. 에러가 발생하거나 디버깅이 발생하게 되는 경우에도 굉장히 힘듦.
3. 당연히 유지보수 또한 어려움.



Promise

위와 같은 콜백지옥의 문제점을 개선하기 위해 나온 문법이 바로 Promise이다.
Promise is a JavaScript object for asynchronous operation.

1) state: pending -> fufilled or rejected
2) producer (when new Promise is created, the executor runs automatically) vs consumers(then, catch, finally)

promise 객체는 아래와 같은 문법으로 만들 수 있다.

const promise = new Promise(function(resolve, reject) {
  // executor (제작 코드)
});

new Promise에 전달되는 함수는 executor(실행자, 실행 함수) 라고 부른다. executornew Promise가 만들어질 때 자동으로 실행되는데, 결과를 최종적으로 만들어내는 제작 코드를 포함한다.

⭐️ executor runs auto.. 유의 -> 선언해주자마자 바로 프로미스가 실행되어진다는 의미

// 🚀 1. Producer
// when new Promise is created, the executor runs automatically.

const promise = new Promise((resolve, reject) => {
	// doing some heavy work (network, read files)
    console.log("doing something...");

	setTimeout(() => {
		resolve("then의 인자로"); // 아무 문제 없이 성공했을 경우, then의 인자로
		reject(new Error("여기에 이유를 잘 명시해서 작성해야함")); // 실패 시, 보통 Error 객체를 넘겨주는 식
        // Error는 자바스크립트에서 제공하는 오브젝트 중 하나
	}, 2000);
});

// 🚀 2. Consumers: then, catch, finally
// then -> 성공했을 때
// catch -> reject error가 발생했을 때
// finally -> 성공 / 실패와 상관없이 무조건 마지막에 호출

promise
	.then(value => {
      console.log(value); 
	// 여기서 value 값은 위 setTimeout 내의 resovle(value); 
	// resolve 내의 인자 값이다.
  })
  .catch(error => {
	console.error(error);
	// 프로미스 내에 작업 중 에러 발생 시 호출
})
.finally(() => {
	console.log("finally");
	// 성공 실패 여부와 상관없이 마지막에 무조건 호출
});

체이닝이 가능한 이유
프로미스의 thencatch 메서드를 호출하게 되면 똑같은 Promisereturn하기 때문에 그 return 된 똑같은 Promise를 다시 호출할 수 있어서 then 다음으로 catch (체이닝 메서드) 가 사용가능 한 것.

// 🚀 3. Promise chaining -> 메서드 체이닝
// 서버에서 숫자를 받아온다고 가정하면

const fetchNumber = new Promise((resolve, reject) => {
	setTimteout(() => {
		resolve(1);
    }, 1000);
});

fetchNumber // 중괄호를 생략하고 아래와 같이 체이닝 문법을 쓸 수도 있다.
	.then(num => num * 2)
	.then(num => num * 3)
	.then(num => { // 다른 서버로 보내서 다른 숫자로 변환된 값을 받아오는 예시
		return new Promise((resolve, reject) => {
			setTimeout(() => {
				resolve(num - 1);
			}, 1000);
		});
	})
.then(num => console.log(num)); // 그 다음 그 숫자를 출력해보는 예제

위 예제를 보면 알 수 있듯이 then에서는 값을 바로 전달해도 되고, 또 다른 Promise를 전달해도 된다.

👉 최종 console 값은 결국 5가 출력이 된다.
걸리는 시간은 두 개의 setTimeout에서 설정한 시간에 의해 2초가 걸려야할 것이다.

// 4. 🚀 Error handling -> catch를 통해 error를 잘 처리하자
// 그렇다면 어떻게 에러핸들링을 할 수 있는지 아래 예시를 보자.

const getHen = () => { // 암탉을 받아오는 프로미스
	new Promise((resolve, reject) => {
		resolve("🐓");
	}, 1000);
});

const getEgg = hen => {
	new Promise((resolve, reject) => {
		resolve(`${hen} => 🥚`);
	}, 1000);
});

const cook = egg => {
	new Promise((resolve, reject) => {
		resolve(`${egg} => 🍳`);
	}, 1000);
});


// 각각 서버에서 값을 받아왔다고 가정하고
getHen // 먼저 닭을 받아오고 나서 
	.then(hen => getEgg(hen)) // 닭이 받아와지면 전달 받은 닭을 이용해서 getEgg라는 함수를 호출
	.then(egg => cook(egg)) // 그리고 위 동작이 정상적으로 수행이 되면 받아온 달걀을 가지고 우리가 cook 요리를 할 것임.
	.then(meal => console.log(meal)); // 그리고 요리가 다 완료된 다음에는 요리된 음식을 콘솔로그에 출력하는 과정.
    .catch(error => console.error(error));
    
// 또 위 과정을 아래와 같이 더 축약해서 작성 가능하다.
getHen
	.then(getEgg)
	.then(cook)
	.then(console.log)
    .catch(console.error);

callback hell을 다시 Promise로 이쁘게 작성해보기

// 🚀 Callback Hell example -> change Promise
class UserStorage {
	loginUser(id, password) {
		return new Promise((resolve, reject) => {
			setTimeout(() => {
				if ((id === "ellie" && password === "dream") || (id === "coder" password === "academy")) {
					resolve(id);
				} else {
					reject(new Error("not found"));
				}
            }, 2000);
		});  
    }
    
// 🚀 이전에 작성한 callback hell getRoles 예시 또한 위 예시를 참고하여 직접 작성해보기 

const userStorage = new UserStorage();
const id = prompt("enter your id");
const password = prompt("enter yout password");

userStorage.loginUser(id, password)
	.then(userStorage.getRoles)
    .then(user => alert("어쩌구 저쩌구 전하고자 하는 경고 내용.."));
    .catch(console.error);

위와 같이 이쁘게 코드 작성 가능해진다는 장점..!

위에 프로미스로 깔끔하게 작성한 부분도 이제는 더 좋게 작성이 가능한데 이는 다음 async/await 정리편에서 다뤄보도록 해야지 👀

출처) 드림코딩

0개의 댓글