본문 바로가기
WEB/JS

(js)Promise만들기(1) - 전체코드

by jackWillow 2024. 1. 27.
// customPromise.js

var CustomPromise = (function getCustomPromise() {
  /** 프로미스 상태 */
  var PromiseState = {
    pending: 'pending',
    fulfilled: 'fulfilled',
    rejected: 'rejected',
  }
  
  /**
   * 프로미스 상태 이벤트 핸들러 생성자
   * 프로미스의 상태를 관리하고, 상태관련 eventListener 생성과 dispatchEvent를 담당
   */
  function PromiseStateEvent() {
    this.state = PromiseState.pending
    Object.defineProperty(this, 'eventTarget', {
      value: undefined,
      writable: true,
      configurable: false,
      enumerable: false
    })
  }
  
  /**
   * 현재 상태가 pending인지 확인
   * @returns {boolean}
   */
  PromiseStateEvent.prototype.isPending = function() {
    return this.state === PromiseState.pending
  }
  
  /**
   * 이벤트 타겟이 있는지 확인
   * (다음 프로미스가 있는지 확인하는 용도)
   * @returns {boolean}
   */
  PromiseStateEvent.prototype.haveTarget = function() {
    return this.eventTarget !== undefined
  }
  
  /**
   * 프로미스 확정 시(fulfilled, rejected) 콜백함수 실행 eventListener 생성
   * dispatchSettleEvent에서 인자로 넘어올 detail.result을 콜백함수에 넣어서 실행
   * @param {string} state
   * @param {function} callback
   */
  PromiseStateEvent.prototype.addSettleEventListener = function(state, callback) {
    if(!this.haveTarget()) this.eventTarget = new CustomEventTarget()
  
    this.eventTarget.addEventListener(state, function(e) {
        if(typeof callback === 'function') {
            callback(e.detail.result)
        } else {
            throw new TypeError('PromiseStateEvent.addSettleEventListener parameter ' +  callback +  ' is not a function')
        }
    })
  }
  
  /**
   * 프로미스 확정 시(fulfilled, rejected) 호출함수
   * 상태를 바꾸고, 확정 이벤트에 result를 담아서 보낸다.
   * @param {string} state 
   * @param {any} result 
   */
  PromiseStateEvent.prototype.dispatchSettleEvent = function(state, result) {
    this.state = state
    var options = {
      detail: {
        result: result
      }
    }
    if(this.haveTarget()) this.eventTarget.dispatchEvent(new MyCustomEvent(state, options))
  }
  
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
  
  /**
   * IE에서 비동기를 처리하기 위해 Promise의 기능을 간단하게 구현한 객체생성자
   * @param {function} executor 
   */
  function CustomPromise(executor) {
    PromiseStateEvent.call(this)
    this.result = undefined
  
    // 함수가 아닌 매개변수(executor)가 들어왔을 때 TypeError를 던진다.
    if(typeof executor !== 'function') {
      throw new TypeError('CustomPromise resolver ' +  executor +  ' is not a function')
    }
  
    var resolvers = getResolvers()
    var resolveFunc = resolvers.resolveFunc
    var rejectFunc = resolvers.rejectFunc
    var errorFunc = resolvers.errorFunc
    
    var curPromise = this
    // 실행순서를 가장 뒤로 보낸다.
    setTimeout(function() {
      try {
        executor(resolveFunc.bind(curPromise), rejectFunc.bind(curPromise))
      } catch(err) {
        errorFunc.call(curPromise, err)
      }
    }, 0)
  }
  
  // PromiseStateEvent 프로토타입 상속
  CustomPromise.prototype = Object.create(PromiseStateEvent.prototype)
  
  /**
   * 프로미스가 이행되었는지를 판단하는 isResolved 변수를 포함한, resolve와 reject 함수를 반환하는 함수
   * @returns {object} { resolve, reject }
   */
  function getResolvers() {
    var isResolved = false // 프로미스 이행상태
    var isRejected = false // 프로미스 거부상태
  
    /**
     * 프로미스 콜백함수가 성공 시 호출함수
     * @param {any} result 
     */
    function resolveFunc(result) {
      if(isResolved || isRejected) return
      isResolved = true
  
      // 매개변수로 프로미스가 들어오면 들어온 프로미스가 확정된 값을 result에 담는다.
      if(result instanceof CustomPromise) {
        var resolvers = getResolvers()
        var resolveFuncInner = resolvers.resolveFunc
        var rejectFuncInner = resolvers.rejectFunc
        result.then(resolveFuncInner.bind(this), rejectFuncInner.bind(this))
        return
      }
  
      settle.call(this, PromiseState.fulfilled, result)
    }
    
    /**
    * 프로미스 콜백함수가 실패 시 호출함수
    * @param {any} result 
    */
    function rejectFunc(result) {
      if(isResolved || isRejected) return
      isRejected = true
  
      // 이상하지만 rejectFunc에 매개변수로 프로미스가 들어오면 resolveFunc과 달리 그대로 result에 넘겨준다. 2024-01-26
      // if(result instanceof CustomPromise) {
      //   // 프로미스 분기
      //   return
      // }
  
      settle.call(this, PromiseState.rejected, result)
  
      // 다음 프로미스가 없는 경우 에러
      if(!this.haveTarget()) {
        _throwError(result)
      }
    }
  
    function errorFunc(err) {
      if(isRejected && !this.haveTarget()) _throwError(err) // 프로미스가 거부되었으나 다음 프로미스가 없는 경우
      if(isResolved || isRejected) return // 이미 이행 혹은 거부된 경우
      isRejected = true
      
      settle.call(this, PromiseState.rejected, err)
  
      if(!this.haveTarget()) {
        _throwError(err)
      }
    }
  
    return { resolveFunc, rejectFunc, errorFunc }
  }
  
  /**
   * 프로미스를 확정짓는 함수
   * @param {string} state // PromiseState
   * @param {any} result 
   */
  function settle(state, result) {
    this.result = result
    this.dispatchSettleEvent(state, result)
  }
  
  /**
   * throw error
   * @param {any} err
   */
  function _throwError(err) {
    if(err instanceof Error) {
      throw err
    } else {
      throw new Error(err)
    }
  }
  
  /**
   * 프로미스 확정 후 실행 시킬 이행, 거부 콜백함수 설정
   * 새로운 프로미스 인스턴스를 생성하고,
   * 이행, 거부 콜백함수를 이벤트 리스너에 저장 후 프로미스 반환
   * @param {function} onFulfilled 
   * @param {function} onRejected 
   */
  CustomPromise.prototype.then = function(onFulfilled, onRejected) {
    // 다음 실행될 프로미스
    var nextPromise = new CustomPromise(function() {})
    
    // 현재 프로미스의 이행을 처리할 onFulfilled 콜백함수를 이벤트 리스너에 저장
    this.addSettleEventListener(PromiseState.fulfilled, function(value) {
      runOnSettled.call(nextPromise, PromiseState.fulfilled, onFulfilled, value)
    })
  
    // 현재 프로미스의 거부를 처리할 onRejected 콜백함수를 이벤트 리스너에 저장
    this.addSettleEventListener(PromiseState.rejected, function(reason) {
      runOnSettled.call(nextPromise, PromiseState.rejected, onRejected, reason)
    })
  
    // 이미 프로미스가 확정된 상태라면 바로 확정이벤트를 보낸다.
    if(!this.isPending()) {
      this.dispatchSettleEvent(this.state, this.result)
    }
  
    return nextPromise
  }
  
  /**
   * 프로미스 확정 후 실행 시킬 거부 콜백함수 설정
   * 새로운 프로미스 인스턴스를 생성하고,
   * 거부 콜백함수를 이벤트 리스너에 저장 후 프로미스 반환
   * 위 기능을 then(undefined, onRejected)로 대체
   * @param {function} onFulfilled 
   * @param {function} onRejected 
   */
  CustomPromise.prototype.catch = function(onRejected) {
    return this.then(undefined, onRejected)
  }
  
  /**
   * 이전 CustomPromise의 비동기처리가 성공하고 넘어온 값을 현재 CustomPromise가 처리하는 로직
   * onFulfilled가 함수라면 먼저 실행하고 그 결과값으로 _resolve를 실행, 함수가 아니라면 바로 _resolve를 실행
   * onFulfilled의 실행 결과가 CustomPromise라면 그것을 처리한 후 _resolve또는 _reject를 실행
   * 처리하는 중 error가 발생하면 _reject를 실행
   * @param {string} state 
   * @param {function} onFulfilled 
   * @param {any} result 
   */
  function runOnSettled(state, onSettled, result) {
    var resolvers = getResolvers()
    var resolveFunc = resolvers.resolveFunc
    var rejectFunc = resolvers.rejectFunc
    var errorFunc = resolvers.errorFunc
  
    if(typeof onSettled === 'function') {
      var nextResult
      try {
        nextResult = onSettled(result)
      } catch(err) {
        errorFunc.call(this, err) // onSettled error 발생 시 _error로 값을 넘겨줌
      }
  
      // 만약 반환된 값이 CustomPromise라면 그 처리가 완료된 후 현재의 CustomPromise가 처리되도록 함
      if(nextResult instanceof CustomPromise) {
        nextResult.then(resolveFunc.bind(this), rejectFunc.bind(this))
      } else {
        resolveFunc.call(this, nextResult)
      }
  
    } else {
        if(state === PromiseState.fulfilled) resolveFunc.call(this, result)
        else rejectFunc.call(this, result)
    }
  }

  return CustomPromise
})()
// myCustomEvent.js

