콜백(callback) 사용하기
비동기 JavaScript 프로그램의 기본 단위는 콜백(callback)입니다.
콜백은 평범한 함수이며, 다른 함수의 인수 형태로 전달됩니다.
동기 프로그램처럼 특정 함수가 고유한 동작(네트워크 요청 등)을 완료하면 호출자가 건넨 콜백 함수를 호출합니다.
비동기 코드가 호출하는 콜백은 보통의 함수라서 비동기로 호출됨을 알리는 전용 타입 시그니처는 존재하지 않습니다.
fs.readFile(디스크의 파일 내용을 비동기로 읽을 때 사용), dns.resolveCname(비동기적으로 CNAME 레코드를 해석할 때 사용) 같은 NodeJS 네이티브 API는 콜백의 첫 번째 매개변수가 에러 또는 null이고 두 번째 매개변수는 결과 또는 null이라는 규칙을 사용합니다.
다음은 readFile의 타입 시그니처입니다.
function readFile(
path: string,
options: {encoding: string, flag?: string},
callback: (err: Error | null, data: string | null) => void
): void
readFile이나 callback은 모두 일반 JavaScript 함수일 뿐 특별한 타입을 갖고 있지 않습니다.
시그니처만 봐선 readFile이 비동기로 동작하며 readFile을 호출한 다음 결과를 기다리지 않고 제어가 버로 다음 행으로 넘어간다는 사실을 알 수 없습니다.
이어지는 예제 코드를 실행하려면 NodeJS용의 타입 선언을 설치해야 합니다. 설치 방법은 다음과 같습니다.
npm install @types/node --save-dev
예를 들어 아파치(Apache) 접근 로그를 읽고 쓰는 NodeJS 프로그램을 구현한다고 해보겠습니다.
import * as fs from 'fs'
// 아파치 서버의 접근 로그에서 데이터 읽기
fs.readFile(
'/var/log/apache/access_log',
{encoding: 'utf-8'},
(error, data) => {
if(error){
console.error('에러 읽힘!', error)
return
}
console.info('잘 읽힘!', data)
}
)
// 동시에 같은 접근 로그에 기록하기
fs.appendFile(
'var/log/apache/access_log',
'새로운 접근 로그 시작',
error => {
if(error){
console.error('에러 읽힘!', error)
}
}
)
NodeJS의 내장 API의 동작 방식에 익숙하지 않다면, 그래서 이들 호출이 비동기로 일어나기 때문에 API 호출 순서로는 파일 시스템에서 실행할 동작 순서를 결정할 수 없다는 사실을 모른다면, 먼저 호출한 readFile이 읽어 들인 데이터에 나중에 호출한 appendFile에서 새로 추가한 접근 로그가 들어 있을 수도, 아닐 수도 있다는 사실을 눈치채기 쉽지 않습니다.
코드를 실행하는 시점에 파일 시스템이 얼마나 바쁘냐에 따라 결과가 달라집니다.
readFile이 비동기로 동작한다는 사실을 경험한 적이 있거나, NodeJS 문서를 통해 이 사실을 발견했다거나, 두 개의 인수 (Error | null과 T | null을 차례로)를 받는 함수를 마지막 인수로 받는 함수는 보통 비동기로 동작한다는 사실을 알고 있었을 수 있습니다.
어찌 되었든 여기서 타입이 해줄 수 있는 것은 없습니다.
타입만으로 함수가 비동기인지 여부를 알려줄 수 없다는 문제와 별개로, 콜백 방식은 연달아 수행되는 작업을 코드로 표현하기 어렵다는 문제도 있습니다.
이를 콜백 피라미드라 부르기도 합니다.
async1((error1, response1)=>{
if(response1){
async2(response1, (error2, response2)=> {
if(response2){
async3(response2, (error3, response3) => {
// 블라 블라
})
}
})
}
})
여러 동작을 연달아 실행할 때 보통 한 동작이 성공했을 때만 다음 동작으로 이어가고, 에러가 발생하면 즉시 빠져나와야 할 때가 많습니다.
콜백을 이용하면 이런 제어를 수동으로 처리해야 합니다.
특히 동기 방식의 에러까지 관여되기 시작하면, 연이은 콜백을 올바로 처리하기가 상당히 어려워집니다.
비동기 동작 여러 개를 이어 붙이는 것도 유용하지만, 때론 함수들을 병렬로 실행시킨 후 모두가 완료됐을 때 통지하도록 하거나, 서로 경쟁시켜서 가장 먼저 끝난 작업 결과를 이용하는 등의 방식으로 활용할 수도 있습니다.
평범한 콜백 방식으론 불가능한 일입니다.
비동기 동작을 정교하게 추상화하지 않는다면 서로의 결과에 의존하는 콜백이 여러 개 등장하면 문제가 금방 복잡해지기 때문입니다.
요약
아래처럼 요약할 수 있습니다.
- 간단한 비동기 작업에는 콜백을 사용합니다.
- 간단한 동작엔 콜백이 적합할 수 있지만, 비동기 작업이 여러 개로 늘어나면 문제가 금방 복잡해집니다.
'👶 TypeScript' 카테고리의 다른 글
비동기 스트림 (0) | 2023.01.24 |
---|---|
JavaScript의 이벤트 루프 (0) | 2023.01.24 |
async와 await (0) | 2023.01.23 |
비동기 프로그래밍, 동시성과 병렬성 (0) | 2023.01.23 |
에러 처리 - Option 타입 (0) | 2023.01.23 |