정보실

웹학교

정보실

javascript JavaScript에서 비동기 프로그래밍에 대해 알아야 할 사항

본문

내 경력을 통해 나는 면접관과 후보자로서 많은 면접을 보았습니다. Promises과 async/await를 사용하여 비동기 코드를 코딩하라는 메시지가 표시 될 때 후보자의 비율이 떨어져 있는 것을 보았습니다.


비동기식 프로그래밍은 JavaScript의 핵심 요소이지만 많은 개발자들이 어떤 이유로 든 진정한 의미를 이해하지 못합니다.


물론, 그들은 비동기 코드로 작업했으며 그들이 볼 때 그것을 인식 할 것입니다. 그들은 그것이 어떻게 작동하는지 어느 정도 알고 있습니다. 그러나 많은 사람들이 그것을 정확하게 재현하지 못하고 모든 필수 구현 세부 사항을 이해하지 못합니다.

내가 인터뷰 한 일부 개발자들은 이 수준에 머물러 있습니다. .


https://medium.com/swlh/what-you-need-to-know-about-asynchronous-programming-in-javascript-894f90a97941 


진지한 JavaScript 개발자가 되려면 비동기 프로그래밍이 제 2의 본성이어야 합니다. 빵 굽는 사람이 빵을 알고 있는 것처럼


비동기식 프로그래밍은 까다로울 수 있지만 로켓 과학은 아닙니다. 알아야 할 것이 있습니다.


이러한 기본 사항을 숙지하면 고급 사용 사례를 이해하고 직접 구현하는 데 어려움이 없습니다.


이미 알고있는 것 


Promise 및 async / await 작업 방법은 이미 알고 있습니다. 적어도 나는 기대합니다. 그렇지 않은 경우 먼저 MDN에서 이 기사를 확인한 다음 다시 방문하십시오.


알아야 할 사항 


JavaScript의 비동기 프로그래밍에는 계속 되돌아 오는 몇 가지 패턴이 있습니다. 이것들은 매우 실용적이고 다목적 솔루션이며, 자주 적용 할 수 있으며 JavaScript 툴박스의 표준 도구 여야 합니다.


콜백 기반 코드를 약속으로 변환 


콜백 기반 코드는 특히 여러 체인 호출 및 오류 처리와 관련하여 작업하기가 번거로울 수 있습니다. 이것이 기본적으로 약속이 존재하는 전체 이유이기 때문에 비동기 프로그래밍이 쉬워집니다.


콜백 기반 코드를 약속을 사용하는 코드로 변환하는 것은 매우 간단합니다.


파일을 비동기 적으로 읽는 Node.js 코드를 보자.


const fs = require('fs');fs.readFile('/path/to/file', (err, file) => {
if(err) {
// handle the error
}
else {
// do something with the file
}
});


fs.readFile 메소드는 파일 경로와 콜백을 인수로 사용합니다. 파일을 읽을 때 콜백은 오류가 발생한 경우 첫 번째 인수로 오류가 발생하거나 첫 번째 인수로 null이 되고 성공할 경우 파일 내용이 두 번째 인수로 null과 함께 호출됩니다.


이 방법을 다음과 같은 약속으로 사용할 수 있다면 좋을 것입니다.


fs.readFile('/path/to/file')
.then(file => {
// do something with the file
})
.catch(err => {
// handle the error
}); 


이를 달성하기 위해 Promise에 쉽게 포장 할 수 있습니다.


const readFileAsync = path => {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, file) => {
return err ? reject(err) : resolve(file);
});
});
};
// usage
readFileAsync('/path/to/file')
.then(file => {
// do something with the file
})
.catch(err => {
// handle the error
});


readFileAsync 함수는 경로를 인수로 사용하여 새 약속을 반환합니다. Promise 생성자는 소위 executor 함수를 사용하여 resolve 및 reject 콜백을 수신합니다.


이러한 콜백은 각각 성공과 실패의 경우에 호출되어야 합니다. 랩핑 된 fs.readFile 메소드의 콜백이 오류를 수신하면 거부되도록 전달됩니다. 성공하면 받은 파일이 전달되어 해결됩니다.


주의를 기울이면 약속은 실제로 콜백을 기반으로 한다는 것을 알 수 있습니다. 


콜백 기반 함수를 Promise 기반 함수로 전환하는 방법입니다.


보너스 팁 : Node.js의 경우 아마도 util.promisify를 사용합니다.


이벤트 기반 코드에서도 동일한 작업을 수행 할 수 있습니다. 예를 들어 FileReader를 사용하십시오. 브라우저에서 파일을 읽고 ArrayBuffer로 바꾸고 싶다고 가정 해 봅시다.


