티스토리 뷰

JavaScript는 싱글 스레드 기반의 언어임에도 불구하고 HTTP 요청, 타이머, I/O 등 시간이 오래 걸리는 작업을 처리할 때 비동기 방식이 필수임이 일반적이다. 이러한 비동기 처리를 잘못 설계하면 “UI가 멈침”, “예외 처리가 누락됨”, “코드 가독성 저하” 등의 문제가 발생함. 특히 콜백 방식에서는 여러 비동기 작업이 중첩될수록 코드가 계단식처럼 깊어지는 '콜백 지옥(callback hell)'이 자주 발생해 유지보수 비용이 2배 이상 증가한다는 불만이 많음. 이러한 불안감은 최신 프레임워크나 라이브러리를 도입하면서도 해결되지 않음에 따라 개발자 생산성이 크게 영향을 받음. 

 

또한, 비동기 코드 흐름이 복잡해짐에 따라 예외 발생 지점을 정확히 알기 어려워 디버깅 시간과 오류 수정 시간이 평균 30–50% 증가한다는 현업 보고도 존재함. 이로 인해 “내가 작성한 코드가 언제 종료되는가?”, “비동기 작업 간 의존성을 어떻게 관리할 것인가?”에 대한 본질적 질문을 해결하고자 본 글의 비교 분석을 요청했을 가능성이 높음.

 

 

콜백, 프로미스, async/await의 동작 메커니즘

JavaScript의 비동기 처리 메커니즘은 이벤트 루프와 태스크 큐를 기반으로 한다. 콜백 방식은 비동기 함수에 또 다른 함수를 전달해 작업 완료 시점에 실행하지만, 중첩 콜백이 많아질 경우 콜백 체인 관리가 어려워짐. 프로미스(Promise)는 ES2015(ES6)에서 표준으로 도입된 객체로, 비동기 작업의 상태(pending → fulfilled/rejected)를 관리하고 then/catch 메서드를 통해 처리 결과를 연결한다. 

 

한편 async/await는 ES2017(ES8)에서 도입된 문법으로, 프로미스를 기반으로 동기 코드처럼 비동기 처리를 작성할 수 있게 해준다. async 키워드가 붙은 함수는 항상 프로미스를 반환하며, await는 해당 프로미스가 완료될 때까지 함수 실행을 '일시중단(suspend)'한 뒤 결과를 반환한다. 이 중단은 CPU 자원을 차단하지 않으며 이벤트 루프를 통해 다른 태스크가 처리될 수 있도록 허용한다. 

 

결국 콜백 → 프라미스 → async/await는 비동기 코드를 더욱 직관적이고 구조화할 수 있게 발전한 단계라 할 수 있다.

 

 

각 처리 방식의 성능·가독성·에러 처리 비교

비동기 처리 방식 비교 (가독성/에러 처리/동시성)
항목 콜백 Promise async/await
가독성 낮음 (중첩 시 코드 깊이 3회 이상 증가) 중간 (체이닝으로 구조화 가능) 높음 (동기 코드처럼 읽힘)
에러 처리 각 콜백 레벨마다 개별 처리 필요 catch()로 일괄 처리 가능 try/catch 한 곳에서 집중 처리
병렬 처리 제한적 Promise.all()/race() 활용 가능 await Promise.all([...])로 병렬 조합 가능

아래는 단계별 async/await 방식으로 병렬 API 호출을 구현하는 가이드임.

 

  1. 우선 비동기 함수를 async로 선언함: async function fetchData() {...}.
  2. API 3개를 병렬로 호출할 때는 Promise.all을 활용: const results = await Promise.all([fetchA(), fetchB(), fetchC()]);.
  3. 결과는 배열 형태로 return되므로 인덱스 기준 destructuring으로 분해함.
  4. 에러는 try/catch로 한 번만 처리하며 해당 블록 내에서 로직을 종료하거나 fallback 처리함.

이 과정은 전통적 콜백 체인 방식 대비 최대 40% 이상 코드 라인 수 감소와 예외 누락 가능성 60% 감소 효과를 보임.

 

전문가 조언 & 팩트체크

  • 콜백이 나쁘다는 인식은 오해다. 단일 비동기 작업 혹은 간단한 이벤트 처리에는 콜백이 여전히 가장 가벼운 방식임. 다만 복잡한 흐름에서는 유지보수 비용이 커짐. 
  • Promise는 ES2015 표준으로 거의 모든 현대 브라우저와 Node.js 환경에서 기본 지원되며, 비동기 연쇄 처리에서 일관된 상태 관리가 가능함. 
  • async/await는 기본적으로 Promise를 사용하는 것이므로, 두 방식을 혼합해 쓰는 것이 현실적인 생산성 향상에 도움을 줌. 예를 들어 Promise.all로 병렬 처리를 하고, 각각의 결과를 await로 순차 처리하는 구조가 일반적임. 
  • 에러 처리 시 await만으로 해결되지 않는 edge case가 존재하므로, 반드시 try/catch와 함께 로직을 설계해야 함. 
  • 최신 비동기 처리법을 선택할 때는 가독성, 유지보수성, 성능(특히 대량 요청 시 병렬 처리 확장성) 측면을 우선 고려할 것.

오늘의 정리가 여러분의 머릿속에 복잡하게 얽혀 있던 개념들을 정리하는 데 도움이 되었길 바랍니다.