event-driven

moongq·2021년 1월 22일
0

원본: https://medium.com/edge-coders/understanding-node-js-event-driven-architecture-223292fcbc2d

HTTP 요청, 응답 및 스트림과 같은 대부분의 Node 객체는 EventEmitter 모듈을 구현하여 이벤트를 내보내고 수신하는 방법을 제공 할 수 있습니다.

이벤트 기반 특성의 가장 간단한 형태는 콜백입니다. 일부 node.js중에서 (ex. fs.readFile)은 이벤트를 한번 발생시키고 콜백 그자체가 이벤트 핸들러로써 작동합니다.

준비되면 불러주세요! Node!

Node의 original 비동기 이벤트를 다루는 방법은 callback이었습니다. 그 오리지널이 아주 오래전이라서 현재는 promise, async/await까지 생겨났죠.

Callback은 기본적으로 함수일뿐입니다. 함수이지만 다른 함수와 연결되는 함수죠. js에서는 함수 자체가 1급 객체이기때문에 이것이 가능해집니다.

1급객체: 아래 조건들을 만족한다면 1급객체입니다.

  • 변수에 담을 수 있다.
  • 인자로 전달할 수 있다.
  • 반환값으로 전달될 수 있다.

Callback은 단순히 비동기라고만 생각하면 안됩니다. Callback은 비동기, 동기 모두 될 수 있습니다.
예를 들어 다음은 콜백 함수 'cb'를 받아들이고 조건에 따라 동기식 및 비동기식으로 콜백 함수를 호출하는 함수(fileSize)입니다.

function fileSize (fileName, cb) {
  if (typeof fileName !== 'string') {
    return cb(new TypeError('argument should be string')); // 동기
  }
  fs.stat(fileName, (err, stats) => {
    if (err) { return cb(err); } // 비동기
    cb(null, stats.size); // 비동기
  });
}

위 예제는 동기/비동기가 같이 쓰이기 때문에 예상 불가한 에러를 발생시킵니다. 반드시 한가시 방식으로 함수를 만드세요.

콜백 스타일로 작성된 일반적인 비동기 함수의 예를 살펴보겠습니다.

const readFileAsArray = function (file, cb) { 
  fs.readFile (file, function (err, data) { 
    if (err) { 
      return cb (err); 
    }
    const lines = data.toString (). trim (). split ( '\ n'); 
    cb (null,); 
  }); 
};

// 결과값.
10 
11
12
13
14
15

readFileAsArray는 파일 경로와 콜백을 받습니다. 이 함수는 파일을 읽어오고 배열로 분리시킵니다. 그리고 분리된 배열과 함께 콜백을 호출합니다.

fs.readFile()함수를 살펴봅시다. 이 함수는 정말 순수한 callback 스타일을 따릅니다. 콜백 함수의 첫째 인자로 err를 받고, 마지막 인자로 callback에 전달할 인자를 받습니다. 인자를 설정하는 것들은 콜백을 이용할 때 꼭 지켜야 합니다.

modern js의 callback의 대체제.

promise 객체가 훌륭한 비동기 callback의 대체제가 될 수 있습니다. 같은 공간(nesting)내에서 에러를 처리하고, 콜백을 전달하던 callback대신에, promise는 에러와 성공의 상황을 분리시켜서 처리할 수 있습니다. 그리고 분리시킨 뒤 함수를 연결하듯이 연결시킬수 있습니다.

만약 readFileAsArray 함수가 promise를 지원해준다면 아래 코드 처럼 이용할 수 있습니다.

readFileAsArray('./numbers.txt')
  .then(lines => { // 성공했을 때.
    const numbers = lines.map(Number);
    const oddNumbers = numbers.filter(n => n%2 === 1);
    console.log('Odd numbers count:', oddNumbers.length);
  })
  .catch(console.error);  // 실패했을 때.

만약 readFileAsArray 함수가 비동기를 지원하지 않는다면 Promise 인터페이스를 이용하여 새로운 Promise 객체로 변경시킬 수 있습니다.

const readFileAsArray = function(file, cb = () => {}) {
  return new Promise((resolve, reject) => {
    fs.readFile(file, function(err, data) {
      if (err) {
        reject(err);
        return cb(err);
      }

      const linse = data.toString().trim().split('\n');
      resolve(lines);
      cb(null, lines);
    });
  });
};