/**
 * 커스텀 이벤트 생성자
 * @param {string} event 
 * @param {object} params 
 * @returns {object} customEvent
 */
var MyCustomEvent = (function setCustomEventConstructor() {
  /**
   * window.CustomEvent의 constructor를 지원하지 않을 때(IE) CustomEvent() 폴리필을 구현.
   * MDN CustomEvent() Polyfill 참고.
   * https://developer.mozilla.org/ko/docs/Web/API/CustomEvent/CustomEvent
   */
  if (typeof window.CustomEvent !== "function") {
      function MyCustomEvent ( event, params ) {
          params = params || { bubbles: false, cancelable: false, detail: undefined };
          var evt = document.createEvent( 'CustomEvent' );
          evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
          return evt;
      }
      MyCustomEvent.prototype = window.Event.prototype;
      
      return MyCustomEvent
  }

  return  window.CustomEvent
})()
// customEventTarget.js

/**
 * 이벤트 타겟 생성자
 * @returns {object} eventTarget
 */
var CustomEventTarget = (function setEventTargetConstructor() {
  /**
   * window.EventTarget의 constructor를 지원하지 않을 때(IE) EventTarget 폴리필을 구현.
   * MDN EventTarget 구현 예제 참고.
   * https://developer.mozilla.org/ko/docs/Web/API/EventTarget
   */
  if(typeof window.EventTarget !== "function") {
      var CustomEventTarget = function() {
          this.listeners = {};
      };
      
      CustomEventTarget.prototype.listeners = null;
      CustomEventTarget.prototype.addEventListener = function(type, callback) {
          if (!(type in this.listeners)) {
              this.listeners[type] = [];
          }
          this.listeners[type].push(callback);
      };
      
      CustomEventTarget.prototype.removeEventListener = function(type, callback) {
          if (!(type in this.listeners)) {
              return;
          }
          var stack = this.listeners[type];
          for (var i = 0, l = stack.length; i < l; i++) {
              if (stack[i] === callback){
              stack.splice(i, 1);
              return;
              }
          }
      };
      
      CustomEventTarget.prototype.dispatchEvent = function(event) {
          if (!(event.type in this.listeners)) {
              return true;
          }
          var stack = this.listeners[event.type].slice();
          
          for (var i = 0, l = stack.length; i < l; i++) {
              stack[i].call(this, event);
          }
          return !event.defaultPrevented;
      };

      return CustomEventTarget
  }

  return  window.EventTarget
})()

 

예전에 IE환경에서 동작하는 Promise를 만들어야하는 일이 있었는데, 그때 기억을 되집어보며 다시 만들어 봤습니다.

이제는 IE 서비스도 종료되었고, 아무 의미도 없지만 한번쯤 재미로 만들어보는 것도 나쁘지 않았던 것 같습니다.
그래서 새롭게 Promise를 만들어보면서 공부한 내용을 한 번 정리해 보았습니다.

부족한 점을 알려주시는 댓글은 언제나 환영입니다.

 

→(js)Promise만들기(2) - 구상

반응형

'WEB > JS' 카테고리의 다른 글

(js)Promise만들기(3) - CustomEvent, EventTarget  (0) 2024.01.28
(js)Promise만들기(2) - 구상  (0) 2024.01.27
(js)마지막 달, 마지막 날 구하기  (0) 2021.11.05
(js)만 나이 계산  (0) 2021.11.05
(js) class 상속 연습  (0) 2021.08.22