sharingStorage

모던 자바스크립트 46장 제너레이터와 async / await 본문

Front-End/모던 자바스크립트 Deep Dive

모던 자바스크립트 46장 제너레이터와 async / await

Anstrengung 2022. 11. 18. 16:38

46.1 제너레이터란? 

ES6에서 도입된 제너레이터는 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수다.

 

제너레이터와 일반 함수의 차이점은 다음과 같다.

 

1. 제너레이터 함수는 함수 실행을 함수 호출자가 제어할 수 있다.

다시 말해, 함수 호출자가 함수 실행을 일시 중지시키거나 재개시킬 수 있다. 이는 함수의 제어권을 함수가 독점하는 것이 아니라 함수 호출자에게 양도할 수 있다는 것을 의미한다.

 

2. 제너레이터 함수는 함수 호출자와 함수의 상태를 주고받을 수 있다.

일반 함수는 매개변수를 통해 함수 외부에서 값을 주입하고 반환받는다. 즉 함수가 실행되고 있는 동안에는 함수의 상태를 변경할 수 없다.

제너레이터 함수는 함수 호출자에게 상태를 전달할 수 있고 함수 호출자로부터 상태를 전달받을 수도 있다.

 

3. 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.

일반 함수를 호출하면 함수 코드를 일괄 실행하고 값을 반환한다. 제너레이터 함수를 호출하면 함수 코드를 실행하는 것이 아니라 이터러블이면서 동시에 이터레이터인 제너레이터 객체를 반환한다. 

 

 

46.2 제너레이터 함수의 정의

제너레이터 함수는 fuction 키워드로 선언한다. 그리고 하나 이상의 yield 표현식을 포함한다. 이것을 제외하곤 일반 함수를 정의하는 방법과 같다.

 

- 애스트리스크(*)의 위치는 function 키워드와 함수 이름 사이라면 어디든지 상관없다. 

일관성을위해 function 키워드 바로 뒤에 위치시키는것을 권장한다.

- 제너레이터 함수는 화살표 함수로 정의할 수 없다.

- 제너레이터 함수는 new 연산자와 함께 생성자 함수로 호출할 수 없다.

 

 

46.3 제너레이터 객체

제너레이터 함수를 호출하면 일반 함수처럼 함수 코드 블록을 실행하는 것이 아니라 제너레이터 객체를 생성해 반환한다. 

제너레이터 객체는 이터러블 이면서 동시에 이터레이터다.

다시 말해, 제너레이터 객체는 iterator 메서드를 상속받는 이터러블이면서 value, done 프로퍼티를 갖는 이터레이터 result 객체를 반환하는 next 메서드를 소유하는 이터레이터다. 

 

 

제너레이터 객체는 next 메서드를 갖는 이터레이터지만 이터레이터에는 없는 return, throw 메서드를 갖는다. 제너레이터 객체의 세 개의 메서드를 호출하면 다음과 같이 동작한다. 

  • next 메서드를 호출하면 제너레이터 함수의 yield 표현식까지 코드블록을 실행하고 yield된 값을 value 프로퍼티 값으로, false를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다. 
  • return 메서드를 호출하면 인수로 전달받은 값을 value프로퍼티 값으로, true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다.

  • throw 메서드를 호출하면 인수로 전달받은 에러를 발생시키고 undefined를 value 프로퍼티 값으로, true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환한다. 

 

 

46.4 제너레이터의 일시 중지와 재개

제너레이터는 yield 키워드와 next 메서드를 통해 실행을 일시 중지했다가 필요한 시점에 다시 재개할 수 있다. 제너레이터가 함수 호출자에게 제어권을 양도하기 때문이다. 

제너레이터 함수를 호출하면 제너레이터 객체를 반환하고 제너레이터 객체는 next 메서드를 갖는다.  제너레이터 객체의 next 메서드를 호출하면 제너레이터 함수의 코드 블록을 실행한다.

단, 일반 함수처럼 한 번에 모든 코드를 일괄 실행하는 것이 아니라 yield 표현식 까지만 실행한다. 

yield 키워드는 제너레이터 함수의 실행을 일시 중지시키거나 yield 키워드 뒤에 오는 표현식의 평가 결과를 제너레이터 함수 호출자에게 반환한다.