프로미스 객체는 resolve 함수와 reject함수를 노출시킵니다. 에러와 함께 콜백을 호출시키고 싶다면 reject함수를 호출합니다. 작업 완료된 데이터와 콜백을 호출 시키려면 resolve함수를 호출하면 됩니다.

async/await와 함께 promise를 소비하기

만약 당신의 코드에 비동기 함수가 있고 이 비동기 함수를 반복시켜야 한다면 promise 인터페이스가 큰도움이 될 것입니다. 만약 콜백을 사용하면 작업 과정이 복잡해질수 있습니다. 그럴 때는 async/await 함수를 이용하면 편합니다. async/await은 비동기 코드를 동기 코드처럼 다룰수 있게 해주고 더불어 코드를 읽기 편하게 만들어줍니다.

아래 예제는 readFileAsArray함수를 async/await으로 변환한 것입니다.

async function consumePromise() {
  try {
    const lines = await readFileAsArray('./numbers');
    console.log(lines);
  } catch(err) {
    console.error(err);
  }
}

countOdd();

첫째로 async 함수를 만들어야 합니다. 일반 함수를 만들듯이 만들지만 앞에 async를 붙여줍니다. 둘째로는 만든 async 함수안에 readFileAsArray 함수를 호출합니다. 여기서 중요한 것이 await를 붙여주어야 합니다.그 뒤엔 readFileAsArray 가 동기적을 작동한다고 생각하고 나머지 작업을 마치면 됩니다. 그리고 마지막으로 async가 붙어있는 함수를 호출합니다. async/await 방식은 참 간편하고 읽기 편합니다. + 에러를 처리하고 싶다면 try/catch문을 이용하면 됩니다.

async/await를 이용하면 .then, .catch와 같은 특별한 API를 이용할 필요가 없어집니다. 그리고 단순히 async, await 이 붙은 함수를 javascript와 이용하면 됩니다.

async/await은 promise 인터페이스를 지원하는 모든 종류의 것들과 이용할 수 있지만, callback 스타일에서는 이용할 수 없습니다.(ex. setTimeout)

EventEmitter 모듈

EventEmitter는 객체간의 커뮤니케이션을 촉진시켜주는 모듈입니다. 대부분의 노드 내부 모듈들은 EventEmitter를 상속받습니다.

개념은 간단합니다. emitter는 상황에 맞는 이벤트를 설정해둡니다. 그리고 설정되어 있는 이벤트를 발생시킵니다.

EventEmitter를 이용하기 위해 단순히 EventEmitter를 확장하는 클래스를 만들면 됩니다.

class MyEmitter extends EventEmitter {
}

Emitter 객체는 방금 우리가 EventEmitter를 확장하여 만든 클래스의 객체입니다.

const myEmitter = new MyEmitter();

emitter가 살아있는 동안 우리는 언제든지 미리 등록해놓은 이벤트를 발생시킬 수 있습니다.

myEmitter.emit('something-happened');

이벤트를 발생시키는 것은 특정 상황이 발생했다는 것을 나타냅니다. 주로 상태벼화를 나타내죠.

방출된, 호출된 이벤트에대해 반응하기 위해 우리는 on 메서드를 이용하면 됩니다.

myEmitter.on('somthing-happend', toSometing())

callback대신에 이벤트를 이용하는 이유? 장점?이 있다면 같은 상황에 대해 여러번 반응하기에 편하다는 것입니다. 이벤트 대신에 콜백을 이용한다면 매번의 콜백마다 이벤트에 해당하는 로직을 추가해야됩니다.

이벤트 리스너의 순서

같은 이벤트에 반응하는 리스너가 복수개라면 작성된 순서대로 반응하게 됩니다.
만약 새 리스너를 정의해야하지만 해당 리스너를 먼저 호출해야하는 경우 prependListener 메소드를 사용할 수 있습니다.

// 먼저 실행됨
withTime.on('data', (data) => {
  console.log(`Length: ${data.length}`);
});

// 두번째로 실행됨
withTime.prependListener('data', (data) => {
  console.log(`Characters: ${data.toString().length}`);
});

EventEmitter를 막상 써보면 크게 어렵지 않습니다. 한번 이용해보시기 바랍니다.

profile
https://medium.com/nodejs-server

0개의 댓글