then 메소드를 갖는 객체를 뜻한다. 체이닝이나 await과 같은 promise 패턴을 가진 구문에서 사용할 수 있다. 모든 promise객체는 thenable 객체이지만 역은 성립하지 않는다.

간단한 thenable 객체 만들기

const thenable = {
  then(onFulfilled, rejected) {
    setTimeout(() => {
      onFulfilled(42);
    }, 10);
  },
};

Promise.resolve(1)
  .then(() => thenable)
  .then(console.log);

const v = await thenable;
console.log('v: ', v);

위의 예시처럼 thenable 객체는 await를 통해 then() 메소드를 호출하여 값을 받을 수 있다.

then() 메소드의 두 인자 (onFulfilled, rejected)

const obj = {
  then(onFulfilled) {
    console.log('inside then method');
    console.log(onFulfilled.toString());
  },
};

(async () => {
  console.log('start');
  await obj;
  console.log('end');
})();

// output>
// start
// inside then method
// function () { [native code] }

then() 메소드의 인자로 두 가지 콜백이 전달된다. 정상적으로 이행했을 때 실행될 코드(onFulfilled)와 실패했을 때 실행될 콜백(rejected)가 그것이다.

위 예제는 onFulfilled을 전달받아서 호출하지 않았다. 그래서 하단에 await문 이후로 코드가 실행되지 않아 console.log(”end”)가 실행되지 않았다. 이 코드를 실행되도록 바꾸면 다음과 같다.

const obj = {
  then(onFulfilled) {
    console.log('inside then method');
    console.log(onFulfilled.toString());
    onFulfilled('resovled');
  },
};

console.log('start');
const result = await obj;
console.log('result: ', result);
console.log('end');

// output>
// start
// inside then method
// function () { [native code] }
// result: resolved
// end
const obj = {
  then(onFulfilled, rejected) {
    console.log('inside then method');
    let i = Math.floor(Math.random() * 10);
    if (i % 2 == 0) {
      onFulfilled('resovled');
    } else {
      rejected('rejected');
    }
  },
};
try {
  console.log('start');
  const result = await obj;
  console.log('result: ', result);
} catch (error) {
  console.log('error', error);
} finally {
  console.log('end');
}

두 번째 인자를 전달하여 호출하게 되면 연산의 실패를 알릴 수 있게 된다. 이를 통해 await문에서 try…catch문을 통해 에러 핸들링도 가능하게 된다.

활용

thenable객체를 활용한다면 비동기 처리 과정에서 특정 로직을 체이닝이나 await를 통해 함께 처리할 수 있게 된다. 다음은 간단한 예이다.

const asyncFunction = (n) =>
  new Promise((resolve, reject) => {
    if (n % 2 > 0) resolve('it is resolved');
    else reject('it is rejected');
  });

const mathOp = (value) => {
  const op = {
    add(n) {
      value += n;
      return op;
    },
    sub(n) {
      value -= n;
      return op;
    },
    mul(n) {
      value *= n;
      return op;
    },
    div(n) {
      value /= n;
      return op;
    },
    then(callback, rejected) {
      return asyncFunction(value).then(callback, rejected);
    },
  };
  return op;
};

try {
  const result = await mathOp(5).add(2).sub(3).mul(-1).add(5).add(1);
  console.log(result);
} catch (error) {
  console.log('error', error);
}

위 코드는 홀수인지 짝수인지에 따라 성공 실패 결과를 알려주는 로직이다. 체이닝을 통해 연산을 진행하고 마지막 판단 로직을 then()메소드를 통해 만들어 await로 외부에 값을 전달할 수 있게 되었다.

이처럼 thenable객체는 체이닝이나 await키워드를 통해 가독성 좋은 코드를 만들 수 있는 이점이 있다.

정리

thenable 객체는 promise만의 것이 아니다. thenable객체를 직접 만들어서 사용한다면 비동기 과정에서 일어나는 일련의 과정 사이에 원하는 로직을 추가하여 더 풍부하고 가독성 좋은 코드를 만들 수 있게 된다.