비동기 작업이 끝날 때 메인 실행 흐름이 이를 통지 받는 가장 기본적인 매커니즘
CallBack : 비동기 작업의 결과를 가지고 런타임에 의해 호출되는 함수
CPS ( Continuous-Passing Style )
비동기에만 관련된 개념은 아니며, 결과를 다른 함수로 전달하는 스타일을 의미한다
DS ( Direct Style )
function add(a, b) {
return a + b
}
CPS Code ( 비동기 X )
function addCps (a, b, callback) {
callback(a + b)
}
console.log('before')
addCps(1, 2, result => console.log(`Result: ${result}`))
console.log('after')
CPS Code ( 비동기 O )
function additionAsync (a, b, callback) {
setTimeout(() => callback(a + b), 100)
}
console.log('before')
additionAsync(1, 2, result => console.log(`Result: ${result}`))
console.log('after')
비동기 작업 완료 -> Callback부터 실행 흐름 재개로 비동기 처리를 하는데, callback이 EventLoop에서 시작되기에 새로운 스택을 가진다.
그럼 호출자 컨텍스트는 다른 실행 흐름에서 실행되기에 사라질 것 같지만, 클로저 덕분에 다른 시점과 다른 위치에서 호출돼도 사리지지 않는다.
실행 컨텍스트 : https://gamguma.dev/post/2022/04/js_execution_context
클로저 : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
Non-CPS Callback
const result = [1, 5, 7].map(element => element - 1) console.log(result) // [0, 4, 6]
Callback이 연산 결과를 전달하지 않고, 결과 또한 DS로 동기 전달 됨
즉, callback 패턴이라 하더라도 반드시 비동기다! 는 아니다.
주의해야 할 패턴
1. 예측 불가능한 함수
동기와 비동기 실행이 조건에 따라 바뀌는 경우는 위험하다.
/* eslint handle-callback-err: 0 */
import { readFile } from 'fs'
const cache = new Map()
function inconsistentRead (filename, cb) {
if (cache.has(filename)) {
// invoked synchronously
cb(cache.get(filename))
} else {
// asynchronous function
readFile(filename, 'utf8', (err, data) => {
cache.set(filename, data)
cb(data)
})
}
}
function createFileReader (filename) {
const listeners = []
inconsistentRead(filename, value => {
listeners.forEach(listener => listener(value))
})
return {
onDataReady: listener => listeners.push(listener)
}
}
const reader1 = createFileReader('data.txt')
reader1.onDataReady(data => {
console.log(`First call data: ${data}`)
// ...sometime later we try to read again from
// the same file
const reader2 = createFileReader('data.txt')
reader2.onDataReady(data => {
console.log(`Second call data: ${data}`)
})
})
2. Zalog
예측할 수 없는 비동기/동기 혼용 함수를 사용해 에러로 집히지 않는 기능적 결함을 풀어 놓는 것을 Zalog를 푼다라고 한다.
즉, 동기든 비동기든 함수의 기능을 통일하는 것이 중요하다.
1. 의 코드를 동기로 통일해보자
import { readFileSync } from 'fs'
const cache = new Map()
function consistentReadSync (filename) {
if (cache.has(filename)) {
return cache.get(filename)
} else {
const data = readFileSync(filename, 'utf8')
cache.set(filename, data)
return data
}
}
console.log(consistentReadSync('data.txt'))
// the next call will read from the cache
console.log(consistentReadSync('data.txt'))
readFileSync 를 통해 파일 읽기I/O를 동기처리 시켰다.
주의! APP이 비동기적 동시성 작업을 처리하는데 영향을 주지 않는 경우만 블록킹 API를 사용하자.
( I/O 작업은 웬만해서는 논블록킹 API를 사용하자 )
1. 의 코드를 비동기로 통일해보자 ( Deferred Execution )
지연 실행이란 Callback이 가까운 미래에 실행 되도록 예약하는 것이다.
process.nextTick() | setImmediate() |
이벤트 큐 전단에 callback 추가 ( appendleft ) | 이벤트 큐에 callback 추가 ( append ) |
큐 전단에 들어가기에 다른 이벤트보다 우선 실행 | 타이머가 존재하지 않아 특정 조건에서 setTimeout(cb, 0) 보다 우선 실행 |
재귀 호출 등 특정 상황에서 I/O Starvation 발생 가능 | - |
process.nextTick() 으로 지연된 callback을 마이크로테스크 라고 함
setImmediate vs setTimeout(cb, 0) 차이
timer는 setImmediate 콜백 이전에 실행된다.
- setTimeout callback 내부
- I/O callback 내부
- microtask 내부
에서 사용시에는 setImmediate가 우선 실행된다.
Callback Rule
1. Callback은 마지막 인자에
2. Error는 맨 앞 인자에
에러가 없으면 이 인자는 null / undefined이고, 에러 파라미터에 넘기는 아규먼트는 항상 Error type이어야 함.
3. 에러 전파
- 동기식 : throw 문
- 비동기식 CPS : 호출 체인의 다음에서 callback으로 전달
import { readFile } from 'fs'
function readJSON (filename, callback) {
readFile(filename, 'utf8', (err, data) => {
let parsed
if (err) {
// propagate the error and exit the current function
return callback(err)
}
try {
// parse the file contents
parsed = JSON.parse(data)
} catch (err) {
// catch parsing errors
return callback(err)
}
// no errors, propagate just the data
callback(null, parsed)
})
}
const cb = (err, data) => {
if (err) {
return console.error(err)
}
console.log(data)
}
readJSON('valid_json.json', cb) // dumps the content
readJSON('invalid_json.json', cb) // prints error (SyntaxError)
try 블록 내부에서 callback을 호출하지 않은 이유
callback 자체에서 발생하는 에러를 위해 try 문을 사용한 것이 아니기 때문!
4. 캐치되지 않는 예외
비동기 callback 내부 에러 발생 -> 예외가 EventLoop로 이동 -> 절대 다음 callback으로 체이닝 되지 않음
이런 일련의 과정을 제대로 처리하지 않으면 회복 불능 상태가 됨.
예외는 비동기식 실행을 발생시키는 함수 내부가 아니라!!!
-> 이벤트 루프에 예외가 발생한 별도의 콜 스택을 타고 올라간다.
따라서 예외가 EventLoop에 도달하면 종료시켜야한다.
import { readFile } from 'fs'
function readJSONThrows (filename, callback) {
readFile(filename, 'utf8', (err, data) => {
if (err) {
return callback(err)
}
callback(null, JSON.parse(data))
})
}
// The error is not propagated to the final callback nor is caught
// by a try/catch statement
try {
readJSONThrows('invalid_json.json', (err) => console.error(err))
} catch (err) {
console.log('This will NOT catch the JSON parsing exception')
}
// Our last chance to intercept any uncaught error
process.on('uncaughtException', (err) => {
console.error(`This will catch at last the JSON parsing exception: ${err.message}`)
// Terminates the application with 1 (error) as exit code.
// Without the following line, the application would continue
process.exit(1)
})
'Backend > Node.js' 카테고리의 다른 글
비동기 흐름 제어 by Callback (0) | 2025.01.06 |
---|---|
Observer 패턴 (0) | 2025.01.06 |
Module System ( ESM ) (0) | 2025.01.06 |
Module System ( CJS ) (0) | 2025.01.06 |
Node.js Interview (0) | 2024.12.30 |