[JavaScript] 비동기 처리와 콜백 함수(==콜백 지옥)

홍예찬·2020년 11월 20일
1
post-thumbnail

1. 비동기 처리?

프론트엔드 개발자로서 개발을 하다 보면, 비동기 처리로 인해 애를 먹는 경우가 많습니다. 저 역시도 프로젝트를 진행하면서 백엔드 API로부터 데이터를 요청할 때, 예상과는 다르게 undefined가 뜨거나 로직에 오류가 발생하는 경우가 많았고 이로 인해 정말 많은 삽질을 한 경험이 있었죠.

비동기 처리와 callback 함수, Promise, async, await에 대해 블로그 작성을 하는 이유는
첫째, 비동기 처리로 인해 고통을 당하는 누군가가 저의 블로그를 보고 조금이나마 도움을 얻길 바람이고(작고 소중한 나의 바램😚)
둘째, 저 역시도 개발(삽질)을 하면서 얻게 된 지식을 잊지 않기 위함입니다.

비동기 처리를 이해하기 위해서는 먼저 자바스크립트 엔진의 특성을 잘 이해할 필요가 있습니다. 이 개념이 제대로 잡혀있어야 결국 비동기 처리, 자바스크립트의 lifecycle을 이해하는 데 훨씬 수월하기 때문입니다.

2. 자바스크립트의 특징

자바스크립트의 가장 큰 특징 중 하나는, 싱글 스레드(Single Thread)로 동작하는 언어라는 점입니다. 싱글 스레드란 말 그대로 한 번에 하나의 작업만을 수행할 할 수 있다는 것을 의미합니다. 즉, 하나의 작업이 다 끝나야만 다음 작업이 실행될 수 있다는 것을 의미하는 것이죠.

이 말이 이해된다면 당연히 다음과 같은 의문이 들 것입니다.
'자바스크립트가 싱글 스레드라는데 어떻게 비동기 처리가 가능하다는거지?
비동기 처리란 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고, 순차적으로 다음 코드를 먼저 실행하는 것을 의미하는데?'

여기서 중요한 사실이 드러납니다. 많은 개발자들조차 자바스크립트가 자체적으로 "비동기 처리"를 한다라는 잘못된 개념을 갖고 있기 때문입니다. , 위의 개념에 입각해서 보자면 자바스크립트 자체적으로는 비동기 처리를 할 수 없습니다.

그렇다면 비동기 처리가 가능한 이유는 무엇일까요? 그 해답은 Web API에 있습니다. setTimeOut()와 같이 타이머가 완료된 후 실행되는 함수거나, http요청을 보내는 작업, 데이터를 읽어오는 등의 시간이 걸리는 작업의 경우 Web API가 이를 처리하는 역할을 맡게 됩니다. 따라서 자바스크립트 엔진은 계속해서 순차적으로 코드를 처리할 수 있게 되는 것이죠.

Web API가 무엇인지, 비동기 처리가 어떻게 이뤄지는지를 조금 더 명확하게 알고 싶으시다면 이 영상을 참고해보세요!

3. 비동기 처리 방식

➀ Callback function (==Callback Hell)

console.log("1");
setTimeout(() => console.log("2"), 1000)
console.log("3");

//expected output: '1' '3' '2'

콜백 함수에 가장 대표적인 예시는 위와 같습니다. setTimeout()함수는 첫 번째 인자로 함수를, 두 번째 인자로 지연 시간을 받습니다. setTimeout 함수를 알고 계신 분이라면 1, 3, 2,의 순서대로 출력된다는 것이 예상 가능하죠.

그러나 제가 그동안 고통받았던 비동기 문제는 위와 같은 쉬운 예시가 아닌, 더 복잡한 상황에서 발생하게 됩니다. 실제 프로젝트를 진행하면서 데이터를 받아오는 fetch함수와 같은 비동기 함수를 실행하게 된다면 제 생각과는 다르게 undefined가 뜨기 때문입니다.

따라서 자신이 예상했던 타이밍에 데이터를 불러오거나 순차적으로 함수를 처리하기 위한 콜백 함수를 사용하게 됩니다.

여기서 드러나는 사실은 콜백함수가 두 가지의 경우로 나뉘어진다는 것입니다. 위의 예시와 같은 비동기적인 함수(Asynchronous callback) 뿐만 아니라 동기적인 함수(Synchronous callback)도 있다는 것이죠. 아래의 예시 코드를 보면 좀 더 명확하게 이해가 될 것입니다.

- 동기적인 콜백함수(Synchronous callback)

console.log("1");
setTimeout(() => console.log("2"), 1000)
console.log("3");

