brunch

You can make anything
by writing

C.S.Lewis

by 한상훈 May 29. 2022

[고급 자바스크립트] 프라미스 깊게 이해하기

Promise의 유틸리티 메서드와 한계



프라미스(Promise)는 자바스크립트를 사용하는 개발자라면 숙명처럼 쓸 수밖에 없는 비동기 처리 방식입니다. 그러나 프라미스를 비롯해 콜백 함수, async-await와 같은 비동기 처리는 동기적 처리와 다르게 결괏값을 예측하기 어렵게 만들고, 사이드 이펙트를 양산하는데 큰 기여를 합니다.


1. 이것이 프라미스인가?


프라미스를 잘 쓰기 위해서는 먼저 사용하고자 하는 함수, 메서드 등이 제대로 된 프라미스를 리턴하는지 아는 것부터 시작합니다.


const p = new Promise();
console.log(p instanceof Promise);


와 같은 형태로 쓰면 타입 검증이 가능할 것 같지만, 몇 가지 이유에서 불확실한 방법입니다. 프라미스의 값들이 들어오는 경로에 따라서 위의 방법이 제대로 인스턴스 체크를 해주지 못할 수 있고, 또한 라이브러리/모듈에 따라서 ES6의 프라미스를 사용하지 않고 구현한 프라미스에 대해서도 검증하지 못합니다. 이러한 특징 때문에 개발자들은 프라미스를 검증하기 위해 데너블(Thenable)이란 방식을 사용해 검증했습니다.


2. 데너블

모든 프라미스에는 .then 이라는 메서드를 가집니다. 완전한 방법은 아니지만 ‘데너블 값은 프라미스다.’라고 간주한 셈입니다. 데너블을 사용한 프라미스 검증은 대부분의 케이스에서 큰 문제가 없습니다. 그러나 프라미스의 .then과 다르게 동작하면서 .then이라는 메서드를 가지는 함수를 제공하는 라이브러리/모듈 등에 대해서는 사용할 수 없는 문제가 있습니다. 이러한 이유에서 특정 라이브러리/모듈은 프라미스 호환이 제대로 이뤄지지 않는다는 경고문이 포함되어 있습니다.


결과적으로 프라미스의 값이 들어오고, 사용함에 있어서 검증단에서 에러가 생길 여지가 있고, 데너블 이외에도 프라미스를 검증하기 위해 쓸 수 있는 방법은 더 있습니다.(‘브랜딩’ 또는 ‘안티 브랜딩’ 방법 등) 자바스크립트 개발자라면 외부에서 들어오는 프라미스를 가져오거나 직접 라이브러리를 만들어 배포할 때 해당 내용에 주의를 기울여야 합니다.


3. 프라미스의 체이닝 이슈

예를 들어 우리가 특정 API를 호출하여 값을 얻는다고 가정해봅시다. 해당 값을 얻고 나면 그 값을 이용해 그다음 프라미스가 호출되고, 이 과정이 반복되는 체이닝 형태라 가정합시다. 그러면 최초에 호출하는 특정 API의 값이 개발자가 원치 않는 값이 들어온다면, 이후에 연결된 프라미스들을 따라가며 시스템에 영향을 줄 수 있습니다. 인풋 값이 예상치 못한 값이 들어와 문제를 만드는 것은 타입과 인터페이스를 정의하여 해결할 수 있습니다. 그러나 타입과 인터페이스로 인풋 값을 검증하여 에러를 리턴하는 경우에 캐치되지 않은 에러로 인한 문제가 발생할 수 있습니다.


프라미스는 기본적으로 약속의 귀결(resolve)/버림(reject)이라는 2개의 리턴으로 구성되어 있습니다. 만약 거절이 발생하는 경우 이후 프라미스는 모두 거절을 따라 에러가 로직의 하단까지 그대로 내려오게 됩니다. 만약 프라미스 연쇄 과정에서 제대로된 .catch가 이뤄지지 않다면 시스템은 에러를 캐칭 하지 못하게 되고, 멈추는 경우가 발생할 수 있습니다. 시스템적으로 캐치되지 않은 에러를 리턴하여 시스템을 멈추는 것이 좋을 수도 있으나, 무중단이 보장되어야 하는 시스템이어야 한다면 연쇄적인 프라미스 체인을 만들 때 에러를 명확하게 캐칭 해야 합니다.


4. 언제나 .catch()를 해야 하는가?

그렇다면 이쯤에서 프라미스를 위험천만한 방식으로 생각해 ‘.catch로 언제나 보호하고, 꼼꼼히 에러의 propagation에 대응해야 하는가?’라고 생각할 수 있습니다. 이에 대해 상황에 따라 현명하게 에러를 처리하는 게 필요하다고 봅니다. 제 경우에는 .catch()로 리턴함으로 생기는 에러를 위쪽에서 받았을 때 추가적인 로직이 필요하다면 resolve로 값을 보내되 에러에 대한 명시를 하여 에러 자체는 리턴하지 않고, 시스템에 에러라는 사실을 전달하는 방식을 쓰기도 합니다.(이 방법을 추천하는 것은 아닙니다.) 가장 좋은 것은 언제나 .catch를 사용해 적절하게 버림(reject)의 시나리오에 대응하는 것이겠지만 코드를 짜다보면 .catch에 대한 시나리오를 만드는 것 자체가 일이 되기도 합니다. 그렇다고 방치하자니 캐치되지 않고, 떠도는 버림 프라미스는 어떻게 하면 좋을까요.


5. 조용한 에러 만들기