next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티에는 yield된 값(yield 키워드의 뒤의 값)이 할당되고 done 프로퍼티에는 제너레이터 함수가 끝까지 실행되었는지를 나타내는 불리언 값이 할당된다.

generator.next() -> yield -> generator.next() -> yield -> generator.next() -> yield -> ... ->generator.next() -> return 

이터레이터의 next 메서드와 달리 제너레이터 객체의 next 메서드에는 인수를 전달할 수 있다. 제너레이터 객체의 next 메서드에 전달한 인수는 제너레이터 함수의 yield 표현식을 할당받는 변수에 해당한다. 

 

 

46.5 제너레이터의 활용

46.5.1 이터러블의 구현

제너레이터 함수를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간단히 이터러블을 구현할 수 있다.

 

무한 이터러블을 생성하는 함수

 

무한 이터러블을 생성하는 제너레이터 함수

 

 

46.5.2 비동기처리

제너레이터 함수는 next 메서드와 yield 표현식을 통해 함수 호출자와 함수 상태를 주고받을 수 있다는 특성을 사용하여 프로미스를 사용한 비동기 처리를 동기처럼 구현할 수 있다.

 

다시말해, 프로미스의 후속 처리 메서드 then / catch / finally 없이 비동기 처리 결과를 반환하도록 구현할 수 있다.

이렇게 구현하는 과정에서

require() of es modules is not supported. 

error [err_require_esm]: must use import to load es module

위 에러들이 떴다. 

 

사전 지식

fetch에 대해

우선 fetch()는 web이기때문에 웹 브라우저 환경이 아닌 Node.js 경우에는 해당 api를 지원하지 않는다.

따라서 fetch를 사용하기 위해서는 node-fetch 패키지를 npm 패키지 매니저로 install 하고 최상단에서 require하거나 import해야한다. 

 

ES module과 CommonJS

두개의 키워드 모두 하나의 파일에서 다른 파일의 코드를 불러온다는 동일한 목적을 가지고 있습니다.

 

ES module 

const fetch=require('node-fetch');

브라우저 JS환경에서 JS 모듈은 import, export 에 따라 ECMA Script  module를 가져오기, 내보내기를 한다.

ES module 형태는 재사용을 위한 JS 코드 패키징의 공식 표준 형식이고 대부분의 최신 웹 브라우저는 기본적으로 모듈을 지원한다. ES6 module은 import와 export 키워드를 사용하여 모듈을 불러오고 내보낸다.

시스템이 조금 더 최신 스펙이다 보니 CommonJS 대비 여러가지 이점이 있는데 import, from, export, default 처럼 모듈 관리 전용 키워드를 사용하기 때문에 가독성이 좋다. 또한 비동기 방식으로 작동하고 모듈에서 실제로 쓰이는 부분만 불러오기 때문에 성능과 메모리 부분에서 유리한 측면이 있다. 뿐만 아니라 CommonJS에서는 지원하지 않는 Named Parameter와 같은 기능들이 존재합니다.

 

CommonJS 

import fetch from "node-fetch";

많은 프로젝트에서 ES6모듈 시스템을 점점 더 널리 사용하는 추세이지만 항상 import 키워드를 사용해서 코딩할 수 있는 것은 아니다. <script> 태그를 사용하는 브라우저 환경에서는 물론이고 NodeJS에서도 CommonJS를 기본 모듈 시스템으로 채택하고 있기 때문에 Babel 같은 ES6코드를 변환해주는 도구를 사용할 수 없는 상황에서는 싫든 좋은 require 키워드를 사용해야합나다. 

 

commonJS모듈은 require()을 사용해서 가져오고 변수와 함수를 내보낼때는 module.exports와 exports를 사용한다.

여러 개의 객체를 내보낼 경우, exports 변수의 속성으로 할당하고 딱 하나의 객체를 내보낼 경우, module.exports 변수 자체에 할당한다.

출처: https://www.daleseo.com/js-module-import/

 

 

에러가 발생한 이유

파일이름.js file is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.

( (파일이름).js 파일은 가장 가까운 부모 package.json파일이  scope의 모든 .js 파일을 ES 모듈로 정의하는
"type" :"module"을 포함하는 파일이므로 ES 모듈 파일입니다.)

