본문 바로가기

카테고리 없음

비동기적 프로그래밍(2) - 2. 프로미스, 3. 제너레이터

책 <러닝자바스크립트>를 기본으로 배운 자바스크립트 내용입니다.

 

목차

  • Promise
  • Promise 생성
  • Promise의 메서드 체이닝(method chaining)
  • Promise chain
  • 제너레이터
  • 비동기적 프로그래밍 요약

 

<비동기적 프로그래밍(2) - 프로미스, 제너레이터>

Promise

  • 프로미스는 현재에는 당장 얻을 수 없지만 가까운 미래에는 얻을 수 있는 어떤 데이터에 접근하기 위한 방법을 제공한다. 당장 원하는 데이터를 얻을 수 없다는 것은 데이터를 얻는데까지 지연시간이 발생하는 경우를 말한다. 때문에 Non-blocking코드를 지향하는 자바스크립트에서는 비동기 처리가 필수적이다.
  • 프로미스 기반 비동기적 함수를 호출하면 그 함수는 Promise인스턴스를 반환한다.
  • 프로미스는 완전히 resolve(성공), reject(실패) 딱 2가지 뿐이다.
  • 프로미스는 콜백을 예측 가능한 패턴으로 사용할 수 있게 한다.
  • 프로미스는 객체이므로 어디든 전달할 수 있다.
  • 프로미스는 체인으로 연결할 수 있다는 장점이 있다.
  • 콜백의 단전임 콜백 헬을 보완하기 위해서 만들어진 것이 프로미스이다. 콜백 자체를 대체하는 것이 아니라 프로미스를 사용함으로써 안전하고 관리하기 쉬운 코드를 만들 수 있다.
findUser(1).then(function(user) {
  console.log("user:", user);
});

function findUser(id) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log("waited 0.1 sec.");
      const user = {
        id: id,
        name: "User" + id,
        email: id + "@test.com"
      };
      resolve(user);
    }, 100);
  });
}

//실행 결과
waited 0.1 sec.
user: {id: 1, name: "User1", email: "1@test.com"}

 위 코드는 콜백 함수를 인자로 넘기는 대신에 Promise객체를 생성하여 리턴하였고, 호출부에서는 리턴받은 Promise객체에 then()메서드를 호출하여 결과값을 가지고 실행할 로직을 넘겨주고 있다.

콜백 함수를 통해 비동기 처리를 하던 기존 코드와 가장 큰 차이점은 함수를 호출하면 Promise 타입의 결과값이 리턴되고, 이 결과값을 가지고 다음에 수행할 작업을 진행한다는 것이다.
따라서 기존 스타일보다 비동기 처리 코드임에도 불구하고 마치 동기 처리 코드 처럼 코드가 읽히기 때문에 좀 더 직관적으로 느껴지게 된다.

 

Promise 생성

  • Promise는 객체는 new 키워드와 생성자를 통해서 생성할 수 있는데 이 생성자는 함수를 인자로 받는다. 그리고 이 함수 인자는 reslove와 reject라는 2개의 함수형 파라미터를 가진다.
  • 일반적으로 resolve() 함수의 인자로는 미래 시점에 얻게될 결과를 넘겨주고, reject() 함수의 인자로는 미래 시점에 발생할 예외를 넘겨준다.
  • 정상적인 인자를 넘긴 경우 then() 메서드가 호출되고, 비정상적인 인자를 넘긴 경우 catch() 메서드가 호출된다.
# 프로미스 생성해 변수에 할당
const promise = new Promise(function(resolve, reject) { ... } );

# 프로미스 생성해 함수의 리턴값으로 사용
function countdown(seconds){
    return new Promise(function(resolve, reject) => { ... } );
}

# 프로미스 사용
countdown(5)
    .then(result => console.log("성공:", result))
    .catch(error => console.log("실패:", error));

 

Promise의 메서드 체이닝(method chaining)

then()과 catch() 메서드는 또 다른 Promise 객체를 리턴한다. 그리고 이 Promise 객체는 인자로 넘긴 콜백 함수의 리턴값을 다시 then()과 catch() 메서드를 통해 접근할 수 있도록 해준다.
간단히 말해, then()과 catch() 메서드는 마치 사슬처럼 계속 연결하여 연쇄적으로 호출을 할 수 있다.

 

