여기 예제에서는 브라우저 방법을 사용합니다.
콜백, 약속 및 기타 추상 개념의 사용을 보여주기 위해 몇 가지 브라우저 메서드, 특히 스크립트 로드 및 간단한 문서 조작 수행을 사용할 것입니다.
이러한 방법에 익숙하지 않고 예제에서의 사용법이 혼란스럽다면 튜토리얼의 다음 부분에서 몇 장을 읽어 보는 것이 좋습니다.
하지만 어쨌든 우리는 상황을 명확하게 하려고 노력할 것입니다. 브라우저 측면에서는 실제로 복잡한 것은 없습니다.
비동기 작업을 예약할 수 있는 많은 기능이 JavaScript 호스트 환경에서 제공됩니다. 즉, 지금 시작했지만 나중에 완료되는 작업입니다.
예를 들어, 그러한 함수 중 하나가 setTimeout
함수입니다.
비동기 작업의 다른 실제 사례도 있습니다(예: 스크립트 및 모듈 로드). (이에 대해서는 이후 장에서 다룰 예정입니다.)
주어진 src
사용하여 스크립트를 로드하는 함수 loadScript(src)
를 살펴보세요.
함수 로드스크립트(src) { // <script> 태그를 생성하고 페이지에 추가합니다. // 이는 주어진 src가 있는 스크립트가 로드를 시작하고 완료되면 실행되도록 합니다. let script = document.createElement('script'); script.src = src; document.head.append(스크립트); }
주어진 src
와 함께 동적으로 생성된 새로운 태그 <script src="…">
를 문서에 삽입합니다. 브라우저는 자동으로 로드를 시작하고 완료되면 실행됩니다.
이 기능을 다음과 같이 사용할 수 있습니다.
// 주어진 경로에서 스크립트를 로드하고 실행합니다. loadScript('/my/script.js');
스크립트는 지금 로드를 시작하므로 "비동기적으로" 실행되지만 나중에 함수가 이미 완료되면 실행됩니다.
loadScript(…)
아래에 코드가 있으면 스크립트 로딩이 완료될 때까지 기다리지 않습니다.
loadScript('/my/script.js'); // loadScript 아래 코드 // 스크립트 로딩이 완료될 때까지 기다리지 않습니다. // ...
새 스크립트가 로드되자마자 사용해야 한다고 가정해 보겠습니다. 새로운 함수를 선언하고 우리는 이를 실행하려고 합니다.
하지만 loadScript(…)
호출 직후에 그렇게 하면 작동하지 않습니다.
loadScript('/my/script.js'); // 스크립트에는 "function newFunction() {...}"이 있습니다. 새로운 함수(); // 그런 기능은 없습니다!
당연히 브라우저는 스크립트를 로드할 시간이 없었을 것입니다. 현재 loadScript
함수는 로드 완료를 추적하는 방법을 제공하지 않습니다. 스크립트가 로드되고 결국 실행됩니다. 그게 전부입니다. 하지만 우리는 그 스크립트의 새로운 함수와 변수를 사용하기 위해 언제 그런 일이 발생하는지 알고 싶습니다.
스크립트가 로드될 때 실행되어야 하는 loadScript
에 두 번째 인수로 callback
함수를 추가해 보겠습니다.
함수 loadScript(src, 콜백) { let script = document.createElement('script'); script.src = src; script.onload = () => 콜백(스크립트); document.head.append(스크립트); }
onload
이벤트는 리소스 로딩: onload 및 onerror 문서에 설명되어 있으며, 기본적으로 스크립트가 로드되고 실행된 후에 함수를 실행합니다.
이제 스크립트에서 새 함수를 호출하려면 콜백에 이를 작성해야 합니다.
loadScript('/my/script.js', function() { // 스크립트가 로드된 후 콜백이 실행됩니다. 새로운 함수(); // 이제 작동합니다. ... });
이것이 바로 아이디어입니다. 두 번째 인수는 작업이 완료될 때 실행되는 함수(대개 익명)입니다.
다음은 실제 스크립트를 사용하여 실행 가능한 예입니다.
함수 loadScript(src, 콜백) { let script = document.createElement('script'); script.src = src; script.onload = () => 콜백(스크립트); document.head.append(스크립트); } loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { Alert(`멋지네요. ${script.src} 스크립트가 로드되었습니다`); 알리다( _ ); // _는 로드된 스크립트에 선언된 함수입니다. });
이를 비동기 프로그래밍의 "콜백 기반" 스타일이라고 합니다. 비동기적으로 작업을 수행하는 함수는 함수가 완료된 후 실행할 함수를 넣는 callback
인수를 제공해야 합니다.
여기서는 loadScript
로 수행했지만 물론 일반적인 접근 방식입니다.
두 개의 스크립트를 어떻게 순차적으로 로드할 수 있습니까? 첫 번째 스크립트와 그 다음 두 번째 스크립트는 무엇입니까?
자연스러운 해결책은 다음과 같이 콜백 내부에 두 번째 loadScript
호출을 넣는 것입니다.
loadScript('/my/script.js', function(script) { Alert(`멋지네요. ${script.src}가 로드되었습니다. 하나 더 로드하겠습니다`); loadScript('/my/script2.js', function(script) { Alert(`멋지네요. 두 번째 스크립트가 로드되었습니다`); }); });
외부 loadScript
완료된 후 콜백은 내부 로드스크립트를 시작합니다.
스크립트를 하나 더 원하면 어떻게 될까요…?
loadScript('/my/script.js', function(script) { loadScript('/my/script2.js', function(script) { loadScript('/my/script3.js', function(script) { // ...모든 스크립트가 로드된 후 계속됩니다. }); }); });
따라서 모든 새로운 작업은 콜백 안에 있습니다. 이는 소수의 작업에는 적합하지만 많은 작업에는 적합하지 않으므로 곧 다른 변형을 보게 될 것입니다.
위의 예에서는 오류를 고려하지 않았습니다. 스크립트 로딩에 실패하면 어떻게 되나요? 우리 콜백은 이에 반응할 수 있어야 합니다.
다음은 로딩 오류를 추적하는 향상된 버전의 loadScript
입니다.
함수 loadScript(src, 콜백) { let script = document.createElement('script'); script.src = src; script.onload = () => 콜백(null, 스크립트); script.onerror = () => callback(new Error(`${src}에 대한 스크립트 로드 오류`)); document.head.append(스크립트); }
성공적으로 로드되면 callback(null, script)
호출하고 그렇지 않으면 callback(error)
호출합니다.
사용법:
loadScript('/my/script.js', function(오류, 스크립트) { if (오류) { // 오류 처리 } 또 다른 { // 스크립트가 성공적으로 로드되었습니다. } });
다시 한번 말씀드리지만, 우리가 loadScript
에 사용한 방법은 실제로 매우 일반적입니다. 이를 "오류 우선 콜백" 스타일이라고 합니다.
규칙은 다음과 같습니다.
callback
의 첫 번째 인수는 오류가 발생할 경우 이를 위해 예약되어 있습니다. 그런 다음 callback(err)
이 호출됩니다.
두 번째 인수(및 필요한 경우 다음 인수)는 성공적인 결과를 위한 것입니다. 그런 다음 callback(null, result1, result2…)
이 호출됩니다.
따라서 단일 callback
함수는 오류를 보고하고 결과를 다시 전달하는 데 모두 사용됩니다.
언뜻 보면 비동기 코딩에 대한 실행 가능한 접근 방식처럼 보입니다. 그리고 실제로 그렇습니다. 하나 또는 두 개의 중첩 호출의 경우 괜찮아 보입니다.
그러나 차례로 이어지는 여러 비동기 작업의 경우 다음과 같은 코드가 있습니다.
loadScript('1.js', function(오류, 스크립트) { if (오류) { 핸들오류(오류); } 또 다른 { // ... loadScript('2.js', function(오류, 스크립트) { if (오류) { 핸들오류(오류); } 또 다른 { // ... loadScript('3.js', function(오류, 스크립트) { if (오류) { 핸들오류(오류); } 또 다른 { // ...모든 스크립트가 로드된 후 계속됩니다(*) } }); } }); } });
위의 코드에서:
1.js
로드하고 오류가 없으면…
2.js
로드하고 오류가 없으면…
3.js
로드한 다음 오류가 없으면 다른 작업을 수행합니다 (*)
.
호출이 더 많이 중첩됨에 따라 코드가 더 깊어지고 관리하기가 점점 더 어려워집니다. 특히 더 많은 루프, 조건문 등을 포함할 수 있는 ...
대신 실제 코드가 있는 경우 더욱 그렇습니다.
이를 '콜백 지옥' 또는 '파멸의 피라미드'라고도 합니다.
중첩된 호출의 "피라미드"는 모든 비동기 작업에서 오른쪽으로 증가합니다. 곧 통제 불능 상태가 됩니다.
그래서 이런 코딩 방식은 별로 좋지 않습니다.
다음과 같이 모든 작업을 독립형 함수로 만들어 문제를 완화할 수 있습니다.
loadScript('1.js', step1); 함수 step1(오류, 스크립트) { if (오류) { 핸들오류(오류); } 또 다른 { // ... loadScript('2.js', step2); } } 함수 step2(오류, 스크립트) { if (오류) { 핸들오류(오류); } 또 다른 { // ... loadScript('3.js', step3); } } 함수 step3(오류, 스크립트) { if (오류) { 핸들오류(오류); } 또 다른 { // ...모든 스크립트가 로드된 후 계속됩니다(*) } }
보다? 동일한 작업을 수행하며 모든 작업을 별도의 최상위 기능으로 만들었기 때문에 이제는 깊은 중첩이 없습니다.
작동하지만 코드는 찢어진 스프레드시트처럼 보입니다. 읽기가 어렵고, 읽는 동안 조각 사이를 눈으로 건너뛰어야 한다는 것을 눈치챘을 것입니다. 특히 독자가 코드에 익숙하지 않고 어디에서 눈을 떼야 할지 모르는 경우에는 불편합니다.
또한 step*
이라는 함수는 모두 일회용이며 "파멸의 피라미드"를 피하기 위해서만 만들어졌습니다. 누구도 액션 체인 외부에서 이를 재사용하지 않을 것입니다. 따라서 여기에는 약간의 네임스페이스가 어수선하게 있습니다.
우리는 더 나은 것을 갖고 싶습니다.
다행히도 그러한 피라미드를 피할 수 있는 다른 방법이 있습니다. 가장 좋은 방법 중 하나는 다음 장에서 설명할 "약속"을 사용하는 것입니다.