부트 캠프 강의를 들을 때도 그렇고, 유명한 유튜브 영상에서도 비동기의 핵심이 콜백함수라고 가르친다.
지금에서야 다시금 깨달은 거지만, 이 말은 틀렸다.
비동기함수에 콜백함수가 종종 쓰일 뿐이거지. 콜백함수와 비동기는 아무 상관이 없다.
콜백 함수는 그저 디자인 패턴일 뿐이니까...
지금부터 이런 엽기적인 설명들 때문에 헷갈렸던 나 자신을 되돌아보며, 콜백함수에 대해서 제대로 알아보자!!
자바스크립트는 동기적이다.
=> 호이스팅이 된 이후부터, 코드가 우리가 작성한 순서에 맞춰서 하나씩 동기적으로 실행되다는 말이다.
<script>
console.log(1);
console.log(2);
console.log(3);
</script>
쉬운 예로 위의 코드를 실행해보면, 1
->2
->3
순서로 실행되는 것을 알 수있다.
이렇게 위에서 아래로 코드가 순서대로 실행되는 것을 동기적이다~ (synchronous)
라고 한다.
그냥 거의 대부분의 프로그래밍 언어들은 이런 특징을 가지고 있다.
JS는 단순히 코드 실행만 하는게 아니라, 웹페이지 렌더링 역시 담당한다.
예를 들어, 위에서 아래로 코드가 실행되던 중에
container.addEventListener("click", clickAfter);
console.log(1);
setTimeout(function(){}, 1000);
console.log(2);
addEventListener
함수를 만났다면, JS는 동기적이기 때문에, 클릭이 될 때까지 JS는 멈춰버린다.
다시 말해서, container.addEventListener("click", clickAfter);
가 실패든 성공이든 어떻게든 해결이 될 때까지 "JS는 결과값이 나올 때까지" 그 라인에서 마냥 기다리고 있다.
웹페이지 UI를 뿌려줘야 될 JS가 클릭이 될지 안 될지도 모르는 라인 하나 때문에, 마냥 대기타고 있는데...
부들부들...
"JS의 실행기"인 브라우저는 이런 말도 안 되는 상황을 보고만 있을 수없었다.
그래서, 브라우저는 이런 오래 걸리는 혹은, 언제 결과값이 나올지 모르는 작업들을 따로 모아다가 Web API
라는 곳에 넣어둔다.
그리고 결과값이 받아와지면, 다시 메인 무대(콜 스택)으로 복귀해서 작업을 수행한다.
브라우저들의 이런 천재성 덕분에 addEventListener
가 나오든, setTimeout
이 나오든 빠꾸없이, 코드를 쭉쭉 실행할 수 있게 됐다.
console.log(1);
setTimeout(function(){ console.log("메롱") }, 5000);
console.log(2);
// 1 -> 2 -> "메롱"
브라우저가 없었으면, JS는 1
찍고 => 5초 ㄱㄷ렸다가 "메롱"
찍고 => 2
를 찍는 대형 참사가 벌어졌을 거다.
물론, 코드를 짜는 개발자 입장에서는 이게 더 직관적이고, 이해하기 쉽지만, 우리는 어디까지나 "JS는 웹페이지를 그려주는 역할을 담당한다."는 사실을 명심해야 한다.
5초 ㄱㄷ렸다가 다음 라인으로 넘어가면, 웹페이지를 기다리는 유저는 5초를 더 기다려야 웹페이지를 볼 수있는 거다.
정리하면,
- JS는 다른 언어들과 마찬가지로 동기적으로 실행된다.
- 그러나, JS는 웹 페이지를 렌더링해주는 특수한 역할을 수행하는 언어이다.
- 그래서, 마냥 코드를 순차적으로 실행할 수만은 없다.
- JS의 실행기인 브라우저는 이런 문제를 해결하고자 비동기하는 방식으로 오래걸리는 코드들을 처리해준다.
- 즉, 콜백함수를 쓴다고 비동기가 되는게 아니라,
JS에서 비동기적으로 코드를 실행하고 싶다면,Web API
에서 처리해주는 함수들을 쓰는 방법밖에 없다.- 또다른 "JS의 실행기"인 Node.js 역시 브라우저와 동일하게 JS를 처리한다. (비동기 쓴다고!!)
설령, 아무리 오래걸린다고 해도 반드시 특정 코드가 실행된 이후에야, 그 다음 코드가 실행되어야만 하는 상황이 있다.
=> 다시 말해서, 코드가 반드시 순서대로 실행되어야 하는 상황이 있다는 뜻이다.
필자가 부트캠프에서 진행했던 날씨 API Sprint 예시를 들어보자!!
function renderWeatherData() {
// TODO: 여기에 DOM을 이용하여 날씨 데이터를 표시하세요
const json = getData(); // 문제의 발생 지점!!
console.log("this is ", json); // => undefined
console.log(json.weather[0]["main"]); // => error
console.log(json.weather[0]["description"]); // => error
}
function getData() {
fetch(API_URL_OpenWeatherMap)
.then(function (resp) {
return resp.json();
})
.then(function (json) {
// TODO:
// 요청이 완료되고 나면 여기서부터 날씨 데이터(json)를 사용할 수 있습니다.
// 하드코딩된 data를 대체하세요.
return json;
});
}
renderWeatherData();
fetch()
가 비동기 방식이기 때문에 const json = getData();
으로 할당하려고 해도 undefined
가 뜬다. 다시 말해서, 데이터를 받아온 다음에 json
이라는 변수에 할당해줘야하는데, getData();
함수 내에 비동기적으로 처리되는 녀석이 있어서 Web API
로 들어가버린다.
그러면, 브라우저는 const json = getData();
을 일단 건너뛰고 그 다음 줄부터 실행해버린 것이다.
그 결과, 데이터 값을 못 받아온 채로 UI를 뿌려줘서 웹페이지가 제대로 렌더링 되지 못했다.
Web API
로 넘기고, 다음 라인을 실행한다.바로, 이때 사용하는 디자인 패턴이 바로... "콜백 함수"이다!!
자바스크립트에서 1
=> 2
를 출력하는데, 그 사이에 1초 텀을 두는 코드를 짜보자.
console.log(1);
setTimeout( ()=>{ }, 1000);
console.log(2);
console.log(1);
setTimeout( ()=>{ console.log(2); }, 1000);
이렇게, JS에서 코드를 순차적으로 실행시키고 싶을 때, 콜백함수 ()=>{ console.log(2); }
를 하나 만들고, 그 안에 기능 개발을 한다.
그러면, 순차적으로 실행이 된다.
콜백 함수는 함수 안에 파라미터로 들어가는 함수이다.
또한,
=> 성공의 결과를 받아줄 수있는 함수 이다.
용도 : JS에서 뭔가를 순차적으로 실행하고 싶을 때, 자주 쓰는 디자인 패턴
특징
- 다른데서 만든 함수도 콜백함수로 집어넣을 수있다.
document.querySelector('.button').addEventListener('click', 함수명);
- 콜백함수가 필요한 함수들에만 콜백함수 사용 가능하다.
ex)map
,filter
,addEventListener
등등
여기까지 읽고 이해한 우리들은 이제 아래의 코드가 항상 순서대로 작동하지 않는다는 사실을 안다.
first();
second();
그럼, 이 코드를 강제로 반드시 순차적으로 실행하게 만들어보자!!
function first( a ){
console.log('first');
a();
}
function second(){
console.log('second');
}
// 이렇게하면, first() 다음에 second()가 실행되도록 할 수 있다.
first(second);
일반적으로 함수에 이름을 짓는 경우는 그 함수의 기능을 다시 쓸 일이 있어서이다.
그러나, 모든 함수들의 재사용성이 높지는 않다. 즉, 함수의 이름을 지을 필요가 없는 것들도 많다.
=> 다시 쓸 일이 있는 함수들만 이름을 짓는다.
=> 콜백 함수로 쓰는 함수들은 이름을 안 짓는 경우도 많다.
위의 경우처럼 재사용성이 있는 미리 만들어놓은 함수를 집어넣을 수도 있고,
첫째함수(function(){
console.log(2)
}):
이렇게 직접 함수선언문을 집어넣을 수도 있다.
그러나, 순차적으로 실행하려고 콜백함수를 여러개 사용하면 단점이 조금 있다.
코드가 옆으로 길어진다.
첫째함수(function(){
둘째함수(function(){
셋째함수(function(){
어쩌구..
});
});
}):
첫째함수 둘째함수 셋째함수 이렇게 차례로 실행해주는 코드이다.
근데, 이렇게 되면, 코드가 너무 우측으로 길어진다.
별 수 없다. JS가 이렇게 생겨먹은 것을 어쩌겠나...
=> 특히 자바스크립트로 서버개발시, 이런 패턴이 흔하다.
이런 식으로 콜백 함수를 계속 사용하다보면, 코드가 너무 길어져서 그것을 읽는 사람이 지옥을 맛 본다는 뜻으로 콜백 지옥이라는 이름이 붙었다.
콜백지옥의 문제점
1. 가독성이 떨어진다.
2. 가독성이 떨어지면, 에러가 발생했을 때 디버깅하기가 어렵다.
3. 체인이 길어지면 길어질수록, 문제를 분석하고 디버깅하기 어렵다.
4. 결국, 유지보수도 어렵다 ㅠ.ㅠ
이런거 보기싫으시면 ES6 신문법인 Promise라는 기계를 만들어 사용하면 된다.
콜백대신 쓸 수 있는 Promise 디자인 패턴을 적용하면 어떻게 되냐면
첫째함수().then(function(){
그 담에 실행할거
}).then(function(){
그 담에 실행할거
});
옆으로 길어지지 않고 .then()
이라는 키워드 덕분에 그나마 뭘 하는지도 파악이 쉬워진다.
document.querySelector('.button').addEventListener('click', 함수명);
함수명
에 함수명()
이렇게 넣으면 안 된다.document.querySelector('.button').addEventListener('click', function(){
alert('깔깔깔깔깔깔!!!'); // 버튼을 클릭시, 이거 실행됨
// 순차적으로 실행되죠?? 콜백이니까...
})
이 경우 역시도 익명 함수를 할당해놓은 것뿐이지 실행시켜놓은게 아니다.
함수명()
과 function(){...}()
둘은 같은 의미이다.협업을 했을 때는 콜백의 강력함이 발휘된다.
안정적으로 확실하게 특정 코드를 순차적으로 실행할 수 있다.
예를 들어, 필자가 first()
를 만들었는데, 너무 유용해서 다른 사람들이 갖다 쓴다고 하자.
그런데, 개발자 A는 first()
이후에 console.log(1)
을 실행하고 싶다면 어떻게 해야 할까??
직관적으로는
first();
console.log(1);
이렇게 구현할 수 있겠지만, first()
가 비동기처리가 되면 순서대로 작동하지 않는다.
확실한 안전빵으로 first()
를 업그레이드 하면 된다.
first( 파라미터 )
에 파라미터를 추가하고,
function first( 파라미터 ){
console.log('first');
파라미터();
}
function second() {
console.log(1);
}
first(second);
이렇게 콜백 함수 패턴을 사용하면, 안전하게 순차적으로 원하는 코드를 실행시킬 수 있다.
비동기함수들만 콜백 함수 디자인 패턴을 쓰는 것은 아니다.
동기적인 함수 내에서도 콜백 함수 패턴을 쓸 수 있는데, 이런 경우를 잘 못보는 이유는 구지 이렇게 쓸 필요가 없어서이다.
이 사실만 염두해두고 아래의 예시들을 보면, 크게 어렵지 않다.
위의 fakeSetTimeout()
콜백 함수는 함수가 불려진 순서대로 출력 된다.
이제 대표적인 비동기 함수인 setTimeout()
을 써보자!
=> 아니! 좀 더 정확하게 말해서, 콜백 함수라는 디자인 패턴을 가지고 있는 비동기 함수인 setTimeout()
을 써보자!
=> 뭔가 이상하다. 분명히 setTimeout()
함수의 작동시간도 0초로 line 14에 만들어 줬는데... 가장 마지막에 작동된다. 왜 이럴까??
그러나, 좀 더 디테일한 원리를 알고 싶다면, JS 비동기의 핵심, 이벤트 루프를 보길 바란다.
잘 보고 갑니다. 콜백에 대한 사무친 한이 느껴지네요ㅋㅋㅋㅋ