본문 바로가기

오픈소스/노드

[Node] 04. 콜백을 사용한 비동기 제어 흐름 패턴

 본 문서는 Node.js 디자인 패턴 바이블을 읽고 리뷰를 남기고 있습니다. 문고들은 이 책의 일부분을 인용한 것임을 밝힙니다. 동기식 프로그래밍 스타일을 사용하는 플랫폼에서 Node.js와 같이 연속 전달 스타일과 비동기 API를 일반적으로 사용하는 플랫폼으로의 적응은 쉽지 않을 수 있습니다. 비동기 코드는 구문이 실행되는 순서를 예측하기 어렵게 할 수 있습니다. 

 

 이 장에서는 몇 가지 규칙과 패턴을 사용하여 실제로 어떻게 콜백을 능숙하게 제어하며, 깔끔하게 관리 가능한 비동기 코드를 작성할 수 있는지 살펴볼 것입니다. 콜백을 올바르게 다루는 법을 알게 되는 것은 프라미스와 async/await와 같이 최근에 쓰이는 접근법을 사용하기 위한 초석이 될 것입니다.

 

 

# Contents


  • 비동기 프로그래밍의 어려움
  • 간단한 예제

 

 

# 비동기 프로그래밍의 어려움


 JavaScript에서는 비동기 코드에 대한 제어를 놓치는 일이 흔하게 일어납니다. 클로저와 익명 함수의 in-place 정의를 통해 개발자는 코드베이스 지점들을 옮겨 다니지 않고 원할한 프로그래밍을 할 수 있게 됩니다.
 하지만 무조건적인 장점이 있는 것 만은 아닙니다. 모듈화, 재사용성 그리고 유지보수성과 같은 도움을 주는 대신 불행히도 콜백의 중첩을 통제할 수 없이 급증하게 되고, 함수의 크기가 커지며 구성 또한 엉망이 되게 합니다.
 그래서 비동기 프로그래밍을 작성할 때에는 규칙들을 지켜서 코딩을 하는 것이 중요합니다. 코드가 다루기 힘들어지는지 좋아지고 있는지를 인지하며 그것이 통제하기 힘들어지는 것을 미리 알고 그에 따라 최선의 해결책을 가지고 행동하는 것이 전문가와 초보자의 차이입니다.

 

1. 웹 스파이더 예제 (1)

 책에서는 웹 스파이더를 만들어서 콜백의 복잡도를 설명하고 있습니다. 주소는 아래와 같습니다.

https://github.com/PacktPublishing/Node.js-Design-Patterns-Third-Edition/tree/master/04-asynchronous-control-flow-patterns-with-callbacks/01-web-spider

 

GitHub - PacktPublishing/Node.js-Design-Patterns-Third-Edition: Node.js Design Patterns Third Edition, published by Packt

Node.js Design Patterns Third Edition, published by Packt - GitHub - PacktPublishing/Node.js-Design-Patterns-Third-Edition: Node.js Design Patterns Third Edition, published by Packt

github.com

 

 애플리케이션의 핵심기능은 spider.js 라는 모듈에 있습니다. 사용할 모든 종속성을 로드하는 것으로 시작하겠습니다.

 

import fs from 'fs'
import path from 'path'
import superagent from 'superagent'
import mkdirp from 'mkdirp'
import { urlToFilename } from './utils.js'

export function spider (url, cb) {
  const filename = urlToFilename(url)
  fs.access(filename, err => { // [1]
    if (err && err.code === 'ENOENT') {
      console.log(`Downloading ${url} into ${filename}`)
      superagent.get(url).end((err, res) => { // [2]
        if (err) {
          cb(err)
        } else {
          mkdirp(path.dirname(filename), err => { // [3]
            if (err) {
              cb(err)
            } else {
              fs.writeFile(filename, res.text, err => { // [4]
                if (err) {
                  cb(err)
                } else {
                  cb(null, filename, true)
                }
              })
            }
          })
        }
      })
    } else {
      cb(null, filename, false)
    }
  })
}

 

 콜백함수의 사용을 그대로 처리하게 되면 복잡하게 느껴질 뿐 아니라 가독성도 좋아지지 않습니다. 다음 섹션을 통해 이 코드의 가독성을 어떻게 향상시키고 가능한 어떻게 콜백 기반 코드를 깔끔하게 유지할 수 있는지 배워보겠습니다. 

 해당 코드를 일정한 콜백 규칙을 통해 적용해보도록 하겠습니다. 

 

