지난 글 ‘한 번에 이해하는 자바스크립트 비동기 프로그래밍’에서 자바스크립트의 비동기 처리 개념을 살펴보았습니다. 자바스크립트는 싱글 스레드 언어이기 때문에, 동기적으로 실행되는 코드가 시간이 오래 걸리면 전체 코드의 흐름이 멈추는 단점이 있습니다. 이를 해결하기 위해 setTimeout과 같은 비동기 메서드를 활용하여 코드 실행을 지연시키지 않으면서도, 원하는 시점에 작업을 수행할 수 있었습니다. 또한 프로미스 객체를 사용하면 비동기 작업의 상태를 관리하고, 성공 및 실패 처리를 더 직관적으로 할 수 있다는 점도 배웠죠. 하지만 프로미스를 제대로 활용하지 못하면 여전히 복잡한 코드가 생성될 수 있습니다. 특히 지난 글에서 살짝 언급했던 ‘콜백 지옥(Callback Hell)’ 현상은 코드 가독성을 심각하게 저하시킬 수 있습니다. 이번 글에서는 콜백 지옥이 무엇인지, 그리고 이를 어떻게 해결할 수 있는지, 그리고 더 깔끔한 비동기 코드 작성을 위해 async/await을 어떻게 활용하는지에 대해 알아보겠습니다. 또한 실전에서 가장 많이 활용되는 API 호출과 비동기 처리의 관계에 대해서도 함께 살펴봅시다. 콜백 지옥과 프로미스 객체비동기 처리를 하다 보면 여러 개의 비동기 작업을 순차적으로 실행해야 할 때가 있습니다. 이러한 작업을 단순히 콜백 함수로 연결하면 코드의 복잡도가 급격히 증가할 수 있습니다. 특히 중첩된 콜백이 많아질수록 가독성이 떨어지고 유지보수가 어려워지는데요. 이를 콜백 지옥이라고 부릅니다. 콜백 지옥을 나타내는 유명한 그림이 있는데요, 콜백 지옥이 발생한 코드의 모양을 나타내는 그림입니다. <출처: 하나몬님 블로그> 1) 콜백 지옥이란?콜백 지옥이란 비동기 함수 내에서 콜백 함수가 연쇄적으로 중첩되는 현상을 말합니다. 비동기 작업이 여러 개 연속으로 실행되어야 하는 경우, 다음과 같은 코드가 만들어질 수 있습니다. setTimeout(() => { console.log("1초 후 실행"); setTimeout(() => { console.log("2초 후 실행"); setTimeout(() => { console.log("3초 후 실행"); }, 1000); }, 1000); }, 1000); 이처럼 여러 개의 setTimeout이 중첩되면 코드의 가독성이 떨어지고 유지보수가 어려워집니다. 특히 API 호출처럼 여러 개의 비동기 작업이 순차적으로 실행되어야 하는 경우, 이러한 콜백 지옥 문제는 더욱 심각해집니다. 2) 프로미스 객체를 사용한 콜백 지옥 해결콜백 지옥을 해결하는 방법 중 하나가 바로 프로미스 객체를 사용하는 것입니다. 콜백 지옥의 해결방법에 대해 배워보기 전에, 먼저 저번 시간에 살펴봤던 프로미스 객체에 대해 다시 한 번 더 살펴보도록 하겠습니다. 프로미스는 비동기 작업의 완료 또는 실패를 처리하기 위한 객체입니다. 프로미스 객체는 new Promise()를 통해 생성하며, 생성자 함수는 다음과 같이 resolve와 reject를 인수로 받는 콜백 함수를 요구했죠. const myPromise = new Promise((resolve, reject) => { // 비동기 작업 수행 if (/* 작업 성공 */) { resolve('성공 결과'); } else { reject('실패 이유'); } }); resolve 함수는 작업이 성공했을 때 호출하는 함수로, 프로미스의 상태를 이행(fulfilled)으로 변경하고, reject 함수는 작업이 실패했을 때 호출하며, 프로미스의 상태를 거부, 실패(rejected)로 변경하는 함수였습니다. 앞서 살펴봤듯이, 여러 비동기 작업을 순차적으로 실행해야 할 때 콜백 함수를 중첩하면, 코드가 복잡해져서 가독성이 떨어지는 콜백 지옥이 발생할 수 있습니다. 이러한 문제는 프로미스 객체를 사용해, 콜백을 중첩하지 않고 then 메서드를 사용한 then() 체이닝을 통해 가독성을 개선하고, 콜백 지옥 문제를 해결할 수 있습니다. const delay = (ms) => { return new Promise(resolve => setTimeout(resolve, ms)); } delay(1000) .then(() => { console.log("1초 후 실행"); return delay(1000); }) .then(() => { console.log("2초 후 실행"); return delay(1000); }) .then(() => { console.log("3초 후 실행"); }); 이처럼 프로미스의 then 메서드를 체이닝, 즉 then 메서드를 연결해서 사용하면 비동기 작업을 순차적으로 처리하면서도 코드의 중첩을 피할 수 있습니다. 그리고 이러한 방식을 ‘프로미스 체이닝’ 이라고 부르기도 합니다. 프로미스 체이닝을 사용하면 콜백 지옥을 피할 수 있을 뿐만 아니라, 비동기 코드의 실행 흐름을 직관적으로 표현할 수 있다는 장점이 있습니다. async와 await콜백 지옥을 해결하는 또 다른 강력한 방법은 바로 async/await 문법을 사용하는 방법입니다. 이를 활용하면 비동기 코드를 마치 동기 코드처럼 작성할 수 있어, 가독성이 아주 크게 향상되는 것을 볼 수 있습니다. 1) async와 await의 역할async와 await은 자바스크립트에서 비동기 코드를 보다 직관적으로 작성할 수 있도록 도와주는 키워드입니다. async 키워드를 함수 앞에 붙이면 해당 함수는 항상 프로미스 객체를 반환하며, 함수의 내부에서는 await 키워드를 사용할 수 있습니다. <출처: 작가> await은 특정 비동기 작업이 완료될 때까지 기다렸다가, 그 결과를 반환하는 역할을 합니다. 이를 통해 비동기 작업을 동기 코드처럼 순차적으로 실행할 수 있게 해줍니다. 기존의 then 메서드 체이닝보다 가독성이 뛰어나고 유지보수가 용이하다는 장점이 있기 때문에, 비동기 작업을 할 때 더 많이 사용되는 문법입니다. 2) 사용 방법그럼 async와 await의 사용법에 대해 살펴봅시다. 먼저 아래의 코드를 예시로 들어보겠습니다. const delay = (ms) => { return new Promise((resolve) => { setTimeout(() => { resolve('3초가 지났습니다.'); }, ms); }); }; const start = () => { delay(3000).then((res) => { console.log(res); }); }; start(); 위의 코드는 간단한 비동기 코드인데요. delay 함수의 내부에는 프로미스 객체를 반환하는 코드를 작성하고, resolve 함수에 “3초가 지났습니다”를 전달했습니다. 그리고 start 함수에서는 delay 함수를 호출하고, delayTime으로 3초를 전달한 다음, resolve에 전달된 값을 출력하는 코드를 작성했습니다. 코드를 실행하면 start 함수가 호출되어, 3초 후에 알맞은 문장이 출력되겠죠. 자바스크립트에서 async는 비동기 작업을 처리할 때 사용되는 키워드로, 비동기 작업을 포함하고 있기 때문에 프로미스 객체를 반환하는 함수에 작성하는 키워드입니다. async를 작성하면 코드를 훨씬 직관적으로 해석할 수 있는데요. async를 사용해서 위에 작성한 코드를 변경해 보겠습니다. const delay = (ms) => { return new Promise((resolve) => { setTimeout(() => { resolve('3초가 지났습니다.'); }, ms); }); }; const start = async () => { delay(3000).then((res) => { console.log(res); }); }; start(); async는 비동기를 수행할 함수의 이름 오른쪽에 작성합니다. 어떠한 함수에 async 키워드를 작성하면, 해당 함수는 항상 자동으로 프로미스 객체를 반환하게 됩니다. <출처: 작가> 실제로 async 키워드가 붙은 start 함수에 마우스를 올려보면, 위와 같이 프로미스 객체를 반환하는 함수라는 것을 알 수 있습니다. 그럼 이제 await 키워드에 대해서도 알아봐야겠죠. await은 async 키워드가 작성된 함수의 내부에서 사용하는 키워드로, await 키워드가 포함된 코드가 실행되면 해당 작업이 종료될 때까지 프로그램의 실행이 중단된다는 특징이 있습니다. 그럼 await 코드를 사용해 위의 코드를 수정해 볼까요? const delay = (ms) => { return new Promise((resolve) => { setTimeout(() => { resolve('3초가 지났습니다.'); }, ms); }); }; const start = async () => { let result = await delay(3000); console.log(result); }; start(); async 키워드가 붙어있는 start 함수의 내부에 result 변수를 새로 생성한 다음, 해당 변수에 delay 함수를 호출한 결괏값을 할당해 주었습니다. 그리고 이 delay 함수의 앞에 ‘await’ 키워드를 작성했습니다. await은 “기다리다”라는 뜻으로, 프로미스 객체가 처리될 때까지 기다리면서, 그동안은 함수의 실행을 중단하는 역할을 합니다. 따라서 start 함수를 호출하면, delay 함수의 프로미스 객체의 처리가 완료될 때까지 잠시 중단되었다가, 프로미스 객체의 처리가 완료되면 코드가 순서대로 다시 실행됩니다. 이후 result 변수에 delay 함수의 반환값인, 실행 완료된 프로미스 객체가 할당되어 3초 후에 ‘3초가 지났습니다’ 문장이 출력됩니다. 이렇게 await 키워드는 프로미스 객체가 처리될 때까지 함수의 실행을 기다리게 만드는 역할을 합니다. await 키워드를 사용해서 코드를 작성하면, 프로미스 객체의 then 메서드를 사용해서 코드를 작성하는 것보다 훨씬 가독성이 좋고 편리하게 작성할 수 있습니다. 다만, await 키워드는 프로미스 객체를 반환하는 함수의 내부에서만, 즉 async 키워드가 붙어있는 함수의 내부에서만 사용할 수 있다는 점을 주의해야 합니다. 3) 에러 핸들링그럼 이제 오류가 발생했을 때 어떻게 처리해야 하는지도 알아보겠습니다. 에러 핸들링 방법은 아주 간단한데요. async와 await을 사용한 비동기 처리에서는 try/catch 문을 사용해, 에러를 처리할 수 있습니다. async 함수인 start 함수의 내부에 try/catch를 사용해, 코드를 한번 작성해 보겠습니다. const delay = (ms) => { return new Promise((resolve) => { setTimeout(() => { resolve('3초가 지났습니다.'); }, ms); }); }; const start = async () => { try { let result = await delay(3000); console.log(result); } catch (error) { console.log(error); } }; start(); 이렇게 try/catch문을 사용해 작성한 코드의 실행 순서를 살펴보면, 먼저 try 블록 안에 작성된 코드가 실행되고, 해당 코드에서 에러가 발생했다면 바로 아래에 작성된 catch 블록 내부의 코드가 실행됩니다. 발견된 에러는 catch에 전달된 error 객체에 저장되기 때문에, 에러 발생 시 이 error 객체를 사용하면 어떤 에러가 발생했는지를 출력할 수 있습니다. 이처럼 try/catch문을 활용하면, 비동기 코드에서 발생하는 오류를 더욱 안전하게 처리할 수 있습니다. API 호출자바스크립트에서 비동기 작업을 처리하는 가장 대표적인 예시가 바로 API 호출이죠. 서버와 데이터를 주고받는 과정에서는 네트워크 지연을 포함한 다양한 변수들이 발생할 수 있기 때문에, 이러한 작업은 비동기적으로 처리해야 합니다. 이 글의 마지막에서는 우리가 비동기를 배운 목적인 클라이언트와 서버 간의 통신 원리, API 호출 방식, 그리고 에러 핸들링 방법에 대해 알아보겠습니다. 1) 클라이언트와 서버 통신웹 브라우저(클라이언트)는 네트워크를 통해 서버와 통신하며, 서버는 데이터베이스에서 필요한 정보를 가져와 클라이언트에게 전달합니다. 이 과정은 우리가 커피숍에서 커피를 주문하는 과정과 유사하게 이해할 수 있습니다. <출처: 인프런, ‘한 번에 끝내는 자바스크립트’> 커피숍에서 손님이 바리스타에게 커피를 주문하면, 바리스타는 창고에서 원두를 찾아 커피를 만들어 제공하는 것처럼, 클라이언트가 서버에 데이터를 요청하면, 서버는 데이터베이스에서 필요한 정보를 찾아 응답합니다. <출처: 인프런, ‘한 번에 끝내는 자바스크립트’> 즉, 클라이언트는 직접 데이터베이스에 접근하지 않고, 서버에게 요청을 보내 원하는 데이터를 받아오는 구조입니다. 이를 통해 보안과 효율성을 유지하면서 데이터를 주고받을 수 있습니다. 2) API 호출과 비동기자바스크립트에서는 fetch라는 내장 함수를 사용해서 API를 호출할 수 있습니다. fetch 메서드의 괄호 안에 API 주소를 입력하면 해당 API를 호출합니다. let response = fetch('https://jsonplaceholder.typicode.com/users'); console.log(response);; 다음과 같이 코드를 작성하고 response 값을 출력하면, 우리가 앞서 살펴봤던 프로미스 객체가 출력되는 것을 볼 수 있습니다. 이렇게 state 프로퍼티가 fulfilled인 프로미스 객체가 출력됩니다. <출처: 작가> fetch 메서드를 사용해 API를 호출하면 이렇게 프로미스 객체를 반환하므로, then 메서드를 활용해 결과 값을 출력할 수 있겠지만, 우리는 더욱 편리한 비동기 처리 방식은 async/await을 배웠기 때문에, 바로 적용해 보겠습니다. async/await을 사용하기 위해 API를 호출해 값을 받아오는 기능의 함수인 getData 함수를 생성해 봅시다. 그리고 JSON 형식의 데이터인 API 호출 결과를, 자바스크립트에서 사용할 수 있도록 json() 이라는 메서드를 활용해 변경해 줄게요. const getData = async () => { let response = await fetch('https://jsonplaceholder.typicode.com/users'); let data = await response.json(); console.log(data); }; getData(); json() 메서드를 사용해서 response에 담긴 값을, 자바스크립트가 활용할 수 있는 객체의 형태로 변환했습니다. 이때 fetch 함수는 비동기적으로 처리되기 때문에, API 호출이 완전히 끝난 이후에 response 변수를 객체로 변환할 수 있도록 await 키워드를 작성해 줍니다. <출처: 작가> 코드를 실행하면 실제로 위와 같은 값들이 배열에 담겨 출력되는 것을 볼 수 있고, 이렇게 async/await을 사용하면 자바스크립트에서 API 호출과 같은 비동기 작업을 아주 간단하게 처리할 수 있습니다. 3) 에러 핸들링API 호출은 필요한 데이터를 전달받기 위해 데이터를 요청하는 작업입니다. 데이터를 요청할 때는 네트워크 오류 또는 인터넷 속도 등의 다양한 이유로, 데이터 요청에 실패할 수 있다는 점을 주의해야 하는데요. 이렇게 성공할 수도, 실패할 수도 있는 비동기 작업은 항상 에러를 처리할 수 있도록 해야 합니다. 앞서 살펴봤던 try/catch문을 사용해 에러를 처리해 볼게요. const getData = async () => { try { let response = await fetch('https://jsonplaceholder.typicode.com/users'); let data = await response.json(); console.log(data); } catch (err) { console.log(err); } }; getData(); 이렇게 try/catch문을 사용하면 매우 쉽게 에러를 처리할 수 있습니다. 실제로 fetch 메서드 내부에 있는 API 주소를 임의로 변경해 볼까요? ‘https://jsonplaceholder1313.typicode.com/users’라는 이상한 주소로 변경하고 코드를 실행하면, catch문을 통해 에러 메세지가 알맞게 출력되는 것까지 확인해볼 수 있습니다. 이렇게 API 호출을 async와 await을 사용해서 비동기로 처리하면, 가독성이 좋은 코드를 작성할 수 있습니다. 이로써 코드의 실행 흐름과 역할을 직관적으로 할 수 있고, 에러 처리 또한 편리하게 할 수 있다는 장점이 있습니다. 마치며지금까지 자바스크립트의 비동기에 대한 내용을 1, 2편으로 나눠 다뤄보았습니다. 자바스크립트가 비동기 처리를 필요로 하는 이유와 setTimeout, 프로미스 객체, 콜백 지옥, async/await, 그리고 API 호출까지 다양한 비동기 처리 방식에 대해 배웠는데요. 비동기 프로그래밍을 올바르게 활용하면 사용자의 경험을 개선하고, 보다 안정적인 애플리케이션을 개발할 수 있습니다. 비동기는 매우 중요한 개념이고 자주 활용되므로, 직접 코드를 작성해 보며 결괏값을 확인해 보는 과정이 필요합니다. 이번 글에서 사용했던 사이트를 활용하면, 무료로 여러 API를 호출해 볼 수 있는데요. 다양한 예제를 통해 비동기 처리를 연습하고 싶으신 분들은 참고해 보시길 바랍니다.<참고>javascript.info{JSON} Placeholder웹 프론트엔드를 위한 자바스크립트 첫 걸음 ©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.