동기는 말 그대로 프로그램이 순차적으로 위에서부터 아래로 실행되는 것을 말한다.
예를 들면 우리의 console.log. 백날 쳐보지 않아도 콘솔창의 값이 어떻게 나올지 알 수 있다.
console.log("안녕")
console.log("나는")
console.log("프로그래머")
//실행결과
안녕
나는
프로그래머
반대로 비동기는 프로그램이 순차적으로 실행되지 않는, 언제 실행이 마무리될지 모르는 것을 말한다. 예를 들면 JS의 비동기적 함수에는 setTimeout이 있다. 또한, 서버에 특정한 요청을 하는 경우도 대게 비동기적으로 처리된다.
자바스크립트는 기본적으로 동기적이고 setTimeout이나 서버요청 같은 비동기적인 예외 상황이 있다고 보면 된다.
그래서 가끔,코드가 동기적으로 처리되겠거니 생각하고 서버에서 어떤 값을 요청받으면 엉뚱하게 그 값이 undefined가 나올 떄가 있다. 예를 들면 다음과 같은 경우이다.
function getData() {
var tableData;
$.get('https: 서버 URL', function (response) {
tableData = response;
});
return tableData;
}
console.log(getData()); // undefined
해당 코드는 콜백함수를 설명하는 어떤 블로그를 들어가도 볼 수 있을만큼 흔하다.
순서를 보자면 이렇다. 우선 getData 함수가 호출되면 tableData라는 변수가 선언된다. 초기화가 안되어있으니 변수는 undefined이다. 이후 ajax의 메서드가 '임의의 URL에 get 요청을 보낸다. 이 요청은 비동기적으로 실행된다.
[참고: 비동기적으로 실행된다는건 프로그래밍적으로 호출스택이 아닌 백그라운드에서 실행됨을 의미한다. 간략하게 말해서 one track -> two track으로 일이 동시에 진행되는 것. 이는 자바스크립트의 이벤트 루프를 다루는 포스팅에서 자세히 설명함. ]
서버 URL 뒤에는 요청값을 URL에서 받았을 때 실행되는 콜백함수가 있다. 콜백함수는 tableData 변수에 response 값을 넣어준다. 그리고 tableData가 반환된다.
모든 코드가 동기적으로 실행된다면 tableData에는 response가 들어갔을 것이고, console.log(getData())를 했을 때 undefined가 아닌 서버의 response 값이 나와야 한다. 하지만 값은 예상과 다르게 undefined이다.
그 이유는 ajax 메서드가 미처 URL에서 response를 가져오기도 전에 tableData가 반환되었기 때문이다.
그렇다면 이를 어떻게 해결해야할까? 서버에서 값을 가져왔는데 undefined이면 큰일나지 않는가? 여기서의 해법 중 하나가 콜백함수이다.
콜백함수의 본래 정의는 특정 시점(특정 이벤트가 발생했을 때)에 호출되는 함수이다. 다음 간단한 예제를 보자.
const sampleFuncion = (func) => {
console.log("안녕 나는")
func()
}
const printProgrammer = () => {
console.log("프로그래머")
}
sampleFunction(printProgrammer)
//실행결과
안녕 나는
프로그래머
//console.log("안녕 나는")이 호출되고 (이벤트가 발생하고) '프로그래머'를 출력하는 함수가 호출된다.
이정도의 예제로 콜백함수가 무엇인지는 단번에 이해가 되었으리라 생각한다.
더 중요한건, 이 콜백함수를 통해서 어떻게 비동기처리를 해결할 수 있을까에 대한 고민이다.
콜백함수를 이용해서 위의 서버예제를 바꾸어보겠다. return값에 undefined가 아닌 response가 있도록! 두 코드를 모두 적었으니 비교해보고 차이점이 무엇인지 생각해보면 좋겠다.
<기존>
function getData() {
var tableData;
$.get('https: 서버 URL', function (response) {
tableData = response;
});
return tableData;
}
console.log(getData()); // undefined
<콜백함수 이용>
function getData() {
var tableData;
$.get('https: 서버 URL', function (response) {
tableData = response;
console.log(tableData) //response
});
}
기존처럼 비동기처리 이후 return을 받지않고, 비동기처리의 콜백함수에서 작업을 수행하면 된다.
ajax 메서드 뒤에 이미 콜백함수가 있기 때문에 해당 예시가 적절해보이지 않아 다른 예시를 만들어보았다. 다른 예시는 setTimeout을 가지고 만든 예시이다.
setTimeout 메서드는 setTimeout(콜백함수, 특정시간) 이렇게 생겼으며, 특정시간 이후에 콜백함수를 호출한다. 시간은 밀리세컨드로, 3000 이라고 하면 3초이다.
const arr = []
const getUserInfo = () => {
setTimeout(()=>{arr.push("유진")}, 2000)
}
getUserInfo()
console.log(arr[0]) //결과값은 undefined
setTimeout 함수에 대해 너무 깊게 생각하지 말고 우선 getUserInfo의 역할만 생각하자. getUserInfo는 호출되고 2초뒤에 빈 배열 arr에 "유진"을 추가한다. 이게 서버에서 "유진"이라는 값을 가져오는거랑 동일하다고 보면 된다.
getUserInfo를 호출하고 console.log를 출력했을때 결과값이 undefined가 나오는 것을 볼 수 있다. 이는 아까 서버 예시랑 마찬가지로 "유진"이 배열에 push되기 이전에 출력되었기 때문이다.
그렇다면 "유진"을 출력하기 위해선 어떻게 할까?
이를 콜백함수를 통해서 해결해보자.
const arr = []
const print = (x) => {
console.log(x)
}
const getUserInfoCallBack = (func) => {
setTimeout(()=> {arr.push("유진"); func(arr[0])}, 2000)
}
getUserInfoCallBack(print) //결과값 "유진"
print는 그저 매개변수를 출력하는 함수이다. 그리고 getUserInfoCallBack에는 func라는 함수인자가 있으며, 이는 setTimeout이 2초 뒤에 호출하는 함수 안에서 호출된다.
따라서 arr.push로 이미 "유진"이 push되고 이것이 이후 print 함수의 호출에 의해 출력되는 것이다. 따라서 결과값이 이전과 다르게 "유진"이 나온다.
그런데, 이렇게 비동기처리를 할때 콜백함수를 이용하게 되면 콜백함수 안에 콜백함수를 집어 넣는 '콜백지옥'에 빠질 확률이 높다.
1초마다 초를 표시하는 타이머 예시를 통해 '콜백지옥'을 보여주겠다.
setTimeout(()=>{
console.log('1초')
setTimeout(()=>{
console.log('2초')
setTimeout(()=>{
console.log('3초')
setTimeout(()=>{
console.log('4초')
},1000)
}, 1000)
}, 1000)
다음 코드를 실행하면
1초가 지난 다음 콘솔에 '1초'
그 다음 1초가 지나면 또 콘솔에 '2초'
... '4초'까지 출력된다.
요약하자면, 콜백함수는 간단히 말해 이후(later) 혹은 특정 이벤트 후 호출되는 함수일 뿐이며 이러한 함수를 중첩시켜서 비동기적인 코드를 동기적으로 실행되도록 만들 수 있다.
하지만 콜백함수를 여러번 중첩시킬 경우 '콜백지옥'과 같이 가독성이 심하게 떨어지는 코드가 만들어질 수 있다.
따라서 콜백함수를 가지고 비동기처리를 하는 것은 추천되지 않으며
프러미스/ async await 등의 방법이 있다.
프러미스에 관해서는 다음 포스팅에 자세히 다루도록 하겠다.