2. 콜백 규칙

 콜백 지옥에 대한 첫 번째 예를 보았고 이제 이것을 피해야 함을 알게되었습니다. 그러나 비동기 코드를 작성할 때 주의해야 할 사항은 그것 뿐만이 아닙니다. 실제로 일련의 비동기 작업들의 흐름을 제어하려면 특정 패턴과 기법을 사용해야만 하는 상황이 있습니다. 특히 외부 라이브러리를 사용하지 않고 일반 JavaScript만 사용하는 경우에는 더욱 그렇습니다. 먼저 콜백 규칙을 통해 콜백 지옥을 피하는 방법에 대해 설명하도록 하겠습니다.

 

 비동기 코드를 작성할 때 명심해야 할 첫 번째 규칙은 콜백을 정의할 때 in-place 함수의 정의를 남용하지 않는 것 입니다. 모듈화 및 재사용성과 같은 문제에 대한 추가적인 사항을 고려할 필요가 없어 매력적이겠지만, 이는 장점보다 단점이 더 많을 수 있는 방식이라는 것을 이미 앞서 살펴보았습니다. 대부분의 경우 콜백 지옥 문제를 해결하기 위한 어떤 라이브러리나 멋집 기술 혹은 패러다임의 변화가 필요한 것은 아니며, 간단하고 일반적인 상식이면 충분합니다.

 

 다음은 중첩수준을 낮게 유지하고 일반적으로도 코드 체계를 개선하는데 도움이 되는 몇 가지 기본 원칙입니다.

 

  • 가능한 빨리 종료합니다.
  • 콜백을 위해 명명된 함수를 생성하여 클로저 바깥에 배치하며 중간 결과를 인자로 전달합니다.
  • 코드를 모듈화 합니다.

 

이 원칙들을 적용하여 위의 코드를 바꿔보겠습니다. 예제 코드의 주소는 아래와 같습니다.

https://github.com/PacktPublishing/Node.js-Design-Patterns-Third-Edition/tree/master/04-asynchronous-control-flow-patterns-with-callbacks/02-web-spider-functions

 

GitHub - PacktPublishing/Node.js-Design-Patterns-Third-Edition: Node.js Design Patterns Third Edition, published by Packt

Node.js Design Patterns Third Edition, published by Packt - GitHub - PacktPublishing/Node.js-Design-Patterns-Third-Edition: Node.js Design Patterns Third Edition, published by Packt

github.com

 

 앞에 함수와 동일한 처리를 콜백 규칙을 통해 적용시킨 코드 는 다음과 같습니다.

 

import fs from 'fs'
import path from 'path'
import superagent from 'superagent'
import mkdirp from 'mkdirp'
import { urlToFilename } from './utils.js'

function saveFile (filename, contents, cb) {
  mkdirp(path.dirname(filename), err => {
    if (err) {
      return cb(err)
    }
    fs.writeFile(filename, contents, cb)
  })
}

function download (url, filename, cb) {
  console.log(`Downloading ${url}`)
  superagent.get(url).end((err, res) => {
    if (err) {
      return cb(err)
    }
    saveFile(filename, res.text, err => {
      if (err) {
        return cb(err)
      }
      console.log(`Downloaded and saved: ${url}`)
      cb(null, res.text)
    })
  })
}

export function spider (url, cb) {
  const filename = urlToFilename(url)
  fs.access(filename, err => {
    if (!err || err.code !== 'ENOENT') { // [1]
      return cb(null, filename, false)
    }
    download(url, filename, err => {
      if (err) {
        return cb(err)
      }
      cb(null, filename, true)
    })
  })
}

 

 spider() 함수의 기능 및 인터페이스는 완전히 똑같습니다. 코드가 구성된 방식만 바뀌었습니다. 알아보아야 할 한가지 중요한 세부사항은 우리가 앞서 언급한 빠른 반환 원칙이 적용될 수 있도록 파일의 존재 유무에 대한 검사의 순서를 뒤바꾸었다는 것입니다. 빠른 반환 원칙과 다른 콜백 규칙들을 적용함으로써 코드의 중첩을 크게 줄일 수 있엇으며, 동시에 재사용성 및 테스트 가능성을 높일 수 있었습니다.

 

