[자바스크립트] 비동기 처리 1부 - Callback

자바스크립트의 콜백 함수와 비동기 함수애 대해서 혼란스러워 하시는 분들이 주변에 많은 것 같아서 개념 정리를 해보고자 합니다.
이번 포스트에서는 실제 프로젝트에서 자주 접할 수 있는 유저 데이터 조회 시나리오를 통해 콜백 함수를 이용한 비동기 처리에 대해서 알아보겠습니다.

콜백 함수

유저 ID를 인자로 받아 DB나 API 연동 없이 임의의 유저 객체를 리턴하는 findUser()라는 함수를 작성해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
function findUser(id) {
const user = {
id: id,
name: "User" + id,
email: id + "@test.com"
};
return user;
}

const user = findUser(1);
console.log("user:", user);
  • 결과
1
user: {id: 1, name: "User1", email: "1@test.com"}

위와 같이 우리가 흔히 생각하는 일반적인 함수란 입력(파라미터)이 있고 출력(리턴값)이 있습니다.

하지만 자바스크립트에서는 출력값이 없고 그 대신에 콜백 함수를 입력받는 함수들이 많이 있습니다.
콜백 함수는 다른 함수에 인자로 넘어가서 실행될 로직을 담게 됩니다.

예를 들면, 위 코드는 아래와 같이 다른 스타일로 동일한 결과를 출력하도록 재작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
function findUserAndCallBack(id, cb) {
const user = {
id: id,
name: "User" + id,
email: id + "@test.com"
};
cb(user);
}

findUserAndCallBack(1, function(user) {
console.log("user:", user);
});
  • 결과
1
user: {id: 1, name: "User1", email: "1@test.com"}

10번째 줄의 findUserAndCallBack() 함수의 호출부를 보면 두번째 인자로 콜백 함수를 선언하여 넘겼습니다.
따라서 7번째 줄의 findUserAndCallBack() 함수가 실행될 때 cb 매개 변수는 콜백 함수는 할당 받으며, cb(user); 가 실행될 때, 이 콜백 함수가 실행되게 됩니다.

위 두 코드의 차이점은 findUser() 함수는 결과값을 리턴하고 함수 외부에서 결과값을 이용하여 작업을 수행하는 반면에, findUserAndCallBack() 함수는 결과값을 이용해 해야할 작업까지 함수 내부에서 수행해주기 때문에 결과값을 구지 리턴할 필요가 없습니다.

자바스크립트에서는 함수도 숫자나 문자처럼 변수에 할당할 수 있는 하나의 값이기 때문에 콜백 함수를 다른 함수의 인자로 넘기는 것은 매우 자연스러운 현상입니다.
그래서 위 두 코드는 단순히 스타일 차이로도 볼 수 있지만 자바스크립트 특유의 비동기 처리가 들어가게 되면 얘기가 약간 달라지게 됩니다.

콜백 함수를 통한 비동기 처리

비동기(Asynchronous) 함수란 쉽게 설명하면 호출부에서 실행 결과를 가다리지 않아도 되는 함수입니다.
반대로 동기 함수(Synchronous) 함수는 호출부에서 실행 결과가 리턴될 때 까지 기다려야 하는 함수입니다.

비동기 함수의 이러한 Non-blocking 이점 때문에, 자바스크립트처럼 싱글 쓰레드 환경에서 실행되는 언어에서 광범위하게 사용됩니다.
예를 들어, 브라우져에서 어떤 로직이 동기 함수만으로 실행될 경우, 기다리는 시간이 많아져서 사용자 경험에 부정적인 영향을 미칠 것입니다.
또한, 비동기 함수를 사용하면 로직을 순차적으로 처리할 필요가 없기 때문에 동시 처리에서도 동기 함수 대비 유리한 것으로 알려져있습니다.

하지만 코딩을 하는 개발자 입장에서는 비동기 함수는 동기 함수에 비해서 좀 덜 직관적으로 느껴질 수 있습니다.
동기 함수처럼 순차적 처리가 보장되지 않기 때문에 아래에 위치한 코드가 위에 위치한 코드보다 먼저 실행될 수 있기 때문입니다.

