이번 주부터 시작하는 스프린트를 이해하지 못하면 다음 일정에 지장이 생긴다는 말에 바짝 정신을 차리게 된다. 이전에 이미 해당 내용에 대해 정리한 바 있지만 나의 머리는 이미 그 기억들을 다 날려버렸을 뿐이고오(...) 다시 이해하기 쉽도록 이미지 파일과 코드를 덧붙여서 정리해본다! 화이팅!
이전에 이미 카페에서 주문을 하는 것에 비유해서 동기와 비동기에 대해 설명한 적이 있다.
( 이전 내용 참고: [JS] 비동기 호출 )
자바스크립트는 싱글스레드(single thread)라 동기적(synchronous/blocking)이다. 즉, 호이스팅(hoisting)이 된 이후부터 우리가 작성한 순서에 맞춰서 코드가 하나하나 동기적으로(자동적으로) 실행된다. 호이스팅은 var나 함수 선언(function declaration)이 자동적으로 코드의 가장 상단으로 올라가는 것을 말한다.
이런 특징을 클라이언트(Client)와 서버(Server)에 대입해서 이해해본다. 클라이언트는 "서버로 접속하는 컴퓨터"를 말하고 보통 내가 사용하는 컴퓨터라고 생각하면 무방하다. 서버는 "서비스나 리소스 같은 것을 제공하는 컴퓨터"를 말하는데 웹 서버, 게임 서버 등을 떠올리면 쉽다.
위의 그림에도 나와있지만 동기적인 코드는 1이 다 실행될때까지 기다렸다가 2가 실행되고 3이 실행되고 마지막으로 4가 실행된다. 마치 한칸인 화장실이 있고 첫번째 사람이 들어갔다가 볼일을 다 보고 나오면(!) 두번째 사람이 들어가고 나오면(...) 세번째 사람이 들어가는 것과 비슷하다.
클라이언트가 어떤 특정 요청을 보낸다. 그러면 서버가 그 요청을 받아서 데이터베이스에서 무언가를 꺼내오기도 하고 가공하기도 하는 작업을 한다. 그런데 동기적이라면 이런 작업이 이루어지는 동안 클라이언트는 응답(response)이 오기 전까지 아무것도 하지 않고 기다려야 한다. 1번 작업이 끝나야 그 다음 작업으로 넘어갈 수 있기 때문에 4가지 요청을 처리하는데 오랜 시간을 기다려야 할 것이다.
반대로 비동기적(Asynchronous/non-blocking)이라는 말은 코드가 언제 실행될지 예측할 수 없는 것과 비슷한 의미다. 특정 코드가 실행되고 있더라도 그 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행할 수 있다.
클라이언트가 서버에 뭔가를 요청한다. 서버가 작업을 하고 응답(response)을 줄 준비를 하는데 그동안 클라이언트는 기다리지 않고 다른 일을 계속해서 한다. 그러다가 서버에서 작업을 마치고 클라이언트에게 응답을 주면 클라이언트는 그것을 받아서 하던 일을 계속 한다. 4가지 일을 동시에 처리할 수 있기 때문에 4가지 작업 중 가장 오래 걸리는 작업을 수행하는 시간이 최종 작업 시간이 된다(위의 그림에서는 4번째 작업을 하는데 걸리는 시간이 해당). 이전 기능이 다른 작업을 blocking하지 않기 때문에 동시에 작업이 가능한 것이다.
이 코드를 조금 더 자세히 살펴보면,
setTimeout은 web API로 '지정한 일정 시간이 지나면 전달한 콜백함수를 호출하는 역할'을 한다. 아래에서 더 자세히 설명하겠지만 콜백함수는 '우리가 전달해준 함수를 나중에 네가 불러줘'라는 명령을 저장하는 함수다. 즉, 2번 코드는 1초 뒤에 '2'를 출력해달라는 말이다. 해당 코드를 실행하면 브라우저에게 '1초 뒤에 전달해준 콜백을 실행해 줘'라는 요청을 보내고 1초가 지나 브라우저는 '1초 지났으니까 이 콜백함수 실행해'라고 명령하게 되며 그제서야 2가 출력된다.
자세한 작동원리는 추후 Event loop에 대해 다루면서 다시 정리할 예정이다.
동기와 비동기를 이야기하면서 함께 잘 나오는 개념이 blockig과 non-blocking이다.
가끔 컴퓨터에서 어떤 프로그램을 돌리다가 회색화면이 나오면서 멈추는 아찔한 경험을 해봤을거다. 어떤 키를 눌러도 아무것도 안먹혀서 전혀 다른 행동을 할 수 없는 경우! 이게 blocking이다. non-blocking은 이전 작업이 완료될 때까지 멈추지 않고 다음 작업을 수행하는 것을 말한다. event loop, task queue에서 non-blocking 방식으로 작업을 처리하게 되면 오래 걸리는 작업들을 나중에 수행하고 먼저 수행할 작업들을 빠르게 수행할 수 있기 때문에 작업을 효율적으로 할 수 있다. 완벽하게 일치하는 것은 아니지만 자바스크립트에서는 blocking과 동기를 매칭하고 non-blocking과 비동기를 매칭해서 이해하기도 한다.
콜백함수는 다른 함수(A)의 전달인자(argument)로 넘겨주는 함수(B)를 말한다. parameter를 넘겨받는 함수(A)는 callback 함수(B)를 필요에 따라 즉시 실행(synchronously: 동시에 일어나게)할 수도 있고, 아니면 나중에(asynchronously: 비동기로) 실행할 수도 있다. 또 자바스크립트에서는 콜백함수를 변수에 할당할 수도 있다.
그럼 대체 동기, 비동기와 콜백은 무슨 상관이 있는걸까?
위에서 설명했지만 비동기는 예상할 수 없다는 뜻이다. 비동기로 어떤 작업들이 여러개 걸려있을 경우 각각의 작업에 걸리는 시간이 다 다를테니 작업1, 2, 3을 순서대로 시작했다고 해서 1, 2, 3의 순서대로 작업이 끝나는 것이 아니다. 그런데 내가 1, 2, 3을 동시에 끝나도록 하고 싶다면? 비동기이지만 순서를 주고 싶다면? 이때 사용할 수 있는 방법 중 하나가 콜백이다.
이런 코드를 꼭 A,B,C의 순서로 출력하고 싶다면?
이때 콜백을 쓰면 된다.
자, 콜백은 이런 비동기를 순서대로 다룰 수 있게 해주는 매우 고마운 녀석이다:)
즉각적으로 실행되는 동기적 콜백(synchronous callback)! 아래의 코드를 보고 왜 결과가 저런 순서로 출력되었는지 생각해보자. 자바스크립트 엔진이 어떻게 움직인걸까?
4. function printImmediately(print){
print();
} //자바스크립트는 hoisting이 가능하기 때문에 함수 선언이 제일 위로 갔을 것, 함수 올려놓고
1. console.log('1');
2. setTimeout(()=>
console.log('2'), 1000); //비동기
3. console.log('3');
5. printImmediately(() => console.log('hello'));
/* 4번 함수 선언을 맨 위로 올려놓은 다음
1번으로 내려와서 1을 출력하고,
2번을 브라우저에 요청해놓고(setTimeout- 비동기)
3번으로 내려와서 콘솔창에 3을 출력하고
5번으로 내려와서 printImmediately를 실행(4번에 해당)해서 hello를 출력한 다음
브라우저에 요청한 2번의 결과인 2를 1초 뒤에 콘솔창에 띄운다 */
나중에 언제 실행될지 예측할 수 없는 비동기적 콜백(Asynchronous callback)! 아래의 코드를 보고 왜 결과가 저런 순서로 출력되었는지 생각해보자. 자바스크립트 엔진이 어떻게 움직인걸까?
4. function printImmediately(print){
print();
}
6. function printWithDelay(print, timeout){
setTimeout(print, timeout);
}
// 아까와 동일하게 모든 함수선언은 호이스팅이 되므로 맨 위로 올라갔을 것!
1. console.log('1'); //동기
2. setTimeout(()=>
console.log('2'), 1000); //비동기
3. console.log('3'); //동기
5. printImmediately(() => console.log('hello')); //동기
7. printWithDelay(()=> console.log('async callback'), 2000); //비동기
/* 출력순서는 우선 함수선언은 호이스팅으로 4번과 6번을 맨 위로 올려두고
1번 실행해서 1바로 출력,
2번은 브라우저에 넘겨놓고(setTimeout-비동기),
3번은 실행해서 바로 3 출력,
5번의 함수를 실행해서(4번에 해당) 'hello' 출력,
7번도 브라우저에 넘겨놓고(setTimeout-비동기),
2번이 1초후 2를 출력하는 것이 먼저이므로 2 출력,
마지막으로 브라우저에 넘겨운 7번의 async callback이 2초 후 출력
이렇게 콜백에도 동기적, 비동기적 콜백 2가지가 있다는 것을 기억하자!
콜백을 어떻게 사용할 수 있을까? 사용자를 고려해서 에러(error)를 처리하거나 데이터를 처리하는 함수를 디자인할 수 있다. null과 something의 자리는 바뀔 수 있지만 보통 아래의 코드에 작성한 자리에 쓴다. API나 라이브러리(library)를 함수로 작성할 때도 함수를 이렇게 작성해서 처리해준다.
실제로 적용은 아래의 코드처럼 해볼 수 있다.
somethingGonnaHappen((err, data) => {
if (err) {
console.log('ERR!!');
return;
}
return data;
})
앞에서 한 모든 설명을 종합해보면 콜백은 동기와 비동기를 처리해주는 매우 유용한 존재이다. 단점이 하나도 없을 것 같지만 그렇지 않다. 아래의 코드를 보자! 한눈에 코드를 읽을 수 있을까? 3,4개의 콜백을 처리하는 것은 그나마 코드를 알아보기 쉬울 수 있지만 그 이상의 콜백 함수가 작성된 코드는 한번에 이해하기 굉장히 힘들어진다. 벌써부터 눈이 아파오는 느낌.. 이런 걸 콜백 지옥이라고 부르기도 한다.