즉 내 파일은 ES module 파일인데 commonJS에서 사용하는 require을 사용하여 오류가 난 것이다.  내 프로젝트는 package.config파일이 없고 import를 통한 경로설정보다 더 간단한 방법을 찾기위해 알아본 결과 아래와 같은 방법으로 해결했다.

 

에러 해결

1. node-fetch 삭제 후 v2로 재다운로드

npm uninstall node-fetch
npm install node-fetch@2

나는 위 방법으로 해결되었지만 node-fetch의 v2버전의 환경에 대한 조사가 이루어지지 않아 혹시 프로젝트에서 사용할 예정이라면 조금 더 공부한 후에 사용하길 권장한다.

 

2. npm audit fix 명령어 사용

문제를 해결하려면 다음을 실행해야한다면서 npm audix fix를 요구했다. 

npm audix은 dependency  tree의 보안 취약점과 해결방안을 제공해준다. 또 npm audit fix 명령어를 사용하면 npm audit으로 나온 결과를 자동으로 고쳐준다. 

 

 

46.6 async / await 

위 예시를 통해 제너레이터를 사용해서 비동기 처리를 동기처럼 구현했지만 코드가 무척이나 장황해지고 가독성도 나빠졌다. ES8에서는 제너레이터보다 간단하고 가독성 좋게 비동기 처리를 동기 처리처럼 동작하고 구현할 수 있는 async/await 가 도입되었다.

 

async / await 는 프로미스를 기반으로 동작한다. 이는 then / catch / finally 후속 처리 메서드에 콜백 함수를 전달해서 비동기 처리 결과를 후속 처리할 필요없이 마치 동기 처리처럼 프로미스를 사용할 수 있다. 

다시말해, 프로미스의 후속 처리 메서드없이 마치 동기 처리처럼 프로미스가 처리 결과를 반환하도록 구현할 수 있다.

 

46.6.1 async 함수

await 키워드는 반드시 async 함수 내부에서 사용해야한다. async 함수는 async 키워드를 사용해 정의하며 언제나 프로미스를 반환한다. async 함수가 명시적으로 프로미스를 반환하지 않더라도 async 함수는 암묵적으로 반환값을 resolved 하는 프로미스를 반환한다.

※ 클래스의 constructor 메서드는 async 메서드가 될 수 없다. constructor 메서드는 인스턴스를 반환해야하고 async함수는 언제나 프로미스를 반환해야되기 때문이다.

 

 

 

46.6.2 await 키워드

await 키워드는 프로미스가 settled (비동기 처리가 수행된 상태)가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve한 처리 결과를 반환한다. await 키워드는 반드시 프로미스 앞에서 사용해야한다.

await 키워드는 다음 실행을 일시 중지시켰다가 프로미스가 settled 상태가 되면 다시 재개한다.

foo 함수가 수행하는 3개의 비동기처리는 서로 연관이 없어 개별적으로 수행하는 비동기 처리이므로 앞선 비동기 처리가 완료될 때까지 대기해서 순차적으로 처리할 필요가 없다. 따라서 foo 함수는 다음과 같이 처리하는 것이 좋다.

 

다음의 bar 함수는 앞선 비동기 처리의 결과를 가지고 다음 비동기 처리를 수행해야한다. 따라서 비동기 처리의 처리 순서가 보장되어야 하므로 모든 프로미스에 await 키워드를 써서 순차적으로 처리할 수 밖에 없다.

 

 

46.6.3 에러처리

비동기 처리를 위한 콜백 패턴의 단점 중 가장 심각한 것은 에러처리가 곤란하다는 것이다. 

에러는 호출자 방향으로 전파된다. 즉 콜 스택의 아래 방향(먼저 푸시된 싱행 컨텍스트 방향)으로 전파된다. 하지만 비동기 함수의 콜백 함수를 호출한 것은 비동기 함수가 아니기 때문에 try ... catch 구문을 사용해 에러를 캐치할 수 없다.

 

async / await에서 에러처리는 try ... catch 구문을 사용할 수 있다. 콜백 함수를 인수로 전달받는 비동기 함수와는 달리 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확하다.

async 함수 내에서 catch 문을 사용해서 에러 처리를 하지 않으면 async 함수는 발생한 에러를 reject하는 프로미스를 반환한다. 따라서 async 함수를 호출하고 Promise.prototype.catch 후속 처리 메서드를 사용해 에러를 캐치할 수 있다.

Comments