const toArrayBuffer = blob => {
const reader = new FileReader();
reader.onload = e => {
const buffer = e.target.result;
};
reader.onerror = e => {
// handle the error
};
reader.readAsArrayBuffer(blob);
};


이 이벤트 기반 코드를 같은 방식으로 약속으로 전환 할 수도 있습니다.


const toArrayBuffer = blob => {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = e => resolve(e.target.result);
reader.onerror = e => reject(e.target.error); reader.readAsArrayBuffer(blob);
});
};


여기서 기본적으로 콜백 대신 이벤트와 동일한 작업을 수행했습니다.


Promise 체인에서 중간 결과 사용 


Promises와 잠시 동안 작업하는 경우 Promises를 연결하고 Promise 콜백 범위를 벗어난 중간 결과를 사용해야 하는 상황이 발생합니다.


const db = openDatabase();db.getUser(id)
.then(user => db.getOrders(user))
.then(orders => db.getProducts(orders[0]))
.then(products => {
// cannot access orders here!
})


이 예에서는 데이터베이스 연결을 열고 ID로 사용자를 검색하고 해당 사용자의 주문을 얻은 다음 사용자의 첫 번째 주문 인 제품을 가져옵니다.


여기서 문제점은 db.getProducts()에서 리턴 된 Promise의 콜백 내에서 orders 변수는 이전 Promise의 콜백 범위에만 정의되어 db.getOrders()에서 리턴 되었기 때문에 액세스 할 수 없다는 것입니다.


이에 대한 가장 간단한 해결책은 외부 범위에서 주문 변수를 초기화하여 어디서나 사용할 수 있도록 하는 것입니다.


const db = openDatabase();let orders;  // initialized in outer scopedb.getUser(id)
.then(user => db.getOrders(user))
.then(response => {
orders = response; // orders is assigned here
return db.getProducts(orders[0]);
})
.then(products => {
// orders is now accessible here!
})


작동하지만 특히 많은 변수가 있는 복잡한 Promise 체인이 있는 경우 가장 깨끗한 솔루션은 아닙니다. 이로 인해 초기화 해야 하는 변수 목록이 길어집니다.


대신, 주요 사용 사례 중 하나이므로 async 및 await를 사용해야 합니다. 동기식 구문과 함께 비동기 코드를 사용할 수 있도록 모든 변수가 동일한 범위를 공유합니다.


const db = openDatabase();const getUserData = async id => {
const user = await db.getUser(id);
const orders = await db.getOrders(user);
const products = await db.getProducts(orders[0]);
return products;
};
getUserData(123);


getUserData 내에서 모든 변수는 이제 동일한 범위를 공유하고 해당 범위에서 모두 액세스 할 수 있지만 코드는 완전히 비동기 적입니다. getUserData에 대한 호출은 다음 코드를 차단하지 않습니다.


이것은 async와 await가 실제로 빛을 발하는 예입니다.


async/await를 Promises와 결합 할 수 있습니다 


이전 예제에서 getUserData에서 제품을 어떻게 반환합니까?


getUserData는 비동기 함수이므로 다른 비동기 함수 내에서 await를 사용합니다.


const getProducts = async () => {
const products = await getUserData(123);
}; 


그러나 .then을 사용할 수도 있습니다.


getUserData(123)
.then(products => {
// use products
}) 


비동기 함수는 항상 암시 적 Promise를 반환하기 때문에 작동합니다.


주의를 기울 였다면 async/await는 실제로 Promises를 기반으로 한다는 것을 알게 될 것입니다. 


알아야 할 것 


항상 그렇듯이 악마는 세부 사항에 있으며 JavaScript의 비동기 프로그래밍에도 적용됩니다. 나는 종종 면접 중에 개발자가 필수 구현 세부 정보를 놓치게 되는 것을 보았는데, 이는 개념에 대한 이해가 부족하다는 것을 보여줍니다.


Promise 콜백은 항상 Promise을 반환 


체인을 약속 할 때 일반적으로 체인의 모든.에서 약속을 반환합니다.


db.getUser(id)
.then(user => db.getOrders(user))
.then(orders => db.getProducts(orders[0]))
.then(products => db.getStats(products)) 


위 예제에서 .then 내의 모든 콜백에서 Promise가 반환됩니다. 즉, db.getOrders, db.getProducts 및 db.getStats는 모두 Promise를 반환합니다.


그러나 복잡한 체인이 있으면 조만간 약속이 아닌 것을 반환해야 합니다.


db.getUser(id)
.then(user => db.getOrders(user))
.then(orders => db.getProducts(orders[0]))
.then(products => products.length) // <-- oops, not a Promise!
.then(numberOfProducts => {
db.saveStats(numberOfProducts);
return db.getStats(products);
})
...


