[기타] Promise { <pending> }

sarifor·2022년 5월 2일
2
post-custom-banner

문제

Node.js + Express 앱에서 웹페이지의 데이터를 긁어오는 getData 함수를 saveData 함수 내에서 실행하여, console.log()로 결과를 표시해 봤더니, 원하던 데이터는 나오지 않고 Promise { < pending > }만 나옴.
아래는 해당 코드의 편집본. (깃허브에서 오리지널 코드 보기)

export const getData = async (req, res) => {
 try {
   await accessURL();			
	let finalResult = [];			
	finalResult = result;			
	result = [];
   
   return finalResult;
 } catch(e) {
   console.log(e);
 };
};
				
export const saveData = (req, res) => {			
 const finalResult = getData();
 console.log(finalResult);
 
 return res.end();
};

환경

Node.js v14.19.1
npm 6.14.16

원인

async가 붙은 함수는, return 값이 Promise가 아니더라도 Promise화 해준다.
getData 함수에는 async가 붙었으므로, return 값(finalResult)을 Promise화하여 반환한다.
따라서 getData 함수를 써먹을 땐 await getData()가 되어야 하는 것이다.

1차 해결

getData 함수 실행 시 async/await 사용. 아래는 해당 코드.
(깃허브에서 오리지널 코드 보기)

export const saveData = (req, res) => {
  let data = [];
  
  (async () => {
   data = await getData();
     console.log(data);
  })();
  
  return res.end();
};			

2차 해결

이유

Promise, async/await 개념을 되짚어 본 후에 1차 해결 코드를 다시 살펴보니, 개선할 부분이 눈에 띄었다.

1차 수정

변경점

  • saveData 함수 안에 불필요하게 들어가 있는 즉시실행함수 삭제하고, async를 제일 바깥쪽(익명함수 앞)에 붙임. (깃허브에서 보기)

  • saveData 함수 안에 try/catch 에러 핸들링 추가

코드 (편집본)

  export const saveData = async (req, res) => {	

    let data = [];	

    try {	
      data = await getData();	
      (생략)	
      return res.render("home", { data: dataInDB });	
    } catch (e) {	
      console.log(e);	
    };	
  };

2차 수정

변경점

  • accessURL 함수 안에도 try/catch 에러 핸들링 추가. 이로써 프로젝트 내의 모든 await은 try/catch 처리됨. (깃허브에서 보기)

코드 (편집본)

const accessURL = async () => {
  try {	
    (생략)	
    const response = await client.get(`/page/${i}`);	
    (생략)	
  } catch (e) {	
    console.log(e);
  };	
};	

보충

위 이슈의 원인과 해결책을 찾다가 알게 된 지식에 대하여 정리해 보았다.
JavaScript 관련으로 비동기 처리, 함수 기본 개념, 호이스팅, 스코프, 클로저에 대해 조사하였고,
프로그램 실행 과정에 있어 일반적인 순서와 JavaScript에서의 순서를 알아보았다.

1. JavaScript 비동기 처리에서, 함수 실행 순서 보장하려면 ?

콜백 함수를 중첩하여 선언

  • 콜백 함수

    • 데이터가 준비된 시점에 동작 수행하는 함수
  • 콜백 지옥

    • 콜백 함수 중첩이 너무 심한 상태
    • e.g. 서버에서 데이터를 받아와 화면에 표시하기까지 인코딩 및 사용자 인증 등을 처리해야 하는 경우

개선책

  • 중첩하지 말고 각각의 함수로 구분
  • Promise 사용 (더 좋음)
  • async/await 사용 (더더 좋음)

2. Promise ?

기본

  • JavaScript 비동기 처리에 쓰이는 객체이다. 주로 서버에서 데이터를 가져올 때 사용한다.
    • e.g. 사용자 로그인 인증 로직

구조

  • 데이터 가져오는 함수를 Promise 객체로 감싸서,

  • 성공 시 (Fullfilled)

    • resolve() 실행되어, then(첫 번째 인자)로 결과 값을 받을 수 있고,
  • 실패 시 (Rejected)

    • reject() 실행되어, then(두 번째 인자)로 에러를 받을 수 있다.
      • catch()를 사용하는 것이 더 바람직하다.
  • 아직 처리 중일 시 (Pending)

예시

  • Promise 적용 전: Data를 받으면 callbackFunc()에 넘김

    function getData(callbackFunc) {
       $.get('url 주소/products/1', function(response) {
         callbackFunc(response);
       });
    }
    getData(function(tableData) {
       console.log(tableData);
    });
  • Promise 적용 후: Data를 받으면 resolve() 호출하여, then(첫 번째 인자)로 결과값을 받는다."

    function getData(callback) {
     return new Promise(function(resolve, reject) {
       $.get('url 주소/products/1', function(response) {
         resolve(response);
       });
     });
    }
    getData().then(function(tableData) {
     console.log(tableData);
    });