#Promise의 method chaining 예제

fetch("https://jocoma.tistory.com/")
  .then(response => response.json())
  .then(post => post.userId)
  .then(userId => "url" + userId)
  .then(url => fetch(url))
  .then(response => response.json())
  .then(user => console.log("user:", user))
  .catch(error => console.log("error:", error));

 

Promise chain

  • 프로미스는 체인으로 연결할 수 있다
  • 프로미스가 완료되면 다른 프로미스를 반환하는 함수를 즉시 호출할 수 있다.
const p1 =  new Promise(function(resolve, reject){
  resolve(1);
})

const p2 = new Promise(function(resolve, reject){
  resolve(3);
})

p1.then(function(data){
  console.log(data);
  return p2;
  })
 .then(function(data){
  console.log(data)
 })

 

제너레이터

제너레이터는 원래 동기적인 성격이지만, 프로미스와 결합하면 비동기 코드를 효율적으로 관리 할 수 있다.

//노드와 오류 우선 콜백을 프로미스로 변환
function nfcall(f, ...args) {
    return new Promise(function(resolve, reject){
        f.call(null, ...args, function(err,...args) {
            if(err) return reject(err);
            resolve(args.length<2 ? args[0] : args);
        });
    });
}

// setTimeout과 같은 기능에 오류 우선 콜백 패턴을 따르는 ptimeout 함수를 만들어 이용
function ptimeout(delay) {
    return new Promise(function(resolve, reject){
        setTimeout(resolve, delay);
    });
}

//제너레이터 실행기 : 제너레이터와 호출자의 통신을 관리하고, 비동기적 호출 처리하는 grun 함수 만들어 이용
function grun(g) { 
    const it = g();
    (function iterate(val){
        const x = it.next(val);
        if(!x.done) {
            if(x.value instanceof Promise) {
                x.value.then(iterate).catch(err => it.throw(err));            
            }else{
                setTimeout(iterate,0,x.value);
            }
        }
    })();
}

//비동기적이지만 마치 동기적 코드인것처럼 사람이 생각했을 법한 방법을 그대로 사용가능
function* theFutureIsNow() {
    const dataA = yield nfcall(fs.readFile, 'a.txt');
    const dataB = yield nfcall(fs.readFile, 'b.txt');
    const dataC = yield nfcall(fs.readFile, 'c.txt');
    yield ptimeout(60*1000);
    yield nfcall(fs.writeFile, 'd.txt', dataA+dataB+dataC);
}

//실행
grun(theFutureIsNow);

※제너레이터 실행기는 다음 코드를 참고하도록 한다.

co(http://github.com/tj/co)

koa(http://koajs.com/)

 

<비동기적 프로그래밍 요약>

 - 자바스크립트의 비동기적 실행은 콜백을 통해 이루어진다.
 - 프라미스를 콜백 대신 사용할 수 있는 건 아니다. 프라미스 역시 콜백을 사용한다.
 - 프라미스는 콜백이 여러 번 호출되는 문제를 해결했다.
 - 콜백을 여러 번 호출해야 한다면 이벤트와 결합하는 방법을 생각할 수 있다(프라미스도 함께 쓸 수 있다).
 - 프라미스는 반드시 결정된다는(성공 또는 실패한다는) 보장은 없다. 프라미스에 타임아웃을 걸면 이 문제가 해결된다.
 - 프라미스는 체인으로 연결할 수 있다.
 - 프라미스와 제너레이터 실행기를 결합하면 비동기적 실행의 장점을 그대로 유지하면서도 동기적인 사고방식으로 문제를 해결할 수 있다.
 - 제너레이터를 써서 동기적인 사고방식으로 문제를 해결할 때는 프로그램의 어느 부분을 동시에 실행할 수 있는지 잘 살펴야 한다. 동시에 실행할 수 있는 부분은 Promise.all을 써서 실행하면된다. 
 - 제너레이터 실행기를 쓰면 예외 처리도 익숙한 방식으로 할 수 있다.비동기적 실행을 자바스크립트에서 처음 접했다면 막막하고 어려울 수 있지만, 최신 자바스크립트 프로젝트에서는 비동기적 실행이 아주 많이 사용된다.