자바 스크립트에는 setTimeout() 이라는 대표적인 내장 비동기 함수가 있습니다.
setTimeout()은 두 개의 매개 변수를 받는데, 첫번째는 실행할 작업 내용을 담은 콜백 함수이고, 두번째는 이 콜백 함수를 수행하기 전에 기다리는 밀리초 단위 시간입니다.
즉, setTimeout() 함수는 두번째 인자로 들어온 시간 만큼 기다린 후에 첫번째 인자로 들어온 콜백 함수를 실행해줍니다.

실제 프로젝트에서 DB나 API를 통해서 유저 데이터를 얻어오는 경우, 필연적으로 이러한 latency가 발생하게 됩니다.
이러한 상황을 시뮬레이션 하기 위해서 setTimeout()을 이용하여 위 섹션에서 작성했던 findUser() 함수를 수정하였습니다.
const가 아닌 let을 이용해서 user 로컬 변수를 선언하고 setTimeout() 함수를 통해 0.1초 후에 user 변수에 객체를 할당하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function findUser(id) {
let user;
setTimeout(function() {
console.log("waited 0.1 sec.");
user = {
id: id,
name: "User" + id,
email: id + "@test.com"
};
}, 100);
return user;
}

const user = findUser(1);
console.log("user:", user);
  • 결과
1
2
user: undefined
waited 0.1 sec.

코드를 실행해보면 예상치못한 순서로 코드가 실행됨을 알 수 있습니다.
3번째 줄의 setTimeout()은 비동기 함수의 호출이기 때문에 실행이 완료될 때 까지 기다리지 않고 다음 라인인 11번째 줄로 넘어가버립니다.
따라서 14번째 줄의 findUser(1)undefined를 리턴하게 되고 user 변수에는 그대로 undefined가 할당됩니다.
0.1초 후에 setTimeout() 함수의 첫번째 인자로 넘어간 콜백 함수가 실행되면서 waited 0.1 sec.가 출력되고 user 로컬 변수에 원하는 객체가 할당되었지만 이미 때 늦은 상황이 되었습니다.

이와 같이 setTimeout()과 같은 비동기 함수를 호출하게 되면, 함수의 실행이 완료도 되기 전에 다음 라인이 실행되어 버리기 때문에 각별히 주의를 해야합니다.

이와 같이 코드 실행 순서가 뒤죽박죽이 될 수 있는 난처한 상황에서는 이전 섹션에서 했던 것 처럼 콜백 함수를 이용해서 해결할 수 있습니다.
함수로 부터 결과값을 리턴 받기를 포기하고, 결과값을 이용해서 처리할 로직을 콜백 함수에 담아 인자로 던지면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function findUserAndCallBack(id, cb) {
setTimeout(function() {
console.log("waited 0.1 sec.");
const user = {
id: id,
name: "User" + id,
email: id + "@test.com"
};
cb(user);
}, 100);
}

findUserAndCallBack(1, function(user) {
console.log("user:", user);
});
  • 결과
1
2
waited 0.1 sec.
user: {id: 1, name: "User1", email: "1@test.com"}

이번에는 findUserAndCallBack() 함수의 2번째 인자로 결과값을 이용해서 실행될 로직을 넘겼고, setTimeout() 함수는 0.1초 후에 이 콜백 함수를 호출하였습니다.
이와 같이 비동기 함수를 호출할 때는 결과값을 리턴 받으려고 하지말고, 결과값을 통해 처리할 로직을 콜백 함수로 넘기는 스타일로 코딩을 해줘야 예상된 결과르 얻을 수 있습니다.

하지만 자바스크립트 프로젝트가 점점 더 복잡해지면서 최근에는 콜백 함수를 인자로 넘겨서 비동기 처리를 하는 스타일을 피하는 추세입니다.
왜냐하면 콜백 함수를 중첩해서 사용하게 되면 계속해서 코드를 들여쓰기 해야하고 그러다보면 코드 가독성이 현저하게 떨어지게 되기 때문입니다.
결국, 많은 개발자들이 콜백 지옥이라고 불리는 끔찍한 상황을 겪게 되었고 최근에는 Promiseasync/await를 이용하는 방법들로 대체되고 있습니다.

다음 포스트에서는 Promiseasync/await를 이용해서 좀 더 코드 가독성을 해치지 않으면서 비동기처리하는 방법에 대해서 알아보겠습니다.

후속 포스트

전체 코드

공유하기