on
비동기 자바스크립트의 사가: 발전기
비동기 자바스크립트의 사가: 발전기
반응형
도입부
현대의 자바스크립트 프로그램에서 가장 복잡한 것 중 하나는 비동기성이다. 콜백, 툰크 및 약속과 같은 기존 패턴 몇 가지를 이미 살펴보았습니다. 몇 가지 핵심 문제를 해결하긴 했지만, 이 모든 패턴에는 한가지 중요한 공통점이 있습니다. 동기식 코드처럼 보이지 않습니다. 비동기 코드에 대한 작성 방법과 이유 사이에는 항상 차이가 있습니다. 이것은 비현실적인 희망처럼 들릴지 모르지만, 시간은 우리가 그것에 정말 가까이 갈 수 있다는 것을 증명했다.
우리가 배울 것
오늘 기사에서는 발전기에 대해 이야기하겠습니다. ES6에 도입된 새로운 유형의 기능입니다. 우선, 우리가 살펴본 바와 같이, 그것이 비동기 프로그래밍과 어떤 관계가 있는지 바로 알 수는 없을 것이다. 여러분 대부분에게는 이상하게 보일 것입니다. 하지만 천천히 설명과 예제를 거치면서, 결국 코드에 왜 그것들이 필요한지 완전히 이해할 수 있는 지점에 도달할 것입니다. 여러분은 발전기가 정말로 두드러지게 만드는 것이 무엇이고 그들이 우리를 위해 어떤 문제들을 해결하는지 알게 될 것입니다. 마지막으로, 안심하고 제네레이터에 대해 명확히 말할 수 있고 코드에서 제네레이터의 사용을 정당화할 수 있기를 바랍니다.
실행-완료 의미론
자바스크립트의 모든 일반 함수는 공통적으로 주목할 만한 기능을 가지고 있다. 동기 코드를 작성할 때, 우리는 우리의 함수가 실행을 시작할 때 항상 다른 함수가 실행될 기회를 얻기 전에 그것이 끝까지 실행되고 끝날 것이라는 것을 알고 있다. 주어진 초에는 오직 하나의 함수만 능동적으로 실행할 수 있다. 이것은 또한 어떤 것도 다른 것을 실행하기 위해 우리의 기능을 선제적으로 방해할 수 없다는 것을 의미한다. 위에서 말한 모든 것을 완벽하게 설명하는 학술 용어는 실행 완료 의미론이다. 이것이 우리가 두 가지 기능이 서로를 방해하거나 공유 메모리를 손상시키는 것에 대해 걱정하지 않도록 도와주는 것이다. 자바스크립트에 이 "규칙"을 적용함으로써 우리는 순수한 단일 스레드 방식으로 우리의 코드에 대해 추론할 수 있다.
발전기는 그렇지 않다.
발전기는 매우 다른 종류의 것이다. 그들은 이 완성까지의 규칙을 전혀 충족하지 못한다. 표면적으로는, 우리의 코드에 상당한 혼란을 가져왔어야 했다. 하지만 그들은 우리의 문제를 해결할 수 있는 또 다른 방법을 제공하는 것으로 보인다. 비록 그 방법 자체가 조금 이상하게 보일지 모르지만 말이다. 생성자를 설명하는 한 가지 방법은 현재 자바스크립트에서 상태 기계, 즉 이러한 전환을 선언적으로 나열할 수 있는 한 상태에서 다른 상태로의 흐름을 정의할 수 있게 해준다고 말하는 것이다. 저는 여러분 대부분이 꽤 많은 상태 기계를 만들었을 것이고 여러분은 그것이 이렇게 불리는지도 모를 것이라고 확신합니다. 이전에는 자바스크립트에서 사용 가능한 도구를 사용하여 상태 머신을 구현하는 데 많은 노력과 시간이 소요되었습니다. 우리는 이러한 모든 전환을 만드는 함수에서 현재와 이전 상태를 유지하기 위해 폐쇄를 종종 사용했지만, 코드는 점점 복잡해지고, 쓰는 것도 시간이 많이 걸렸습니다. 발전기들은 여러분이 같은 문제를 훨씬 쉽고 분명한 방법으로 해결하도록 해주는 통사당을 첨가하고 있습니다. 비동기 코드에 어떤 도움이 될까요? 그곳에 도달하기 위해서는 우선 발전기의 내부 배관을 잘 파악해야 한다.
항복으로 멈춤
생성기는 항복이라는 새로운 키워드를 도입하고 일시 중지 버튼과 같은 역할을 합니다. 따라서 생성기 함수가 실행 중일 때 항복 키워드와 마주치게 되면 흥미로운 동작을 보여줄 수 있습니다. 이 수익률이 어디서 발생하는지는 중요하지 않다. 식 중간에 짝수일 수도 있지만 생성기가 일시 중지됩니다. 이 시점부터는 제너레이터 자체에서 아무 일도 일어나지 않으며 완전히 차단된 상태로 유지됩니다. 말 그대로 얼어버립니다. 중요한 부분은 전체 프로그램 자체가 차단되지 않고 계속 실행될 수 있다는 것이다. 수율에 의한 블록은 완전히 국소화되어 있습니다. 그리고 그것은 누군가가 와서 계속 달리라고 말할 때까지 무한정 이 "일시 중지된" 상태에 있을 수 있습니다. 생성기는 내부 상태를 잃지 않고 필요한 횟수만큼 일시 중지 및 재개할 수 있는 함수라고 생각할 수 있습니다.
한 가지
이제 이러한 모든 개념이 어떻게 통합되는지 확인하기 위해 제너레이터의 예를 살펴봐야 합니다. 첫 번째 발전기는 다음과 같습니다.
function* helloWorldGenerator() { console.log('Hello world'); yield; // pausing console.log('Hello again!') }
1행에서 별표 기호는 우리가 정의하는 함수가 실제로 생성자라는 것을 자바스크립트에 알려준다. 3번 라인에 일시정지 버튼인 수익률 키워드가 있다는 것을 알 수 있습니다. 생성자는 수율을 사용하여 언제, 어디서, 어떤 방식으로 일시 중지할지 선언합니다. 이것은 협동 멀티태스킹이라고도 불린다. 바깥에 있는 누구도 들어와서 그것의 실행을 방해할 수 없다. 이것이 다중 스레드 언어에서 종종 대재앙을 일으키는 원인입니다. 다행히도, 우리에게는 그것들이 없습니다.
생성기 호출
제너레이터를 호출하면 다른 기능과 약간 다르게 작동합니다. 위의 예제를 계속하면서 이 발전기를 사용하는 방법을 설명하겠습니다.
const iterator = helloWorldGenerator(); iterator.next() // Hello world iterator.next() // Hello again!
생성기 함수를 호출하면 생성기 자체 내에서 코드가 실행되지 않습니다. 생성기를 실행하면 실제로 코드가 실행되지 않습니다. 실제로 일어나고 있는 것은 우리가 반복자를 얻고 있다는 것이다. 여러분은 아마 반복자가 무엇인지 알고 있을 것입니다. 하지만 만약을 위해 그들의 정의를 상기해 봅시다. 반복기는 한 번에 한 결과씩 데이터 집합을 통과하는 방법입니다. 이 경우 반복자의 목적은 항목의 모음을 통해 단계를 수행하는 것이 아니라 문자 그대로 이러한 생산량 진술을 통해 외부로부터 생성자를 제어하는 것입니다. 생성기의 흐름을 제어하는 데 도움이 되는 편리한 API라고 생각해 보십시오. 생성기를 일시 중지할 수는 없지만 반복기를 사용하여 생성기가 스스로 일시 중지되기를 원할 때까지 실행하도록 요청할 수 있습니다. 따라서 1호선에서는 코드가 실행되지 않지만 2호선에서는 반복기 객체의 .next를 호출하여 생성기의 실행을 시작합니다. 그런 다음 console.log( Hello world ) 문을 실행하고 항복 시 스스로 일시 중지한 후 클라이언트 코드로 제어를 되돌립니다. .next에 대한 다음 호출이 발생할 때마다 제너레이터를 다시 시작하고 마지막 console.log( Hello again! ) 문을 실행하면 제너레이터가 완료됩니다.
가치 양보
생성자는 코드에 대한 제어권을 양보할 뿐만 아니라 값도 산출할 수 있는 것으로 보인다. 앞의 예에서는 아무것도 산출하지 못했습니다. 이 점을 보여줄 더미 예제를 생각해 봅시다.
function* authorDossierGenerator () { const author = { name: "Roman", surname: "Sarder", age: 23, } yield author.name; yield author.surname; yield author.age; } const iterator = authorDossierGenerator(); iterator.next() // { value: "Roman", done: false } iterator.next() // { value: "Sarder", done: false } iterator.next() // { value 23, done: false } iterator.next() // { value: undefined, done: true }
마지막 예제에서는 생성기에서 정의되지 않은 값이 나온다고 가정했지만, 이제 실제 값을 반환하고 있습니다. 다음 각 .next call은 가치 있는 객체와 완료된 속성을 제공합니다. 이 값은 생성기에서 산출되는 값과 일치합니다. 이 경우 개체 속성 값 집합입니다. 완료 플래그는 생성기의 완료 여부를 나타냅니다. 처음에는 이것이 까다로울 수 있다. 세 번째 반복기.다음 통화는 시각적으로 제너레이터가 이미 완료된 것처럼 보일 수 있지만 실제로는 그렇지 않습니다. 생성기의 마지막 행이지만 실제로 발생하는 것은 생성기가 항복 작성자인 마지막 식에서 일시 중지되는 것입니다. 일시 중지된 경우 다시 시작할 수 있습니다. 따라서 다음 네 번째 .false가 완료되는 것입니다. 하지만 정의되지 않은 마지막 값은 어떨까요? 단순함수와 마찬가지로 생성기 끝에 반환문이 없는 경우, 자바스크립트는 정의되지 않은 반환을 가정한다. 언제든지 생성기에서 돌아올 수 있으며 생성기는 즉시 자체 완성될 뿐만 아니라 값이 있는 경우 값을 반환합니다. 반환을 "종료" 버튼으로 생각해 보십시오.
값 통과
우리는 생성자가 클라이언트의 코드로 메시지를 전달하는 방법이 실제로 있다는 것을 설명하는데 성공했다. 하지만 우리는 메시지를 생성할 수 있을 뿐만 아니라 .next 메소드를 호출할 때 메시지를 전달할 수 있고 그 메시지는 생성기로 바로 들어갑니다.
function* sumIncrementedNumbers () { const x = 1 + (yield); const y = 1 + (yield); yield x + y } const iterator = sumIncrementedNumbers(); iterator.next() // { value: undefined, done: false } iterator.next(5) // { value: undefined, done: false } iterator.next(2) // { value: 9, done: false } iterator.next() // { value: undefined, done: true }
두 식 중간에 항복 키워드를 넣었습니다. 내부적인 관점에서, 이러한 수익률을 물음표로 생각하십시오. 생성자가 첫 번째 식에 도달하면 기본적으로 다음과 같은 질문을 합니다. 어떤 값이 여기에 들어가야 합니까? 답이 없으면 식을 완성할 수 없습니다. 이 때, 이 값은 자동으로 일시 중지되고 누군가가 이 값을 제공할 때까지 기다립니다. 다음 번호로 전화를 걸어 5를 전달하면 됩니다. 이제 다음 수확으로 진행할 수 있습니다. 이러한 수율은 생성기로 전달되는 특정 시점에 값을 자리 표시자로 사용하고 수율을 대체하여 식을 완성합니다.
비동기식으로 변환
지금 당장, 여러분은 다음 예를 볼 준비가 되어 있어야 하고 여러분의 머리를 완전히 부풀리지 말아야 합니다. 우리는 발전기를 사용하여 비동기 코드로 작업하고 이전의 예들 중 하나를 변환하려고 한다. 게양 때문에 좀 끔찍해 보일 수도 있지만 개념의 증명이라고 생각해라. 우리는 분명 훨씬 더 멋져 보이는 것을 다시 만들 것이다.
function getData (number) { setTimeout(() => { iterator.next(number); }, 1000) } function* sumIncrementedNumbersAsync() { const x = 1 + (yield getData(10)); const y = 1 + (yield getData(20)) console.log(x + y) // 32 } const iterator = sumIncrementedNumbersAsync(); iterator.next();
휴, 아직 거기 있어요? 무슨 일이 일어나고 있는지 알기 위해 각 코드 라인을 살펴봅시다. 우선, 우리는 반복기를 생산하기 위해 우리의 발전기를 호출하고 다음.을 호출함으로써 실행을 시작한다. 지금까지는, 로켓 과학은 진화하지 않았다. 우리 발전기는 x의 값을 계산하기 시작하고 첫 번째 수율에 직면한다. 이제 제너레이터가 일시 중지되고 다음 질문을 합니다. 여기에는 어떤 값이 들어가야 합니까? 답은 getData(10) 함수 호출의 결과에 있습니다. 여기 흥미로운 부분이 있습니다. 가짜 비동기 함수인 집에서 만든 getData 함수가 값 계산을 마치면 생성기를 다시 시작합니다. 여기서는 시간 초과만 설정되지만 어떤 것이든 될 수 있습니다. 따라서 1000밀리초 후에 가짜 getData가 응답을 제공하고 응답 값을 가진 생성기를 다시 시작합니다. 다음 산출 getData(20)도 비슷한 방식으로 처리됩니다. 우리가 여기서 얻는 것은 동기적으로 보이는 비동기 코드입니다. 이제 비동기 값이 동기 값과 정확히 동일한 방식으로 계산될 때 제너레이터를 일시 중지했다가 다시 시작할 수 있습니다. 정말 큰일이네요.
마법의 열쇠
제너레이터는 이러한 일시 중지/재개 기능을 사용하기 때문에 자신을 차단하고 백그라운드 프로세스가 완료될 때까지 기다렸다가 기다리던 값으로 다시 시작할 수 있습니다. 구현 세부 정보는 대부분 라이브러리에 숨겨져 있으므로 구현 세부 정보에서 자신을 추상화하십시오. 중요한 것은 발전기 내부의 코드입니다. 약속을 사용하는 코드에서 본 것과 비교해 보십시오. Promise의 흐름 제어는 콜백을 체인으로 수직으로 구성합니다. 콜백과 툰크는 동일한 콜백을 내포하고 있습니다. 발전기 또한 그들만의 흐름 제어를 가져옵니다. 하지만 이 흐름 제어의 매우 특별한 특징은 완벽하게 동기적으로 보인다는 것입니다. 비동기 코드와 동기화 코드가 동일한 조건으로 나란히 있습니다. 이제 비동기 코드를 다른 방식으로 구성할 필요가 없습니다. 이제 비동기성 자체는 우리가 신경 쓰지 않는 구현 세부 사항입니다. 이는 발전기가 상태 기계의 복잡성을 감추기 위한 통사적 방법을 도입했기 때문에 가능한 일이다. 우리의 경우, 비동기 상태 기계의 복잡성을 감추기 위해서이다. 또한 오류 처리와 같은 동기식 코드의 모든 이점을 누릴 수 있습니다. 같은 방법으로 트라이캐치 블록을 사용하여 비동기 코드의 오류를 처리할 수 있습니다. 아름답지 않나요?
IOC 숙청
이 예제를 좀 더 주의 깊게 살펴보면 이 접근 방식에 한 가지 문제가 있다는 것을 알 수 있습니다. getData 기능은 Inversion Of Control로 이어지는 생성기 실행을 제어하고 있습니다. 이 함수는 예기치 않은 방법으로 생성기에서 .next 메서드를 호출하여 모든 것을 엉망으로 만들며 현재 코드베이스에는 이에 대한 솔루션이 없습니다. 그거 알아? 우리는 더 이상 이전에 무시무시한 이 문제를 두려워하지 않는다. 우리는 단지 어떤 패턴이 우리에게 이 문제를 이미 해결했는지 기억할 필요가 있다. 우리는 발전기와 함께 약속을 혼합할 것입니다! 그리고 이 결합이 이루어지기 위해서는, 정의되지 않은 것을 양보하는 대신, 우리는 프롬시를 양보해야 합니다.
궁극의 듀오
우리가 어떻게 이 일을 해낼 수 있을지 상상해 봅시다. 우리는 이미 우리 발전기 내부에서 약속을 해야 한다고 말했다. 하지만 누가 그 약속을 해결할 수 있을까요? 그것은 발전기를 구동하는 코드에 의해 수행되고, 다음에 호출됩니다. 그리고 일단 그것이 그것에 뭔가를 해야 한다는 약속을 얻으면, 그것은 해결해서 발전기를 재개하겠다는 약속을 기다려야 할 것이다. 우리는 우리를 위해 그것을 해줄 추가적인 추상화가 필요하고 이것은 프레임워크, 라이브러리 또는 자바스크립트 자체에 의해 제공될 가능성이 높다. 문란한 발전기로 작업하고 싶을 때마다 수레바퀴를 재창조하는 것은 현실적으로 어려울 것 같습니다. 하지만 교육적인 목적으로, 우리는 스스로 하나를 알아내서 연구할 것입니다.
약속 생성기 실행기 구축
나는 당신에게 그러한 발전기 런너의 구현을 제공할 것이다. 물론, 적절한 처리와 같이 프로덕션에서 사용할 때 절대적으로 필요한 기능 중 일부는 없지만, 우리의 요구를 충족시키고 개념을 완벽하게 보여주면서 사물을 단순하게 유지합니다.
function runner (generatorFunction) { const iterator = generatorFunction(); function nextStep(resolvedValue) { const { value: nextIteratorValue, done } = iterator.next(resolvedValue); if (done) return nextIteratorValue; return nextIteratorValue.then(nextStep) } return Promise.resolve().then(nextStep) }
우리 주자는 평소처럼 발전기 기능을 취하여 반복기를 생산한다. 그런 다음 해결된 약속을 반환하고 .그 다음 방법으로 작업자 기능을 다음 단계로 전달합니다. 다음 반복기 값을 가져오고 제너레이터가 완료되었는지 확인하는 작업을 수행합니다. 그렇지 않은 경우, 우리는 .다음 통화의 결과가 약속이었다고 가정하고 있습니다. 따라서 반복자 가치 약속이 해결되기를 기다렸다가 그 가치를 작업 기능에 전달함으로써 우리 스스로 새로운 약속을 반환하고 있습니다. 작업자는 결과 값이 필요할 경우 반복자에게 전달하여 생성기가 완료될 때까지 작업을 반복하는 작업을 수행합니다. 별로 복잡할 것 없어요.
Generator Runner와 함께 작업
sumIncrementedNumbers 예제를 추가로 수정하여 새로운 런너를 통합하고 우리가 어떻게 문란한 발전기를 사용하는지 살펴보려고 합니다.
function getData (data) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(data); }, 1000) }) } function* sumIncrementedNumbersAsync () { const x = 1 + (yield getData(10)); const y = 1 + (yield getData(20)); return x + y; } runner(sumIncrementedNumbersAsync).then(value => { console.log(value) // After ~2000ms prints 32 });
여기 있는 모든 것이 이미 익숙해져야 합니다. 우리의 주자가 결국 약속으로 귀결되기 때문에 외부 세계의 관점에서 우리의 포장된 발전기는 단지 또 다른 약속에 지나지 않습니다. 우리는 비동기 코드를 동기식처럼 보이게 하기 위해 발전기를 사용하여 비로컬 비순차 추론 문제를 해결했다. 우리는 컨트롤의 반전 문제를 해결하는 더러운 일을 하겠다는 약속을 가져왔고, 우리의 단순한 프로미스 생성기 주자를 만들었습니다. 마지막으로, 우리는 결과적으로 약속의 깨끗한 인터페이스를 얻게 되었고 약속의 모든 혜택은 포장된 발전기에 적용됩니다. 그것이 발전기가 그렇게 강력한 이유입니다. 비동기 코드 작성 방법을 완전히 변경합니다. 그것들은 마침내 당신에게 우리의 두뇌에 직관적이고 우리가 생각하는 방식과 모순되지 않는 코드를 쓸 수 있는 능력을 제공한다.
비동기/대기?
실제로 이 패턴은 매우 유용하다는 것이 입증되어 2017년 ECMAScript는 비동기/대기 키워드를 도입하여 자체 비동기 생성기 구현을 구현했습니다. 이 기능은 완전히 생성자 기반이며 개념이 동일하므로 속이지 마십시오. 다른 점은 이제 해당 언어에서 적절한 구문을 지원하는 일류 시민이며 더 이상 이 작업을 수행하기 위해 도우미 라이브러리를 사용할 필요가 없습니다. 그러나 현재 비동기/대기 작동 방식에 대한 몇 가지 주의사항이 있습니다.
순수 발전기 대 비동기/대기
비동기 함수를 취소하고 추가 실행을 중지하려면 어떻게 해야 합니까? 문제는 그렇게 할 방법이 없다는 것이다. 현재 비동기/대기 중 약속만 반환합니다. 멋지긴 하지만, 취소하는 능력은 너무 중요해서 무시할 수 없어요. 그리고 현재의 구현으로는 실행을 보다 세밀하게 제어할 수 있는 툴이 충분하지 않습니다. 나는 그들의 설계 결정을 판단하는 사람이 아니지만, 내 요점은 API가 더 향상되어 예를 들어 약속과 취소 기능을 모두 반환할 수 있다는 것이다. 결국에는 풀 인터페이스를 구현하는 발전기와 함께 작업하고 있습니다. 우리는 반복자를 소비하는 방법을 통제하고 있다. 만약 취소 신호를 받는다면 어떻게 주자에게서 소비를 멈출 수 있는지 쉽게 상상할 수 있을 것이다. 요점을 증명하기 위해 우리는 아주 원시적인 취소 메커니즘을 구현하기 위해 간단한 변화를 도입할 수 있다. 그리고 여러분은 누군가가 롤백 전략과 함께 더 정교하고 오류가 없는 변형을 만드는 것을 상상할 수 있을 것입니다.
function runner (generatorFunction) { let isCancelled = false; const iterator = generatorFunction(); function nextStep(resolvedValue) { const { value: nextIteratorValue, done } = iterator.next(resolvedValue); if (done) return nextIteratorValue; if (isCancelled) { return Promise.resolve(); } return nextIteratorValue.then(nextStep) } return { cancel: () => isCancelled = true, promise: Promise.resolve().then(nextStep) }
이것은 위의 나의 요점을 잘 보여준다. 약속 및 취소 방법을 사용하여 개체를 반환하고 있습니다. 취소 방법은 폐쇄를 통해 포함된 플래그 변수를 토글하기만 합니다. 매우 깔끔하고 추가적인 개선을 위한 많은 가능성을 열어줍니다.
아웃로
이번에 배우고 토론해야 할 것들이 많았어요. 하지만 주제 자체는 쉬운 것이 아니며 여러분이 그것을 이해하기 위해 단지 5분 동안만 읽도록 내버려두지 않는다. 여러분 중 누구도 이 기사를 완성한다고 해서 발전기 전문가가 되리라고는 기대하지 않지만, 저는 여러분이 직접 이 주제를 더 탐구하도록 하는 좋은 출발을 했다고 확신합니다. 발전기를 사용하면 비동기 프로그래밍에 대한 질문에 일일이 답한 것 같습니다. Inversion of Control을 해결했고, 이제 동기적으로 보이는 비동기 코드를 작성할 수 있게 되었습니다. 이전 패턴의 가장 좋은 기능들을 조합한 것처럼 보입니다. 그러나 소프트웨어 엔지니어링에서 종종 발생하는 것처럼 동일한 문제에 대한 하나 이상의 가능한 해결책이 있는 경우가 많습니다. 이 시점부터 다음 패턴은 문제를 해결하는 다른 방법을 제공할 뿐이며 각 패턴은 귀하의 경우에 다소 적합할 수 있습니다. 최종 결정은 엔지니어로서 당신에게 달려 있습니다. 현재로서는 자바스크립트의 비동기 프로그래밍에 대해 우리 대부분에게 충분히 알 수 있기 때문에 당신이 시리즈의 이 시점에서 그만둔다면 완전히 문제가 없을 것이다. 하지만 저와 함께 하기로 결정하신다면, CSP나 천문학과 같은 발전된 패턴들을 살펴보도록 하겠습니다. 우리는 다음 번에 반드시 그들 중 한 사람에 대해 이야기 할 것입니다. 오랫동안 읽어주셔서 감사합니다!
크레딧
카일 심슨과 그의 재료 덕분입니다. 저는 특히 그의 비동기식 자바스크립트 강좌에서 영감을 얻었고, 그것은 제가 평상시보다 훨씬 더 열심히 이 주제에 대해 깊이 들어가도록 만들었습니다.
from http://it-ground.tistory.com/262 by ccl(A) rewrite - 2021-09-28 05:02:01