3. async/await ?

  • 기본(간략)

    • JavaScript의 비동기 처리 패턴 중 제일 최신 방법이다. Promise를 기반으로 한다.
    • 비동기 처리에 있어, Promise보다 더 좋은 이유 ?
      • 보통 코드를 위에서 아래로 읽는다. async/await를 쓰면 이를 그대로 유지할 수 있다.
      • 코드 길이도 더 짧고, 가독성이 좋다.
  • 구조(일반)

    • 바깥 함수에 async 붙이고, 안쪽의 비동기 처리 함수에 await 붙이고,

      • async ?
        • async가 붙은 함수는 반드시 Promise를 반환하고, Promise가 아닌 것은 Resolved Promise로 감싸 반환"
        • await을 사용하게 해준다.
      • await ?
        • Promise가 처리될(Settled) 때까지 기다리게 해준다.
        • 일반적으로 대상이 되는 비동기 처리 코드는, Promise를 반환하는 API 호출 함수이다.
          • e.g. Axios
        • Promise가 처리되길 기다리는 동안 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않는다.
    • 성공(이행) 시

      • Promise 객체의 result 값을 반환.
    • 실패(거부) 시

      • 예외가 생성됨(에러가 발생한 장소에서 throw error를 호출한 것과 동일함)
        • try/catch로 처리
      • try/catch 말고, async 함수에 .catch를 붙여 처리할 수도 있다.
        • 예시 (미검증)
          async function f() {
            let response = await fetch('http://유효하지-않은-url');
          }
          f().catch(alert); // TypeError: failed to fetch
  • 구조(예외)

    • 1) 최상위 레벨 코드에도 await 사용하려면 ?

      • 익명 async 함수로 코드 감싸기

        • 커밋 9ab4ae4에선 함수 안이었는데도 '익명 async 함수'를 썼었다. (잘못됨)

          • 익명 async 즉시 실행 함수라는 표현이 더 정확하지 않을까? (확인 필요)
        • 예시

          (async () => {
            let response = await fetch('/article/promise-chaining/user.json');
            let user = await response.json();
            (생략)
           })();
    • 2) 최상위 레벨 코드의 에러 핸들링을 하려면 ?

      • async 함수 바깥의 최상위 레벨 코드에선 await을 사용할 수 없다.

      • 때문에 .then/catch를 추가해 최종 결과나 처리되지 못한 에러를 다루는 것이 관행처럼 되어 있다.

      • 예시

        • (추후 보충)

4. 즉시 실행 함수 ?

  • 정의

    • 정의되자마자 즉시 실행되는 Javascript Function
  • 구성

    • 크게 두 부분으로 구성된다.

    • 첫 번째는 괄호((), Grouping Operator)로 둘러싸인 익명함수(Anonymous Function)이다. 이는 전역 스코프에 불필요한 변수를 추가해서 오염시키는 것을 방지할 수 있을 뿐 아니라 IIFE 내부안으로 다른 변수들이 접근하는 것을 막을 수 있는 방법이다.

      • 함수를 괄호로 감싸면 자바스크립트가 함수를 함수 선언문이 아닌 함수 표현식으로 인식하도록 속일 수 있다.

        • 함수 표현식은 이름이 없어도 괜찮고, 즉시 호출도 가능
    • 두 번째 부분은 맨 끝의 괄호()이다. 즉시 실행 함수를 생성한다. 이를 통해 자바스크립트 엔진은 함수를 즉시 해석해서 실행한다.

  • 예시

    (function () {
      var aName = "Barry";
     })();
  • 주의

    • 괄호로 둘러싸인 익명함수인 거지, 익명함수 그 자체는 아니다. (확인 필요)
  • 용도

    • 표현 내부의 변수는 외부로부터의 접근이 불가능하다.
      • var블록 스코프 갖게 함.

5. 익명 함수 ?

  • 정의

    • 말 그대로 함수명이 없는 함수.
    • 함수 표현식만 익명일 수 있다. (확인 필요)
  • 예시

     var square = function(number) { return number * number };
      var x = square(4)

6. 함수 관련 기본 개념

  • 함수 ?

    • JavaScript에서 기본적인 구성 블록 중의 하나.
    • 작업을 수행하거나 값을 계산하는 문장 집합 같은 자바스크립트 절차.
  • 함수 만드는 법 ?

    • 함수 선언

      • 함수가 독립된 구문 형태로 존재.
      • 함수 선언으로 생성된 함수는 Function 객체로, Function 객체의 모든 속성(property), 메서드 및 행위 특성(behavior)을 갖는다.
      • 함수 선언문은 코드 블록이 실행되기도 전에 처리된다. (호이스팅)
         function square(number) {
            return number * number;
          }
    • 함수 표현식

      • 함수가 표현식의 일부로 존재.

      • 함수 표현식은 실행 흐름이 표현식에 다다랐을 때 만들어진다.

      • 함수 표현식function 문(함수 선언) 사이의 주요 차이점은 함수 이름으로, 함수 표현식으로 익명 함수를 만들 경우 이름을 생략할 수 있다.

         var square = function(number) { return number * number };
          var x = square(4);
      • 함수 표현식은 함수 선언문을 사용하는 게 부적절할 때에 사용하는 것이 좋다.

        • 예시 (추후 보충)
      • 함수 표현식에서 함수의 이름을 지정하면, 함수 내에서 자신을 참조하는 데 사용되거나, 디버거 내 스택 추적에서 함수를 식별할 수 있다.

        • 예시 (추후 보충)
  • 여러 함수 형식

    • 일반 함수(이름 있는 함수)

      • '함수 선언'으로 만듦
      • 호이스팅 O
    • 익명 함수

      • 상기 참고
      • '함수 표현식'으로 만듦
      • 호이스팅 X
    • 즉시 실행 함수

      • 상기 참고
      • '함수 표현식'으로 만듦
      • 호이스팅 X
    • 화살표 함수

      • (생략)
    • 기타

      • (추후 보충)