그러나 .then 또는 .catch 내의 Promise 콜백의 반환 값이 자동으로 Promise에 래핑되므로 위의 코드는 완벽하게 정상적으로 실행됩니다.


이는 약속 체인 내에서 임의의 값을 반환 할 수 있음을 의미합니다.


.then 실제로 두 가지 주장을 취할 수 있습니다 


일반적으로 하나의 인수 만 전달하면 Promise가 해결 될 때 호출되어야 하는 콜백이 전달됩니다. 약속이 거부 될 때 호출되어야 하는 콜백은 .catch로 전달됩니다.


그러나. Promise가 해결 될 때 (성공), Promise가 거부 할 때 (오차) 두 가지 콜백을 실제로 취할 수 있습니다.


그래서 이것 대신에 :


fetch('http://some.domain.com')
.then(response => console.log('success'))
.catch(err => console.error('error')) 


당신은 또한 이것을 할 수 있습니다 :


fetch('http://some.domain.com')
.then(response => console.log('success'),
err => console.error('error')) 


업데이트 : Reddit의 세심한 독자들은 실제로 이에 대한 사용 사례가 있다고 지적했습니다. 내가 틀렸어. 처벌을 위해 나는 일주일 동안 67 레벨의 딥 콜백 지옥 코드를 작성하도록 강요했다. 저에게 가르쳐 줄 것입니다. 아래의 업데이트 된 텍스트를 읽으십시오. 


.catch와 .cat에 대한 두 번째 인수로서의 오류 콜백의 차이점은 Promise 체인의 어딘가에서 성공 콜백에서 오류가 발생하면 .catch에 의해 포착된다는 것입니다.


fetch('http://some.domain.com')
.then(response => response.json())
.then(json => fetch(...))
.then(response => response.text())
.then(text => ...)
.catch(err => console.error(err)) // any error will be caught here 


성공 콜백과 오류 콜백을 모두에 전달하면 성공 콜백에서 오류가 발생하면 오류 콜백에서 오류가 발생하지 않습니다.


fetch('http://some.domain.com')
.then(successCallback,
errorCallback) // an error in successCallback will NOT go here 


이것을 인식하는 것이 매우 중요하며 개인적으로 체인 끝에 항상 하나의 .catch를 넣지 만 실제로는 오류 처리기의 사용사례가 .then의 두 번째 인수로 사용됩니다.


세분화 된 오류 처리가 필요하거나 오류가 발생한 지점 이후 체인의 나머지 약속을 계속 처리해야 할 때마다 이 오류 처리기 인 두 번째 인수는 정확히 필요한 것입니다.


promise1()
.then(() => {
// success callback
}, () => {
// error callback, errors in promise1 will go here
// if an error is thrown here or a rejected promise is returned
// execution will go to the final error handler in .catch
// if we return anything else, the chain will continue

})
.then(() => promise2())
.then(() => promise3())
.catch(finalErrorHandler); 


위의 예에서 오류가 발생하면 .then에 대한 두 번째 인수로 제공되는 오류 콜백에 의해 오류가 발생합니다. 이 오류 처리기 내에서 수행 할 작업을 결정할 수 있습니다.


오류가 발생하거나 거부 된 약속을 반환하면 finalErrorHandler에 의해 잡힙니다. 다른 값을 반환하면 체인은 promise2 및 promise3을 계속 실행합니다. 따라서 이 경우 체인이 종료되지 않고 계속 실행되어 복잡한 세밀한 오류 처리 및 복구를 효과적으로 수행 할 수 있습니다.


나는 그런 시나리오를 직접 보지 못했지만 이것은 두 번째 인수로 오류 처리기에 대한 유효한 사용 사례입니다. 이 접근 방식을 사용하면 성공 처리기에서 발생하는 모든 오류가 포착되지 않습니다.


모르는 것 


비동기 프로그래밍은 물론 훨씬 더 복잡한 시나리오를 요구하는 훨씬 더 복잡해질 수 있습니다. 나는 면접 후보자들에게 비동기식 API 호출이 조건부로 수행되어야 하는 매우 간단한 시나리오를 코딩하도록 요청했다.


조건부 비동기 API 호출 


API 호출은 사용자가 로그인 한 다음에 만 세션을 삭제하기 위해 일부 코드를 실행해야 한다고 가정 해 보겠습니다. 그러나 세션 삭제는 API 호출이 완료된 후에 만 ​​수행 할 수 있습니다. 사용자가 로그인하지 않은 경우 API 호출을 건너 뛰고 세션이 즉시 삭제됩니다.


사용자가 로그인하면 API 호출에서 반환 된 Promise가 해결 될 때까지 기다린 다음에 전달 된 콜백 내에서 세션을 삭제할 수 있습니다.


if(userIsLoggedIn) {
apiCall()
.then(res => {
deleteSession()
})
} 