var result = [1, 2, 3, 4, 5].map((val) => val * 2);
console.log(result); //[2, 4, 6, 8, 10]

 

 콜백은 배열 내의 요소를 반복하는데 사용될 뿐 연산 결과를 전달하지 않습니다. 실제로 여기서 결과는 직접적인 방식으로 동기적으로 반환됩니다.

 

2. 큐를 사용하여 제한된 병렬 실행

 기존 책에서는 재귀함수로 적용하여 순차적인 웹 스파이더를 호출하여 웹 스파이더 버전2를 코딩하였고, 그로 인해 페이지의 모든 링크를 재귀적으로 다운받을 수 있었습니다. 그리고 병렬 실행을 이용하여 비동기 작업을 병렬로 실행하는 방식을 웹 스파이더 버전3로 코딩하였습니다. 병렬 실행의 방식은 동시 작업을 실행할 때의 문제점이 발생할 수 있었는데, 경쟁 상태와 작업 동기화의 문제점이 존재하였습니다. 그래서 경쟁상태를 제거하기 위하여 제한된 병렬 실행을 제시하였고, 결과적으로 큐를 이용하여 제한된 병렬 실행을 요청하는 웹 스파이더 4를 코딩하였습니다. 

 

다음은 웹 스파이더 4의 예제 코드 입니다.

https://github.com/PacktPublishing/Node.js-Design-Patterns-Third-Edition/tree/master/04-asynchronous-control-flow-patterns-with-callbacks/11-web-spider-v4

 

GitHub - PacktPublishing/Node.js-Design-Patterns-Third-Edition: Node.js Design Patterns Third Edition, published by Packt

Node.js Design Patterns Third Edition, published by Packt - GitHub - PacktPublishing/Node.js-Design-Patterns-Third-Edition: Node.js Design Patterns Third Edition, published by Packt

github.com

 

 

# 간단한 예제


  콜백 함수와 일급 함수의 장점을 통하여 간단한 예제를 만들어보았습니다. 이 예제는 arr에 값에 함수를 넣어 각각의 값들을 콜백함수로 실행시키는 예제입니다. 콜백 함수의 핵심과 일급 함수의 핵심을 넣어서 간략화 하였습니다. 아주 쉽지만 유용하게 쓰일 수 있는 개념인지라 이해하는 것이 중요하다고 생각합니다.

 

var arr = [];

function pushArr(listner) {
    arr.push(listner);
}

function sum(a, b, cb) {
    cb(new Error(), a + b);
}

function division(a, b, cb) {
    cb(null, a / b);
}

function square(a, b, cb) {
    cb(null, a * b);
}

function main() {
    pushArr((cb) => {
        sum(10, 5, cb);
    });
    pushArr((cb) => {
        division(10, 5, cb);
    });
    pushArr((cb) => {
        square(10, 5, cb);
    });

    calculate();
}

function calculate() {
    arr.forEach((data) => {
        data((err, res) => {
            if (err) return console.error("error");
            console.log(res);
        });
    });
}

main();

 

 

# 마무리


  이 장에서는 Node.js 프로그래밍이 비동기성으로 인해 어려울 수 있다고 언급했습니다. 특히 다른 플랫폼에서 개발한 사람들에게는 더욱 그렇습니다. 그러나 이 장을 통해서 어떻게 비동기 API가 당신에게 친숙해질 수 있는지 보았습니다.

 다음 장에서는 promise와 async/await에 영향을 미치고 채택되어 널리 사용되는 기술들에 대한 소개를 할 것입니다. 모든 기술들을 배우고 나면 프로젝트에 필요한 솔루션을 선택할 수 있게 되거나 같은 프로젝트에 여러 기술을 함께 사용할 수 있을 것입니다.