7. 호이스팅 ?

  • 정의
    • 인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것.
    • 주로 '변수의 선언과 초기화를 분리한 후, 선언만 코드의 최상단으로 옮기는 것'으로 말하곤 하나, 정확히는 메모리 공간을 미리 할당하는 것이다.
  • 특징
    • 초기화를 먼저 하고 선언을 나중에 해도 정상 실행.
    • 변수/함수 모두에 적용.
  • 사용 목적
    • (추후 보충)
  • 주의점
    • let, const도 호이스팅이 되나, 참조 에러가 발생한다.
      • (추후 보충)

8. 변수 선언법별 스코프 ?

  • var

    • 함수 스코프
    • 런타임 이전에 선언/초기화 진행
      • 선언문은 실행컨텍스트에 등록 (추후 보충)
  • let

    • 블록 스코프
    • 런타임 이전에 선언, 런타임 이후에 초기화 진행 (확인 필요)
  • const

    • 블록 스코프
    • 런타임 이전에 선언, 런타임 이후에 초기화 진행 (확인 필요)
  • '런타임 이전에 선언/초기화' vs. '런타임 이전에 선언, 이후에 초기화' 비교

    • (추후 보충)

9. 클로저

  • 정의

    • (추후 보충)
  • 사용처

    • 함수 레벨 스코프로 인하여 for 루프의 초기화 식에 사용된 변수가 전역 스코프를 갖게 되어 발생하는 문제를 회피하는 데 사용
    • 함수 내 변수를 private화. (공개할 스코프 변경) (재확인 필요)
    • (추후 보충)

10. 프로그램 실행 과정

  • 일반적인 프로그래밍 언어의 경우

    • 주요 흐름

      • 코드를

      • 컴파일 하거나 인터프리팅 하여

        • 컴파일은 기계어로 바꿔주고, 인터프리팅은 Bytecode로 바꿔주는 것.
        • 컴파일 에러
          • e.g. Type 체크 에러
      • 실행 (런타임)

        • 런타임 ?
          • 프로그램 실행되는 환경 or 시간.
          • e.g. Browser 자체, 혹은 Browser에서 작동할 때
        • 런타임 에러
          • e.g. 메모리 부족 에러
    • 요약

      • 기계어 혹은 Bytecode로 바꾸고 실행
  • JavaScript V8 엔진의 경우 (JIT Compilation)

    • 주요 흐름

      • Overview

      • 코드를

      • Bytecode(중간 코드)로 바꾸고

        • By 인터프리터(Ignition)
      • 실행하고, (런타임)

      • 자주 사용되는 코드를 기계어로 바꿈 (컴파일, 최적화)

        • By 컴파일러(TurboFan)
    • 요약

      • JavaScript는 interpreter 언어다. 다만 JavaScript 엔진 내부에서 실행 중 컴파일이 필요한 경우에 내부에서 컴파일한다.

요약

문제/해결

  • Node.js + Express 앱에서 웹페이지의 데이터를 긁어오는 getData 함수가 Promise { < pending > }를 리턴하던 원인은, 해당 함수에 async가 붙어 있었기 때문이었다. 때문에 해당 함수를 쓸 때 await을 붙여줄 필요가 있다.

보충

  • JavaScript

    • async 붙은 함수는 늘 Promise를 리턴한다.
    • 비동기 처리에서 함수 실행 순서 보장하는 방법에는 콜백 함수 중첩 선언, Promise, async/await이 있고, 후자일수록 사용이 더 간편하다.
    • 함수는 함수 선언 혹은 함수 표현식으로 만든다. 함수 선언으로 만드는 경우에만 호이스팅이 된다.
    • 익명 함수즉시 실행 함수는 다른 것이다.
    • 호이스팅이란 정확히는 변수와 함수의 메모리 공간을 미리 할당하는 것이다.
    • var는 함수 스코프, let과 const는 블록 스코프이다.
    • 클로저함수 레벨 스코프로 인한 문제를 해결할 수 있다.
  • 프로그램 실행 과정

    • 일반적인 경우 컴파일런타임이나, JavaScript는 런타임컴파일이다.

참고

profile
잠수 탄 블로그 같지만 살아있어요
post-custom-banner

0개의 댓글