콜백 사용 시점 - ChoDragon9/posts GitHub Wiki

글의 목적

자바스크립트 코드를 작성할 때 콜백을 사용하는 시점은 명확히 구분해서 사용하고 있다. 비동기 코드를 작성할 때는 Promise에만 사용하고, 동기 코드를 작성할 때는 map, filter, reduce와 같은 함수의 인자로 사용한다.

비동기 코드를 작성할 때, 왜! 인자로 콜백을 사용하지 않는 지 뻔한 이야기를 다시 되새겨 보려고 한다. 그리고 동기 코드에서 콜백을 인자로 사용하는 의미를 정리해보려고 한다.

목차

비동기 처리 시 콜백 패턴은 왜 적절하지 않을까

비동기 처리 시 콜백 패턴을 사용하면 중첩과 들여쓰기로 코드를 알아보기 어렵게 한다. 하지만 이것은 시선의 주의를 분산시키는 부수적인 요소일 뿐이고 다른 이유가 있다.

비동기 처리 시 콜백 패턴을 사용하면 안되는 이유는 두가지로 정리할 수 있다.

첫번째는 콜백 패턴은 비동기 흐름을 비선형적, 비순차적인 방향으로 나타낸다. 그래서 구현된 코드를 이해하기 위해서는 한 함수에서 다른 함수로 또 그 다음 함수로 순차적인 흐름을 따라가기 위해 코드 베이스 전체를 널뛰기 해야 한다.

사람의 두되는 순차적, 단일-스레드 방식을 계획하는 데 익숙하지만 콜백 패턴은 사람이 추론하기 힘들게 만든다.

두번째는 콜백 패턴은 제어권을 호출 파트에 암시적으로 넘겨줘야 하므로 믿음성 문제에 봉착하게 된다.

원하지 않는 반복적인 호출을 방지하거나, 성공/에러 신호를 동시에 받거나 전혀 못 받을 수 있는 상황을 걸러내는 코딩을 추가적으로 필요로 한다.

비동기 흐름 제어와 함수 제어권에 문제가 발생하기 때문에 콜백을 인자로 받는 것은 이해하기 쉬운 코드를 작성함에 있어 방해가 된다.

해결 방안은 우리 모두가 알고 있는 Promise를 사용하는 것이다. Promise를 사용하면 비동기 흐름 제어와 함수 제어권을 가질 수 있다.

동기 코드에서 콜백을 인자로 사용하는 의미는 무엇인가?

동기 코드에서 콜백을 인자로 사용하는 의미는 제어권을 나눠가지는 것을 의미한다. 제어권을 나눠가지게 되면 재사용성과 다형성을 할 수 있게 된다.

앞에서 콜백을 사용하면 제어권에 문제가 생기는 것을 설명했다. 비동기일 때는 콜백을 사용하면 순서를 보장하기 힘들기 때문에 콜백에 반환값이 없다. 하지만 동기 코드에서는 순서가 보장되기 때문에 반환값을 가질 수 있다. 그 결과 제어권 또한 나눠가지게 된다.

여기 배열을 반복해서 1을 더하는 add 함수가 있다고 가정하겠다. 이 함수는 반복과 연산의 제어권을 모두 가지고 있다.

const add = (arr) => {
  const newArr = []
  for (const item of arr) {
    newArr.push(item + 1)
  }
  return newArr
}

const arr = [1, 2, 3]
const result = add(arr)
console.log(result) // [2, 3, 4]

이젠 콜백을 인자로 받아 제어권을 나눠서 가져보자. 콜백을 인자로 받아 반복과 연산의 제어권 중 연산의 제어권을 얻을 수 있다.

1을 더하는 연산의 제어권을 add함수를 사용하는 측에서 제어하게 되었다. 또한 다른 연산도 할 수 있게 되었다.

const add = (arr, callback) => {
  const newArr = []
  for (const item of arr) {
    newArr.push(callback(item))
  }
  return newArr
}

const arr = [1, 2, 3]
const added1 = add(arr, (item) => item + 1)
const added10 = add(arr, (item) => item + 10)
console.log(added1) // [2, 3, 4]
console.log(added10) // [11, 12, 13]

이제 다형성에 대한 이야기로 넘어가자. 다형성을 쉽게 설명하면 2개 이상의 타입에서 동작하는 함수를 다형성을 이룬다고 현상을 설명할 수 있다. 즉, 2개 이상의 타입이 사용할 수 있는 연산의 현상을 말한다.

add 함수는 숫자를 더하는 기능 뿐만 아니라 배열의 아이템을 수정하는 기능을 가지고 있다. 이러한 함수는 map이라고 불린다. add함수명을 map으로 바꿔 사용한 예제이다.

const map = (arr, callback) => {
  const newArr = []
  for (const item of arr) {
    newArr.push(callback(item))
  }
  return newArr
}
const arr = [1, 2, 3]
const result = map(arr, (item) => item + 1)
// [2, 3, 4]
const product = [
  { price: 1000, count: 2, total: 0 },
  { price: 2000, count: 3, total: 0 }
]
const changedProduct = map(product, ({price, count}) => {
  return {
    price,
    count,
    total: price * count
  }
})
// [
//   { price: 1000, count: 2, total: 2000 },
//   { price: 2000, count: 3, total: 6000 }
// ]

결론

비동기 코드에서는 콜백을 인자로 사용하면 비순차적인 흐름 제어와 제어 역전에 따른 믿음성 문제가 발생하지만 동기 코드에서는 제어권을 나눔으로써 재사용성과 다형성을 가지게 된다. 같은 콜백 패턴이지만 경우에 따라 많은 이점을 가져다 준다. 적절하게 사용된다면 이해하기 쉬운 코드의 일한이 될 거라고 생각된다.