-
[결국은 자바스크립트] 1. 비동기 처리 다시 파헤쳐보기front-end/Javascript&Typescript 2023. 6. 21. 00:41
1. 서론
리액트, 뷰 , 앵귤러 뭐가 됐든간에 자바스크립트의 비동기처리의 flow를 (비동기뿐만 아니라 자바스크립트의 기초 및 개념) 이해를 잘 못하면 어떤 프레임워크던 사용할때 이해가 잘 가지 않은채로 프론트 개발을 하는것같습니다.
저는 프론트 개발을 시작할때 리액트 , 뷰 SPA 프레임워크를 접하기전에 "자바스크립트만 잘해도 어떤 프레임워크를 사용하던 이해하기 쉬울거야." 약 2년전에 친구이자 선배인 개발자에게 이 말을 들었습니다.
그때 저의 레벨은 막 입사해서 제이쿼리와 바닐라스크립트로 하는 정도 였어서 사실 이게 크게 와닿지 않았습니다.
하지만 2년뒤 현재 리액트로 개발을 하다보니 저 말이 참 공감이 가는거같습니다. 리액트로 프론트 개발을 하다가 어떤 부분이 막히거나
해결이 잘 안되서 구글링을 통해 검색을 해보면 저의 경우 70프로 이상이 자바스크립트의 개념이 조금 더 탄탄 했더라면 , 제가 말했던 70프로를 30프로 정도로 줄일 수 있지 않을까 싶습니다.
그래서 이번 기회에 알고있지만 , "야 그래서 비동기 처리가 뭔데? , 그래서 클로저의 개념이 뭔데? 자바스크립트의 동작원리는 뭔데?"
라는 질문을 받으면 막힘없이 대답 하고자(?) 다시한번 자바스크립트의 개념을 정리하고자합니다.
앞으로
1. 비동기란 무엇인가?
2. 클로저와 실행컨텍스트란 무엇인가?
3. 콜스택 , 콜백큐 , 이벤트 루프
우선은 일단 이렇게 총 3개로 나눠서 포스팅을 할 예정입니다.
이 게시글은 첫번째로 비동기란 무엇인가로 다뤄보겠습니다!
2. 비동기란 무엇인가?
우선 비동기를 알아보기전에 동기 뜻을 알아야 할거같아요
동기식 처리는 한번에 코드를 한줄씩 차례로 실행이 된다는 소리 입니다 .
1.예시
console.log('첫번째'); console.log('두번째'); console.log('세번째');위 함수는 어떻게 실행이 될까요?
콘솔창에 찍어보면 당연히 순서대로 로그가 출력이 될것입니다.
2.예시
console.log('첫번째'); setTimeout(function(){}, 1000); console.log('두번째');위 코드는 어떻게 실행이될까요 ?
첫번째가 콘솔에 출력이 되고 , 1초 뒤에 두번째가 콘솔에 출력이 된다고 생각하시면 , 비동기 처리의 대해서 이해를 못하고 계시는겁니다.
위 코드는 첫번째와 두번째가 출력이 동시에 됩니다.
자바스크립트는 다른 일반 프로그래밍 언어과 다르게 동작합니다.
setTimeOut()의 함수는 실행까지 시간이 걸리는 함수입니다. 1000ms로 선언해놨으니 1초가 걸리겠네요.
자바스크립트의 실행머신의 웹브라우저는 이런 비동기적인 코드를 보면 다른 코드부터 실행하려고 하는게 있습니다.
이런 처리 방식이 비동기 라고합니다. 실행이 오래걸리는 코드는 뒤로하고 실행이 바로 가능한 코드 부터 처리하는 방식이죠
setTimeOut 말고 다른 함수는 대표적으로 데이터를 fetch 해오는 api 밑 라이브러리 등 axois , fetch , ajax 이렇게 api에
get요청을 하는 함수들이 있고 자바스크립트 입문쯤 배우는 addEventListiner도 비동기 함수 입니다.
자 여기서 생각을 해보면 axios , fetch 등으로 데이터를 갖고오는 행위를 할떄 get요청 한 데이터의 갯수가 1억개라고 가정했을때
만약에 동기적으로 1억개의 데이터를 가져올때까지 클라이언트가 브라우저상에서 아무것도 못하면 꽤나 답답하겠죠?
자바스크립트를 실행하는 브라우저는 이런 오래걸리는 함수는 wep api에 잠시 넣어놨다가 대기를 하다가 다른 것이 처리가 되면서
코드가 처리가 완료 되면 그때 다시 코드를 실행시킵니다.
2-1. 비동기 처리의 대표적인 예제 첫번째 : 콜백함수
function fn1(callback){ console.log("첫번째"); callback(); } function f2(){ setTimeout(()=>{ console.log("두번째"); },1000) } fn1(f2);위와 같은 예시를 구글에 "비동기 처리" 라고 검색하면 수도 없이 많이 봤던거같습니다..
간략하게 설명하면 fn1 함수의 파라미터로 콜백함수를 하나 만들어주고 fn1을 호출 할때 fn2를 파라미터에 담아서 호출을 한것뿐입니다.
근데 위와 같은 방법은 현재 두가지 함수로만 구현해서 간단하지 위 예시가 예를들어 10개,100개가 넘어가면 그만큼 콜백함수를 만들어줘야겠죠..
이것을 "콜백지옥"이라고도 합니다.
2-2. 비동기 처리의 대표적인 예제 두번째 : Promise
이런 콜백지옥을 방지하고자 Promise 패턴이 es6에서 나왔습니다.
const myPromise = new Promise((resolve, reject) => { // ..Promise Logic. }); myPromise .then(value => console.log(value)) .catch(error => console.log(error)) .finally(() => console.log("끝!"));myPromise는 Promise를 할당 했기때문에 then , catch 그리고 finally같은 함수를 같이 이용할 수 있습니다.
1.then : promise가 resolve 됐을때의 값
2.catch promise가 reject 됐을때의 값
3.finally promise가 resolve , reject가 됐던간에 , 실행 시킬 함수.
콜백함수와는 다르게 순차적으로 뭔가를 실행할 때 코드가 옆으로 길어지지 않습니다. then 함수를 붙여서 순차적으로 실행하니까요.
콜백함수는 불가능한 '실패시 특정 코드를 실행해주세요~' 라고 코드를 짤 수 있습니다. (catch)
Promise를 좀 직관적으로 설명을 하자면 , 위에 정리했듯이 Promise가 성공하면 then()을 실행하고 실패하면 catch를 실행하는겁니다.
const myPromise = new Promise((resolve, reject) => { let expensiveCal = 10_000 + 100_000; if(expensiveCal === 110_000){ resolve("정답!"); }else{ reject("오답!"); } }); myPromise .then(value => console.log(value)) .catch(error => console.log(error)) .finally(() => console.log("끝!"));위의 예시는 어떻게 출력이 될까요? 10,000 + 100,000은 110,000이 맞으니까 resolve가 실행 될것이니 아래의 then이 실행이 되고
finally의 끝이 실행이됩니다.
만약에 조건문안의 값이 110,000이 아니라 100,000을 넣으면 조건값이 맞지않으니 reject가 호출이 되어서 error 문의 오답과 finllay문의 끝이 출력이 되겠죠.
Promise의 특징
- status가 있습니다. pending , resolved , rejected
pending: resovle , reject 되기전의 말 그대로 pending "대기"인 상태
resolved : 성공 상태
rejected : 실패 상태
Promise 는 동기를 비동기로 만들어주거나 바꿔주는 코드가 아닙니다.
그저 비동기처리를 콜백지옥같은 패턴보다 간결하게 해주는 디자인 패턴 일뿐입니다.
실무에서 제일 많이 쓰는 방법은 fetch api를 이용하여 get요청을 보낼때 인데요 , fetch api는 Promise를 반환하여 , then , catch 그리고 finally 문을 쓸 수 있습니다. then의 체이닝을 이용해서 아래와 같은 예시처럼 여러개의 api 요청을 보낼 수 도 있습니다.
fetch('http://fakeapi1.com') .then(response => response.json()) .then(data => { console.log(data); // 첫 번째 요청 결과 출력 return fetch('http://fakeapi2.com'); }) .then(response => response.json()) .then(data => { console.log(data); // 두 번째 요청 결과 출력 return fetch('http://fakeapi3.com'); }) .then(response => response.json()) .then(data => { console.log(data); // 세 번째 요청 결과 출력 }) .catch(error => { console.error(error); // 에러 처리 });Promise.all 알아보기
Promise.all()은 여러 개의 Promise를 동시에 실행하고, 모든 Promise가 완료될 때까지 기다린 후에 결과를 반환합니다. 모든 Promise가 성공적으로 완료되었을 때는 결과 배열을 반환하고, 그 중 하나라도 실패한 Promise가 있을 경우 에러를 반환합니다.
또한 병렬적으로 여러 개의 Promise를 처리하고, 모든 Promise가 완료될 때까지 기다린 후 결과를 반환하는 데 사용됩니다.
const resourceUrls = [ 'http://fakeapi.com/resource1', 'http://fakeapi.com/resource2', 'http://fakeapi.com/resource3' ]; const promises = resourceUrls.map(url => fetch(url)); Promise.all(promises) .then(responses => { // 모든 데이터가 정상적으로 처리됐을때 로직 responses.forEach(response => { // response를 이용해 화면에 리소스를 표시하는 등의 작업 console.log(response); }); }) .catch(error => { // 하나라도 로드 실패 시의 에러 처리 로직 console.error(error); });위의 예시에서는 각각의 API 요청을 Promise로 감싸고, Promise.all()을 사용하여 모든 요청이 완료될 때까지 기다립니다. 그 후에는 각각의 요청 결과가 배열로 반환되어 처리됩니다.
여기서 중요한건 하나라도 에러가 있을땐 catch문이 실행이 되고 , 병렬적으로 작업이 되고 배열을 리턴을 한다는것입니다.
Promise.race 알아보기
Promise race는 여러 개의 프로미스(Promise) 중에서 가장 먼저 종료되는 프로미스 하나만을 선택하는 방식입니다. 주어진 프로미스 중 가장 빨리 결과를 반환하는 프로미스가 있을 경우, Promise race는 해당 프로미스의 결과를 반환합니다. 다른 프로미스들은 무시되며, 결과가 나올 때까지 대기하지 않습니다.
Promise race를 사용하는 경우는 주로 아래와 같습니다
1. 여러 개의 API 호출을 동시에 시작하고, 가장 빨리 도착한 응답을 사용하고자 할 때.
2. 여러 개의 이벤트를 동시에 감지하고, 가장 먼저 발생한 이벤트에 대한 처리를 수행하고자 할 때.
예시
const fetchFromAPI = (url) => { return new Promise((resolve, reject) => { // API 호출을 수행하고 결과를 resolve 또는 reject로 처리 // 이 예시에서는 setTimeout을 사용하여 임의의 응답 시간을 설정하였습니다. setTimeout(() => { resolve(`${url}의 응답`); }, Math.random() * 3000); }); }; const apiUrls = [ 'https://api1.example.com', 'https://api2.example.com', 'https://api3.example.com' ]; // Promise race를 사용하여 가장 빨리 응답이 도착한 API를 찾습니다. Promise.race(apiUrls.map(fetchFromAPI)) .then(result => { console.log('가장 빨리 도착한 응답:', result); }) .catch(error => { console.error('에러 발생:', error); });위 예시에서 fetchFromAPI 함수는 주어진 URL로 API 호출을 수행하고 프로미스를 반환합니다. apiUrls 배열에는 여러 개의 API 엔드포인트 URL이 들어있습니다. Promise.race 메서드는 apiUrls 배열을 fetchFromAPI 함수로 매핑하여 각 API 호출에 대한 프로미스 배열을 생성합니다. 그리고 가장 빨리 응답이 도착한 프로미스를 선택하여 해당 응답을 출력합니다.
2-3. 비동기 처리의 대표적인 예제 세번째 : async await
Promise 마저 어렵고 then() chainng 을 피하고싶을때 async await을 쓰면 보다 간결하고 직관적으로 코드를 쓸 수 있습니다.
저도 웬만하면 비동기 처리할때 애용하는 방법입니다.
async/await는 ES2017부터 도입된 비동기 처리를 위한 문법입니다.
async 함수를 쓰면 Promise가 리턴이 됩니다. 함수에서만 사용 할 수 있고 await 키워드 또한 async 함수 내부안에서만 사용 할 수 있습니다.
async function myPromise(){ let expensiveCal = new Promise((resolve,reject)=>{ const expensiveResult = 10_000 + 100_000; if(expensiveResult === 110_000){ resolve("정답!"); }else{ reject("오답!"); } }) return expensiveCal; } myPromise().then(res => console.log(res));위와 같은 예시만 보면 아까의 Promise의 패턴과 다른점이 async 키워드만 함수에 추가한거밖에 안되서 별반 좋은걸 모르겠지만
아래의 코드 처럼 await을 사용하면서 써보면 훨씬 간결 하고 직관적 입니다.
async function myPromise(){ let expensiveCal = new Promise((resolve,reject)=>{ const expensiveResult = 10_000 + 100_000; if(expensiveResult === 110_000){ resolve("정답!"); }else{ reject("오답!"); } }) const result = await expensiveCal; console.log(result); } myPromise();이렇게 해도 콘솔에 정답이 출력되는것이 볼 수 있습니다.
다른 예제로 fetch api로 통해서 then chainning 과 await 키워드를 쓰는것을 비교 해보겠습니다.
1. then 을 이용
fetch('https://jsonplaceholder.typicode.com/todos/1') .then(response => response.json()) .then(json => console.log(json))2 async await을 이용한 방법
async function fetchData() { try { const response = await fetch('https://jsonplaceholder.typicode.com/todos/1'); const json = await response.json(); console.log(json); } catch (error) { console.error(error); } } fetchData();await 키워드는 Promise가 fulfilled될 때까지 async 함수의 실행을 일시 중지합니다. 그리고 Promise가 fulfilled되면 해당 값을 반환하고, 다음 문장으로 진행합니다.
- await는 Promise를 기다리는 동안 실행을 일시 중지하지만, 다른 비동기 처리가 가능한 작업은 계속 진행될 수 있습니다. 따라서 await를 사용하면서도 다른 비동기 작업을 동시에 수행할 수 있습니다.
- async 함수 내에서 발생하는 에러는 try/catch 블록으로 처리할 수 있습니다. catch 블록에서 에러를 적절히 처리하거나, 상위 호출자에게 에러를 전달할 수 있습니다.
FYI
async/await는 비동기 코드를 보다 직관적이고 간결하게 작성할 수 있는 문법이지만, 모든 상황에 적합하지는 않을 수 있습니다. 예를 들어, 병렬적으로 여러 개의 비동기 작업을 실행해야 하거나, 작업 간에 의존성이 없는 경우에는 Promise.all()과 같은 Promise 체이닝 방식이 더 적합할 수 있습니다.
또한 비동기 처리의 함수를 구현 할때는 성공했을때의 로직만 생각 하지말고 , 성공 하지못할때의 예외처리를 항상 잘해줘야할것같습니다.
then , catch 문 , async await 을 사용 할땐 try catch문으로 항상 감싸주는 습관을 들여야 할것같습니다.
포스팅 한 내용의 지적은 항상 환영합니다. 감사합니다
'front-end > Javascript&Typescript' 카테고리의 다른 글
[결국은 자바스크립트]3.싱글스레드 자바스크립트의 비동기처리 및 용어정리 (0) 2023.07.04 [결국은 자바스크립트] 2. 클로저와 스코프 Closure & Scope (0) 2023.06.25 [Typescript] Interface 와 Type 차이를 정확히 알아보자 (0) 2023.05.10 [Lodash] findIndex 개념정리. (0) 2022.11.09 [JavaScript] 비동기처리 , 동기처리 정리 (0) 2022.10.07