지난 글에서 동기처리와 비동기처리가 어떤 개념인지 알아봤다. 동기처리는 기본적으로 task가 쌓여있으면 하나 씩 처리하는 Queue와 비슷하게 작동한다. 비동기처리는 동시처리방식으로 멀티태스킹이라고 이해하면 쉬울 것 같다.(음악을 들으면서, 게임을 하면서, 책상을 두드린다..?) 그렇다면 실질적으로 JavaScript에서 코드로는 어떤 방식으로 처리를 하게 될까?
Callback은 이전 Pre course때 공부한 개념인데, 당시에는 함수의 인자로 전달되는 함수
로만 알고 있었다. 이게 어떻게 작동하리라는 것을 알고 공부한 것이 아니었기 때문에, 단순히 특정 함수에 집어넣어 함수 객체 내부의 어떤 값을 mapping하는 용도로 사용하거나 했던 것으로 기억한다. (고차함수인 map에 전달하는 함수 역시, callback함수이고 mapping을 용도로 사용된다.)
다음 글은 stack overflow의 어떤 누군가가 callback함수를 정의했다는 댓글을 퍼온 것이다.
A callback function is a function which is:
1.passed as an argument to another function,
and, 2.is invoked after some kind of event.
직역하자면, 1.다른 함수의 전달인자로서 보내지는 함수
이고, 2.어떤 이벤트에 의해서 불려지는 함수
라고 말하고 있다. 이 얘기는 함수객체 내부에서 쓰이는 한편, 어떤 사건이 일어났을 때 사용하기 위해 숨겨둔다라는 식으로 이해해볼 수 있다. 사건이 일어났을 때 사용한다라는 말이 참 웃기기는 한데, 감춰두지 않고 밖에 내놓으면 싹다 읽어버리기 때문이다. 예시코드를 하나 작성해보자. 이건 비동기로서 사용한다기보다는 콜백의 취지를 이해하기 위한 코드다.
let consoleInfo = (string, callback) => { setTimeout(() => { callback(text); }, 3000); } consoleInfo("몬스터를 잡았습니다.",(text)=>{ console.log(text) });
코드가 지저분해보이고, 안 예쁜거 인정. 근데 그 이전에 코드를 조금만 살펴보자. 저렇게 보내면 어떤 일이 벌어질까? 너무나 당연하게도, 3초뒤에 callback으로 전달된 함수에 전달된 string을 console하게 된다. 이게 일반적으로 작성한거랑 뭐가 다르냐고 하겠지만, 마지막행 이전에 뭔가 하나만 적어주면 기가막히게 이벤트가 터졌을때, 순서에 맞게끔 작동하게 할 수 있다.
if(유저가 몬스터를 잡았을 때)
유저가 몬스터를 잡았을 때, 잡고난 후에 3초간 여유가 있는 시간에 특별한 이벤트를 순서에 맞게 지정해서 발생시킬 수 있다. setTimeout이 걸려있는 동안, 다른 곳에서 callback함수는 타이머를 재고 있을 것이며, 그동안 경험치 획득을 알린다던지 레벨업 여부를 확인해서 알린다던지, 다양한 행위들을 만들 수 있다.아니면 callback 함수 자체를 겹겹이 쌓아서 시간뒤에 벌어지는 일들을 순서로 정해볼 수도 있다.
const consoleInfo = (string, callback) =>{ setTimeout( () => { console.log(string); callback(); }, 100 ) }
const consoleInfoAll = () => { if(유저가 레벨업을 했다){ consoleInfo("레벨업을 했습니다.", () => { consoleInfo("경험치 10을 획득했습니다.", () => { }) }) } }
조금 이상해 보일지 몰라도, 기본적으로 실행 순서를 강제하고 함수실행 사이의 텀 시간을 지정해줄 수 있으며, 내부의 조건문을 이용해서 에러를 잡아내거나, 특정 발생이벤트에 대응하는 함수실행을 강제할 수 있다. 이것은 단순히 함수진행을 지연하는 것이 아니라, 타이머에 따라 처리를 순서를 정해주고, 이후 실행될 함수의 순서를 정해주는 것이다.
콜백함수는 함수의 실행순서나 지연시간을 결정할 수 있으며 조건을 이용해 이벤트를 핸들링할 수 있는데, 이 얘기는 에러처리(예외처리)를 이용해서 상황을 파악하는 에러 핸들링 도구로 사용될 수 있는 여지가 있다는 것을 말해준다. 아래 코드를 보자.
const somethingGonnaHappen = callback => { waitingUntilSomethingHappens() // if(isSomethingGood) { callback(null, somthing);< } if(isSomethingBad) { callback(something, null);< } }
콜백으로 넣어주는 함수에는 data를 받아오거나, err가 발생되거나하는 상황에 맞는 코드를 작성한다. 내부적으로 err, data를 전달인자로 보내주는데, 상황에 따라 if로 분기해서 err를 catch할 수 있다.
somethingGonnaHappen((err, data) => { if(err) { console.log('ERROR!!'); return; } return data; });
콜백 내부를 살펴보면 다음과 같이 될 수 있다. 일반적으로 err는 앞 전달인자, data는 뒤 전달인자로 보내준다. 일종의 관습같다. 콜백을 사용하는 경우 이런식으로 코드를 작성할 수 있도록 해야겠다.
기본적으로 callback함수를 이용해서 비동기처리를 하게되면 순서에 맞게끔 함수가 연속되게 작성해야하는 단점이 있다. 다음 순서의 함수를 callback으로 계속적으로 넘겨줘야하기때문에, 비동기처리를 하는데 여러 실행이 뒤섞이게 되면 다음과 같은 상황이 벌어질 수도 있다.
예를 들어서, 웹페이지의 애니메이션을 작동시키는 함수를 callback 비동기처리로 작성한다고 하면, 사각형이 점점 커져서 만들어지고 한바퀴 돌고 잘라져서 여러개로 나누어지는 애니메이션을 코드로 짠다고하면 4단으로 callback이 쌓이게 된다. (애니메이션같은 경우, keyframe으로 처리하겠지만 함수로 작성한다고 가정해봤다.) 이런 방식이 수십번에 걸쳐서 계획되어있는 실행이라면 callback으로 했을때 위와 같은 callback hell을 만날 수 있다. 그럼 이쯤에서 다른 방식을 알아보자.