사용자가 로그인하지 않은 경우 API 호출을 건너 뛰고 바로 deleteSession으로 이동합니다. 그러나 API 호출은 비동기 적이므로 즉시 실행 된 코드가 실행되므로 deleteSession에 대한 호출을 복제해야 합니다.


const checkLogin = () => {
if(userIsLoggedIn) {
apiCall()
.then(res => {
deleteSession()
})
}
else {
deleteSession();
}
}; 


이제 우리는 deleteSession에 대한 두 번의 호출이 있습니다. apiCall은 비동기식이므로 .then 내부에서 deleteSession을 실행하고 사용자가 로그인하지 않은 경우에 대한 약속을 만들 수밖에 없습니다.


const checkLogin = () => {
if(userIsLoggedIn) {
return apiCall();
}
else {
return Promise.resolve(true); // it works, but this is not good
}

};
checkLogin()
.then(() => {
deleteSession();
})


deleteSession에 대한 중복 호출이 사라졌지 만 이제는 사용자가 로그인하지 않은 상태에서 checkLogin에서 쓸모없는 약속을 반환해야 하므로 코드가 올바른 순서로 실행됩니다.


이것이 프로덕션 코드에서 자주 본 솔루션이지만 코드 냄새가 나므로 피해야 합니다.


이 예제는 여전히 매우 간단하지만 코드가 복잡해지면 오류가 발생하기 쉬운 코드로 바뀔 수 있습니다.


깨끗하고 간결한 방법으로 이 문제를 해결하는 유일한 방법은 async를 사용하고 기다리는 것입니다.


const checkLogin = async () => {
if(userIsLoggedIn) {
await apiCall();
}
deleteSession();
};


이제 사용자가 로그인하면 apiCall이 실행됩니다. checkLogin은 이제 비동기 함수이고 apiCall은 await로 시작하므로 apiCall이 완료 될 때까지 기다린 다음 deleteSession을 실행합니다.


사용자가 로그인하지 않으면 apiCall을 건너 뛰고 deleteSession 만 실행됩니다. 더 이상 중복 코드, 더 이상 쓸모없는 약속이 없습니다.


이것이 async과 await의 힘입니다.


타임 아웃이 있는 약속 


또 다른 일반적인 시나리오는 (아마도) 오래 실행되는 API 호출에서 시간 초과를 설정해야 하는 경우입니다.


5초 이내에 응답해야 하는 API를 호출한다고 가정 해 보겠습니다. 그렇지 않은 경우 통화가 너무 오래 걸리고 시간이 초과되었다는 메시지가 표시되어야 합니다.


개발자가 여러 Promise 스파게티에 얽히면서 어려움을 겪는 것을 보았습니다. 솔루션은 실제로 Promise.race로 구현하기가 매우 간단합니다.


Promise.race는 약속의 배열 (또는 다른 Iterable)을 가져 와서 배열의 약속 중 하나가 해결되거나 거부되는 즉시 해결 또는 거부합니다. 이를 사용하여 시간 초과를 적용하려는 약속을 다른 약속과 함께 해당 시간 이후에 해결 / 거절하는 Promise.race에 전달할 수 있습니다.


따라서 약속 시간이 지정된 제한 시간 내에 해결되면 문제가 없습니다. 그러나 그렇지 않은 경우 두 번째 약속은 시간 초과 시간이 지나면 해결되거나 거부되며 시간 초과가 있었다는 신호를 보냅니다.


const apiCall = url => fetch(url);const timeout = time => {
return new Promise((resolve, reject) => {
setTimeout(() => reject('Promise timed out'), time);
});
};
Promise.race([apiCall(url), timeout(1000)])
.then(response => {
// response from apiCall was successful
})
.catch(err => {
// handle the timeout
});


apiCall에서 발생할 수 있는 다른 오류도 .catch 처리기로 이동하므로 오류가 시간 초과인지 또는 실제 오류인지 확인할 수 있어야 합니다.


JavaScript에서 비동기 프로그래밍을 이해하는 열쇠 


비동기식 프로그래밍을 진정으로 이해하려면 기본이 되는 기초와 기초를 이해해야 합니다.


async/await는 약속을 기반으로 합니다. 

약속은 콜백을 기반으로 합니다. 

콜백은 JavaScript에서 비동기 프로그래밍의 기초입니다. 


내가 인터뷰 한 개발자가 너무 많거나 적다는 점에 대해 모호하거나 얕게 이해했지만 충분하지 않습니다.


기초를 진정으로 이해하는 경우에만 JavaScript로 비동기 프로그래밍을 완전히 이해하고 마스터 할 수 있습니다.



페이지 정보

조회 61회 ]  작성일19-10-07 16:12

웹학교