function synchronous(print) {
  print();
}
synchronous(()=> console.log('Hello world!'));

//output: 1 , 3 , Hello world! , 2

위의 코드는 어떻게 실행된 것일까요? 순차적으로 설명해보면 다음과 같습니다.

synchronous함수 선언은 hoisting으로 인해 최상단으로 올라감
console.log("1") 출력
setTimeout(() => console.log("2"), 1000) 브라우저 API 요청
console.log("2") 출력
synchronous함수 호출, 실행
setTimeout(() => console.log("2"), 1000) 실행

- 비동기적인 콜백함수(Asynchronous callback)

console.log("1");
setTimeout(() => console.log("2"), 1000)
console.log("3");

function synchronous(print) {
  print();
}
synchronous(()=> console.log('Hello world!'));

function asynchronous(print, timeout) {
  setTimeout(print, timeout);
}
asynchronous(() => console.log('Hello!'), 2000);

//output: 1 , 3 , Hello world! , 2, Hello!

그렇다면 위의 코드는 어떻게 실행된 것일까요? 이 코드 역시 순차적으로 설명해보겠습니다.

synchronous함수 선언은 hoisting으로 인해 최상단으로 올라감
asynchronous함수 선언은 hoisting으로 인해 최상단으로 올라감
console.log("1") 출력
setTimeout(() => console.log("2"), 1000) 브라우저 API 요청
console.log("3") 출력
synchronous 호출, 실행
asynchronous 호출
setTimeout(() => console.log("2"), 1000) 실행
asynchronous 실행

이제 비동기 처리가 무엇인지, 콜백함수의 종류는 비동기적인 함수뿐만 아니라 동기적인 함수로도 쓰일 수 있다는 것을 이해했습니다.

그런데 많은 개발자들이 콜백 함수를 두고 콜백 지옥이라고 부릅니다. 왜 그럴까요? 다음의 예시 코드를 보면 왜 콜백 지옥인지 이해가 가실 겁니다.

class UserStorage {
	loginUser(id, pw, onSuccess, onError) {
    	setTimeout(() => {
      if(
      	(id === 'chans' && password === 'hello') ||
        (id === 'hong' && password === 'world')
      ) {
      	onSuccess (id);
      } else {
      	onError(new Error('not found!'))
      }
      }, 2000);
    }
  
  	getRoles(user, onSuccess, onError){
    	setTimeout(() => {
      if (user === 'chans'){
      	onSuccess({name: 'chans', role: 'admin'})
      } else {
      	onError(new Error('no access'));
      }
      })
    }
}

const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your password');
userStorage.loginUser(id, pw, user => {
	userStorage.getRoles(
  	user, 
    userWithRole => {
  		alert(`Hello ${userWithRole.name}, you have a ${userWithRole.role} role`);
  	}, 
  	error => {
  		console.log(error);
			}
		);
	}, 
	error => {
		console.log(error);
	}
);

위 코드는 드림코딩 by 엘리의 영상을 참고했습니다.

콜백 지옥의 예시로 백엔드 서버와의 통신을 통해 로그인을 처리하는 로직을 구현해봤는데요, 여기서 setTimeout 함수는 실제 데이터를 통신하는 비동기 처리 로직이라고 생각하면 되겠습니다.

위 코드의 기본적인 flow는 이렇습니다.
➀ id와 pw를 받아옴
➁ 서버에 로그인
➂ 사용자의 역할을 다시 요청해서 받아옴
➃ 사용자의 이름과 역할이 들어있는 객체 출력

위의 코드를 보면 어떤 생각이 드시나요? 네 보기만 해도 토나오는 코드입니다.🤮🤮
콜백 함수 안에서 다른 함수를 호출하고, 또 다른 콜백을 전달하고 또 호출하고...
이것이 바로 🔥🔥콜백 지옥🔥🔥입니다.

콜백 함수(==콜백 지옥)가 갖는 문제점은 다음과 같이 정리할 수 있겠습니다.
➀ 코드 자체의 가독성😵
➁ 에러가 발생할 경우, 디버깅을 해야 할 경우 비효율적(유지, 보수 극발암🤯)

이러한 이유로 개발자들은 Promise, async, await를 이용해서 비동기 처리를 하게 됩니다. Promiseasync, await에 대한 개념은 비동기 처리를 하는 데 있어서 너무나도 중요한 개념이기 때문에 다음 블로그를 통해 설명하도록 하겠습니다.

profile
내실 있는 프론트엔드 개발자가 되기 위해 오늘도 최선을 다하고 있습니다.

0개의 댓글