이러한 고민에 몇 가지 방법이 있습니다. 프라미스는 캐칭이 표기되어있지 않은 경우라면 콘솔을 통해 에러를 알리도록 설계되어 있습니다. 만약 감지가 되기 전까지 버려지는 프라미스의 버림 상태를 유지해주어야 하는 경우라면 .defer()를 사용해 프라미스의 에러 알림 기능을 꺼줄 수 있습니다. 그러나 이렇게 처리된 에러들은 사실 제대로 해결된 에러가 아닌 조용히 만든 에러이기 때문에 개발자들의 콘솔은 깨끗하게 해 줄 수 있어도 본질적인 해결책이 될 순 없습니다. 우리가 해야 할 일은 무엇일까요? 가장 좋은 시나리오라면 우리가 원하는 시나리오(resolve)만큼 이외에 에러 상황에 대한 시나리오도 고민해 동등하게 개발해주어야 할 겁니다.(결국 더 많은 일을 해야 한다는 말이군요!) 그러나 위에서 제가 서술한 여러 이슈들을 똑똑하게 해결할 방법을 찾고 있다면 프라미스 추상화 라이브러리들을 사용/공부해보시면 답을 찾아가실 수 있으리라 생각합니다.


6. 시퀀스 시나리오

위에서 우리는 프라미스의 여러 최악의 상황에 대해서 서술해봤습니다. 낮은 단계로 개발하는 수준에서는 사실 마주할 일이 별로 없거나, 깊이가 깊게 연쇄 작용이 발생하는 코드를 쓸 일이 적어 문제가 되지는 않는 내용이긴 합니다. 이번엔 프라미스가 가지고 있는 시퀀스 시나리오를 설명해보겠습니다. 프라미스의 유틸리티 메서드를 통해 코드의 양을 줄이고, 동시성이 존재하는 프라미스의 관리 방법의 아이디어를 얻을 수 있을 겁니다.


7. Promise.all([])

비동기 처리를 다중으로 동시에 진행해야 하고, 이 과정이 모두 끝나야 다음 로직이 되어야 할 때 우리는 Promise.all을 통해 과정을 쉽게 만들 수 있습니다. 예를 들어 서버에 이미지를 다중으로 저장해야 하는 상황이거나 또는 동시에 여러 데이터베이스에 액세스해 카피본을 업로드해야 한다거나, 여러 항목에 대하여 비동기 동시 계산을 수행하여 리턴되는 값을 합산해야 하는 경우 등 다양한 시나리오에 대응할 수 있습니다. 그러나 Promise.all은 모든 항목에 대해서 하나라도 버림(reject)이 발생하는 경우에 대해서 버림 시나리오로 가져가기 때문에 하나라도 문제가 생기는 시나리오에 대해서 처리하기에는 곤란한 경우가 있습니다. 이때 사용하면 좋은 것은…


8. Promise.allSettled([])

입니다. Promise.allSettled()를 사용하게 되면 각 프라미스의 이행 여부를 결과로 리턴 받기 때문에 문제가 생긴 프라미스에 대하여 개별 처리가 가능하다는 강점이 있습니다. 물론 .Promiseall()을 사용할 때보다 코드량이나 시나리오에 대한 대응은 더 복잡하지만 에러를 꼼꼼히 처리할 수 있다는 강점이 분명합니다.


9. Promise.race([])

프라미스 간에 타임아웃 경합을 벌여야 하는 경우에는 Promise.race()를 사용합니다. 동시에 여러 프라미스를 호출하고, 이 중 가장 빠르게 완료된 프라미스에 대해서만 리턴합니다. 당연하지만 레이스를 사용하게 되면 즉시 이행 형태의 프라미스를 넣어선 아무런 효과가 없습니다. 그렇기 때문에 외부 라이브러리에 있는 프라미스를 레이스에 사용하는 경우라면 외부 라이브러리에서 즉시 이행되는 프라미스인지 아닌지 검증할 필요가 있습니다.


10. 단일 귀결의 한계점

앞서 프라미스의 여러 측면과 다중 프라미스를 처리하는 방식에 대해 설명했습니다. 그럼에도 불구하고 프라미스의 한계점이 하나 더 있는데 바로 단일 귀결입니다. 프라미스는 결과적으로 이행된 결괏값 또는 버림 결괏값(에러)을 리턴합니다. 만약 프라미스가 진행 중인 과정에서 개입을 해야 하거나, 원하는 데이터가 스트림 형태로 가져와야 한다면 프라미스를 사용할 수 있을까요? 또는 반복 요청에 대해서 비동기 로직이 완료되지 않은 상태에서 요청이 계속 들어올 때에 대해서도 제대로 처리하기 위해서 어려움이 있을 수 있습니다. 그러한 이유에서 자바스크립트 라이브러리 중에서 프라미스의 관찰(Observe)에 집중한 라이브러리들이 나타나고 있고, (RxJS) 복잡한 시스템을 다루는 자바스크립트 개발자일수록 추상화 처리에 대해 어려움을 겪기도 합니다.


11. 취소 불가 문제

또한 프라미스는 실행이 되면 취소를 할 수 없습니다. 단일 귀결을 향해 무조건 진행되므로 이를 프라미스 자체의 힘만으로는 다룰 수 없습니다. 이를 해결하기 위해 Promise.race를 사용해 일정 시간이 지나도 완료되지 않는 프라미스에 대해 취소되는 에러 핸들러를 넣는 방식도 있겠지만, 더 좋은 방법은 프라미스 자체를 수정 가능한 형태로 호출 취소하는 게 우리가 원하는 방향입니다. 그럼 자바스크립트 개발자들은 어떤 해결책을 가져왔을까요? 바로 완전 실행의 규칙을 따르지 않는 제너레이터라는 함수가 등장하게 됩니다.


웹 개발, 블록체인 컨트렉트 개발 문의:



매거진의 이전글 [고급 자바스크립트] 인터섹션 옵서버
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari