많은 작업들, 예를 들어 네트워크 요청들은 비동기적인 특성을 가집니다. 비동기 코드를 다루기 위한 가장 유용하고 강력한 도구 중 하나가 프로미스입니다. 이 안내서에서는 자바스크립트 프로미스에 대해 배우고, 이를 어떻게 사용하는지 알아볼 것입니다.

  • 목차
  • 프로미스란 무엇인가요?
  • 다른 비동기 패턴과 프로미스 비교하기
  • 프로미스 만들기
  • 프로미스의 결과 얻기
  • then으로 에러 다루기
  • 프로미스 체이닝
  • 즉시 이행되거나 거부되는 프로미스 만들기
  • async와 await 사용하기
  • 프로미스 안티 패턴
  • 요약

프로미스란 무엇인가요?

프로미스가 무엇인지 알아보기 위해 시작해봅시다.

간단히 말해서, 프로미스는 비동기 작업을 대표하는 객체입니다. 이 객체는 작업이 성공했을 때, 또는 실패했을 때를 알려줄 수 있습니다.

프로미스 기반 API를 호출하면, 함수는 결국 작업 결과를 제공할 프로미스 객체를 반환합니다.

프로미스 상태
프로미스는 그 생애 동안 세 가지 상태 중 하나가 될 수 있습니다:

  • 보류(Pending): 작업이 진행 중인 동안 프로미스는 보류 상태에 있습니다. 이는 아직 결과(또는 에러)를 기다리고 있는 미결 상태입니다.
  • 이행(Fulfilled): 프로미스를 반환한 비동기 작업이 성공적으로 완료되었습니다. 프로미스는 작업 결과인 값으로 이행됩니다.
  • 거부(Rejected): 비동기 작업이 실패했을 경우, 프로미스는 거부되었다고 합니다. 프로미스는 이유와 함께 거부됩니다. 이는 일반적으로 Error 객체이지만, 프로미스는 단순한 숫자나 문자열을 포함한 어떤 값으로도 거부될 수 있습니다!

프로미스는 보류 상태에서 시작하여 그 결과에 따라 이행 또는 거부 상태로 전환됩니다. 프로미스는 이행 또는 거부 상태에 도달하면 결정되었다고 말합니다.

물론, 비동기 작업이 완료될 것이라는 보장은 없습니다. 프로미스가 영원히 보류 상태에 있을 수도 있습니다만, 이는 비동기 작업의 코드에 있는 버그 때문일 것입니다.

다른 비동기 패턴과 프로미스 비교하기

프로미스는 자바스크립트의 다른 비동기 패턴들과 약간 다르게 동작합니다. 프로미스에 대해 더 깊이 들어가기 전에, 이러한 다른 기술들과 프로미스를 간단히 비교해봅시다.

콜백 함수
콜백 함수는 다른 함수에 전달하는 함수입니다. 호출한 함수가 작업을 마치면 결과와 함께 콜백 함수를 실행합니다.

getUsers라는 함수가 있고 이 함수는 사용자의 배열을 얻기 위해 네트워크 요청을 합니다. getUsers에 콜백 함수를 전달할 수 있으며, 네트워크 요청이 완료되면 사용자 배열을 가지고 호출됩니다:

//콜백 함수의 예
console.log('Preparing to get users');
getUsers(users => {
  console.log('Got users:', users);
});
console.log('Users request sent');


위 코드는 우선 “Preparing to get users”을 출력합니다. 그런 다음 getUsers를 호출하여 네트워크 요청을 시작합니다. 그러나 자바스크립트는 요청이 완료될 때까지 기다리지 않습니다. 대신, 바로 다음 console.log 문을 실행합니다.

나중에 사용자가 로드되면 콜백이 실행되며 “Users request sent”이 출력됩니다.

일부 콜백 기반 API, 예를 들어 많은 Node.js API는 에러 우선 콜백을 사용합니다. 이 콜백 함수는 두 개의 인자를 받습니다. 첫 번째 인자는 오류이고, 두 번째는 결과입니다.

적절하게, 이들 중 하나만 작업 결과에 따라 값을 가지게 됩니다. 이것은 이행과 거부된 프로미스 상태와 유사합니다.

문제는 콜백 API의 중첩입니다. 만약 연속해서 여러 비동기 호출을 해야 한다면, 함수 호출과 콜백이 중첩될 것입니다.

파일을 읽고, 그 파일에서 어떤 데이터를 처리한 다음, 새로운 파일을 작성하는 것을 생각해 봅시다. 이 세 가지 작업은 모두 비동기적이며 가상의 콜백 기반 API를 사용합니다.

//중첩된 콜백 순서
readFile('sourceData.json', data => {
	processData(data, result => {
		writeFile(result, 'processedData.json', () => {
			console.log('Done processing');
		});
	});
});

에러 처리를 함께 상상한다면 더욱 복잡해집니다. 이 함수들이 에러 우선 콜백을 사용했다고 가정해봅시다:

//중첩된 에러 우선 콜백 순서
readFile('sourceData.json', (error, data) => {
	if (error) {
		console.error('Error reading file:', error);
		return;
	}
	
	processData(data, (error, result) => {
		if (error) {
			console.error('Error processing data:', error);
			return;
		}
		
		writeFile(result, 'processedData.json', error => {
			if (error) {
				console.error('Error writing file:', error);
				return;
			}
			
			console.log('Done processing');
		});
	});
});

콜백 함수는 현대 API에서 직접적인 비동기 메커니즘으로는 일반적으로 사용되지 않지만, 곧 보게 될 것처럼, 콜백은 프로미스 같은 다른 유형의 비동기 도구들의 기반입니다.

이벤트

이벤트는 순간순간 여러분이 수신하고 반응할 수 있는 것입니다. 자바스크립트의 일부 객체는 이벤트 발행자로, 이들에게 이벤트 리스너를 등록할 수 있습니다.

DOM에서는 많은 요소들이 EventTarget 인터페이스를 구현하고 addEventListener 및 removeEventListener 메소드를 제공합니다.

주어진 유형의 이벤트는 한 번 이상 발생할 수 있습니다. 예를 들어, 버튼의 클릭 이벤트를 수신할 수 있습니다:

//버튼의 클릭 수신
myButton.addEventListener('click', () => {
   console.log('button was clicked!'); 
});

버튼이 클릭될 때마다, “button was clicked!”라는 텍스트가 콘솔에 출력됩니다.

addEventListener 자체는 콜백 함수를 받아들입니다. 이벤트가 발생할 때마다, 해당 콜백이 실행됩니다.

객체는 여러 유형의 이벤트를 발행할 수 있습니다. 이미지 객체를 고려해봅시다. 지정된 URL의 이미지가 성공적으로 로드되면 load 이벤트가 트리거됩니다. 만일 오류가 발생하면, 이 이벤트는 트리거되지 않고 대신 error 이벤트가 트리거됩니다.

//이미지의 load와 error 이벤트 수신
myImage.addEventListener('load', () => {
    console.log('Image was loaded');
});

myImage.addEventListener('error', error => {
   console.error('Image failed to load:', error); 
});

이미지가 이벤트 리스너를 추가하기 전에 이미 로드를 완료했다고 가정하면 어떻게 될까요? 아무 일도 일어나지 않습니다! 이벤트 기반 API의 한 가지 단점은 이벤트가 발생한 후에 이벤트 리스너를 추가하면, 콜백이 실행되지 않는 것입니다. 이것은 결국, 버튼에 클릭 리스너를 추가할 때 모든 지나간 클릭 이벤트를 받고 싶지 않을 것이기 때문에 말이 됩니다.

이제 콜백과 이벤트에 대해 살펴보았으니, 프로미스에 대해 좀 더 자세히 알아보겠습니다.

프로미스 생성 방법

새로운 키워드와 프로미스 생성자를 사용하여 프로미스를 만들 수 있습니다. 프로미스 생성자는 두 개의 인자, 즉 resolve와 reject를 가지는 콜백 함수를 인자로 받습니다. 이들 각각의 인자들은 프로미스에 의해 제공된 함수로, 프로미스를 이행한 상태 또는 거부된 상태로 전환하는 데 사용됩니다.

콜백 안에서 비동기 작업을 수행합니다. 작업이 성공적으로 완료되면 최종 결과와 함께 resolve 함수를 호출합니다. 오류가 발생한 경우에는 오류와 함께 reject 함수를 호출합니다.

브라우저의 setTimeout 함수를 감싸는 프로미스를 생성하는 예는 다음과 같습니다:

//setTimeout을 프로미스로 감싸기
function wait(duration) {
	return new Promise(resolve => {
        setTimeout(resolve, duration);
    });
}

setTimeout에 첫 번째 인자로 resolve 함수가 전달됩니다. duration에 지정된 시간이 지나면 브라우저가 resolve 함수를 호출하여 프로미스를 이행하게 됩니다.

참고: 예제에서 resolve 함수가 호출되기 전 지연 시간은 함수에 전달된 duration보다 길 수 있습니다. 이는 setTimeout이 지정된 시간에 실행을 보장하지 않기 때문입니다.

주로 다른 API에서 반환되는 프로미스를 사용할 때가 많아서 실제로 자신의 프로미스를 직접 만들 필요가 거의 없다는 점을 명심하세요.

프로미스 결과 얻기

비동기 작업의 결과를 어떻게 얻나요? 이를 위해 프로미스 객체 자체에 then을 호출합니다. then은 콜백 함수를 인자로 받습니다. 프로미스가 이행되면, 결괏값과 함께 콜백이 실행됩니다.

예를 들어, 사용자 객체 목록을 비동기적으로 로드하는 getUsers라는 함수가 있다고 가정해봅시다. getUsers가 반환하는 프로미스에 then을 호출하여 사용자 목록을 가져올 수 있습니다.

//프로미스에 then 호출하기
getUsers()
  .then(users => {
    console.log('Got users:', users);
  });

이벤트 또는 콜백 기반 API와 마찬가지로, 결과를 기다리지 않고 코드가 계속 실행됩니다. 나중에 사용자가 로드되면, 콜백이 실행을 위해 예약됩니다.

console.log('Loading users');
getUsers()
  .then(users => {
    console.log('Got users:', users);
  });
console.log('Continuing on');

위 예제에서는 “Loading users”가 먼저 출력됩니다. 그 다음 출력되는 것은 “Continuing on”으로, getUsers 호출이 여전히 사용자를 로딩 중이기 때문입니다. 나중에 “Got users”가 출력됩니다.

then으로 오류 다루기

then을 사용하여 프로미스가 제공한 결과를 얻는 방법을 보았지만, 오류에 대해서는 어떻게 될까요? 사용자 목록을 불러오는 데 실패하면 어떻게 될까요?

then 함수는 사실 두 번째 인자도 받습니다. 이것은 오류 핸들러 콜백입니다. 프로미스가 거부되면 이 콜백이 거부 값을 가지고 실행됩니다.

getUsers()
  .then(users => {
    console.log('Got users:', users);
  }, error => {
    console.error('Failed to load users:', error);  
  });

프로미스가 이행되거나 거부될 수 있지만, 둘 다는 될 수 없으므로 이 두 콜백 함수 중 하나만 실행됩니다.

프로미스를 사용할 때 항상 오류를 처리하는 것이 중요합니다. 오류 콜백으로 처리되지 않는 프로미스 거부가 있으면 콘솔에 처리되지 않은 거부에 대한 예외가 발생하며, 이는 런타임에 사용자의 문제를 일으킬 수 있습니다.

프로미스 체이닝

여러 프로미스를 연속으로 작업해야 하는 경우는 어떻게 될까요? readFile, processData, writeFile 함수가 콜백 대신에 프로미스를 사용했다고 가정해 봅시다.

다음과 같은 시도를 할지도 모릅니다:

//중첩된 프로미스
readFile('sourceData.json')
  .then(data => {
    processData(data)
      .then(result => {
        writeFile(result, 'processedData.json')
          .then(() => {
            console.log('Done processing');
          });
      });
  });

이것은 보기에 좋지 않고, 콜백 접근법에서 가진 문제점이 그대로 남아있습니다. 다행히 더 나은 방법이 있습니다. 단순한 순서로 프로미스들을 체이닝할 수 있습니다.

이 작업이 어떻게 이루어지는지 더 깊이 살펴보기 위해, then이 어떻게 작동하는지 살펴봅시다. 핵심 아이디어는 다음과 같습니다: then 메소드는 다른 프로미스를 반환합니다. then 콜백에서 반환하는 값이면 이 새로운 프로미스의 이행된 값이 됩니다.

사용자 객체 배열로 이행되는 프로미스를 반환하는 getUsers 함수를 생각해봅시다. 이 프로미스에 then을 호출하고, 콜백에서 배열의 첫 번째 사용자(users[0])를 반환한다면:

getUsers().then(users => users[0]);

전체 표현식은 첫 번째 사용자 객체를 리턴하는 새로운 프로미스가 되기 때문입니다!

//then 핸들러에서 값 반환하기
getUsers()
  .then(users => users[0])
  .then(firstUser => {
    console.log('First user:', firstUser.username);
  });


프로미스를 반환하고 then을 호출한 다음 다른 값을 반환하는 이 과정, 다시 다른 프로미스로 이어지는 것을 체이닝이라고 합니다.

이 개념을 확장해 봅시다. then 핸들러에서 값 대신 다른 프로미스를 반환한다면 어떨까요? readFile과 processData가 모두 비동기 함수로 프로미스를 반환한다는 파일 처리 예제를 다시 생각해봅시다:

//then에서 다른 프로미스 반환하기
readFile('sourceData.json')
  .then(data => processData(data));

then 핸들러는 processData를 호출하면서 결과 프로미스를 반환합니다. 이전처럼, 이것은 새로운 프로미스를 반환합니다. 이 경우, 새 프로미스는 processData에 의해 반환된 프로미스가 이행될 때 이행되어, 같은 값을 제공해 줍니다. 위 예제의 코드는 처리된 데이터로 이행될 프로미스를 반환합니다.

필요한 최종 값을 얻을 때까지 여러 프로미스를 한 번에 체이닝할 수 있습니다:

readFile('sourceData.json')
  .then(data => processData(data))
  .then(result => writeFile(result, 'processedData.json'))
  .then(() => console.log('Done processing'));

위 예제에서 전체 표현식은 처리된 데이터가 파일에 쓰여지고 난 후에 이행될 프로미스로, “Done processing!”이 콘솔에 출력되고, 마지막 프로미스가 이행됩니다.

프로미스 체인에서 오류 처리하기

파일 처리 예제에서는 작업의 어느 단계에서든 오류가 발생할 수 있습니다. 프로미스 체인의 어느 단계에서든 오류를 처리할 수 있는데, 프로미스의 catch 메소드를 사용하면 됩니다.

//catch로 오류 처리하기
readFile('sourceData.json')
  .then(data => processData(data))
  .then(result => writeFile(result, 'processedData.json'))
  .then(() => console.log('Done processing'))
  .catch(error => console.log('Error while processing:', error));

체인에서 프로미스 중 하나가 거부되면 catch에 전달된 콜백 함수가 실행되고 체인의 나머지 부분은 건너뜁니다.

finally 사용하기

성공이든 실패이든 상관없이 수행해야 할 코드가 있을 수 있습니다. 예를 들어, 데이터베이스나 파일을 닫고 싶을 수 있습니다.

openDatabase()
  .then(data => processData(data))
  .catch(error => console.error('Error'))
  .finally(() => closeDatabase());

Promise.all 사용하기

프로미스 체인은 여러 작업을 순차적으로 실행할 수 있지만, 동시에 여러 작업을 수행하고 모두 완료될 때까지 기다리고 싶다면 어떨까요? Promise.all 메소드를 사용하면 이를 할 수 있습니다.

Promise.all은 프로미스 배열을 받고, 새로운 프로미스를 반환합니다. 이 프로미스는 입력 배열의 각 프로미스가 모두 이행되면 이행됩니다. 이행된 값은 입력 배열의 각 프로미스의 이행된 값들을 포함하는 배열입니다.

사용자의 프로필 데이터를 로드하는 loadUserProfile 함수와 사용자의 게시물을 로드하는 loadUserPosts 함수가 있는데, 둘 다 사용자 ID를 인자로 받습니다. 프로필과 게시물 목록 두 가지가 모두 필요한 renderUserPage 함수가 있습니다.

//Promise.all로 여러 프로미스 기다리기
const userId = 100;

const profilePromise = loadUserProfile(userId);
const postsPromise = loadUserPosts(userId);

Promise.all([profilePromise, postsPromise])
  .then(results => {
    const [profile, posts] = results;
    renderUserPage(profile, posts);
  });

오류가 발생하면 어떻게 될까요? Promise.all에 전달된 프로미스 중 하나가 오류로 거부되면, 결과 프로미스 역시 그 오류로 거부됩니다. 다른 프로미스가 이행된 값이 있어도 그 값은 손실됩니다.

Promise.allSettled 사용하기

Promise.allSettled 메소드는 Promise.all과 유사하게 작동합니다. 가장 큰 차이점은 Promise.allSettled에 의해 반환된 프로미스는 결코 거부되지 않는다는 것입니다.

대신, 입력 배열의 프로미스 순서에 해당하는 객체 배열로 이행됩니다. 각 객체는 status 속성을 가지고 있는데, 그것은 결과에 따라 “fulfilled”(이행됨) 또는 “rejected”(거부됨)입니다.

status가 “fulfilled”라면, 객체는 또한 프로미스의 이행된 값을 나타내는 value 속성을 가집니다. status가 “rejected”라면, 객체는 대신 프로미스가 거부된 이유 또는 오류를 나타내는 reason 속성을 가집니다.

다시 한 번 getUser 함수를 생각해 보세요. 이 함수는 사용자 ID를 입력 받고 그 ID를 가진 사용자로 이행되는 프로미스를 반환합니다. 성공적으로 로드된 모든 사용자를 확실히 얻기 위해 Promise.allSettled를 병렬로 사용할 수 있습니다.

//세 사용자를 로딩하려고 시도하면서 성공적으로 로드된 사용자들을 보여주기
Promise.allSettled([
  getUser(1),
  getUser(2),
  getUser(3)
]).then(results => {
   const users = results
     .filter(result => result.status === 'fulfilled')
     .map(result => result.value);
   console.log('Got users:', users);
});

주어진 사용자 ID 배열을 병렬로 로드하는 loadUsers라는 범용 함수를 만들 수 있습니다. 함수는 성공적으로 로드된 모든 사용자의 배열로 이행되는 프로미스를 반환합니다.

function getUsers(userIds) {
  return Promise.allSettled(userIds.map(id => getUser(id)))
    .then(results => {
      return results
        .filter(result => result.status === 'fulfilled')
        .map(result => result.value);
    });
}

실패한 요청을 제거하고 병렬로 여러 사용자를 로드하는 헬퍼 함수
그런 다음 사용자 ID 배열로 getUsers를 호출할 수 있습니다:

//getUsers 헬퍼 함수 사용하기
getUsers([1, 2, 3])
	.then(users => console.log('Got users:', users));

즉시 이행되거나 거부되는 Promise 생성 방법

때때로, 값을 이행된 Promise로 감싸고 싶을 수 있습니다. 예를 들어, Promise를 반환하는 비동기 함수가 있는 상황에서 기본적인 케이스로 값을 미리 알고 있고, 비동기 작업을 진행할 필요가 없을 때 입니다.

이를 수행하기 위해, Promise.resolve 메소드에 값을 전달하면 됩니다. 이는 지정한 값을 가지고 즉시 이행되는 Promise를 반환합니다:

//Promise.resolve 사용하기
Promise.resolve('hello')
  .then(result => {
    console.log(result); // prints "hello"
  });


이것은 다음과 거의 동일한 작업을 수행합니다:

new Promise(resolve => {
   resolve('hello'); 
}).then(result => {
    console.log(result); // also prints "hello"
});

API를 더 일관성 있게 만들기 위해서, 이런 상황에서 즉시 이행되는 Promise를 생성하여 반환할 수 있습니다. 이렇게 하면, 함수를 호출하는 코드가 어떠한 경우에도 Promise를 기대할 수 있습니다.

예를 들어, 이전에 정의된 getUsers 함수를 생각해 봅시다. 사용자 ID 배열이 비어있다면, 어차피 사용자가 로드되지 않으므로 빈 배열을 반환할 수 있습니다.

//getUsers 헬퍼 함수에 조기 리턴 추가하기
function getUsers(userIds) {
  // immediately return the empty array
  if (userIds.length === 0) {
    return Promise.resolve([]);
  }
    
  return Promise.allSettled(userIds.map(id => getUser(id)))
    .then(results => {
      return results
        .filter(result => result.status === 'fulfilled')
        .map(result => result.value);
    });
}


Promise.resolve를 사용하는 또 다른 이유는, 제공받은 값이 Promise인지 아닌지 확실하지 않아도 항상 Promise로 처리하고 싶을 때입니다.

어떤 값에도 안전하게 Promise.resolve를 호출할 수 있습니다. 이미 Promise였다면, 동일한 이행 또는 거부 값을 가진 또 다른 Promise를 얻게 됩니다. Promise가 아니었다면, 즉시 이행된 Promise로 감쌉니다.

이 방식의 이점은 다음과 같은 작업을 할 필요가 없다는 것입니다:

//어떤 것이 Promise인지에 따라 then 호출을 조건부로 하기
function getResult(result) {
  if (result.then) {
     result.then(value => {
         console.log('Result:', value);
     });
  } else {
      console.log('Result:', result);
  }
}


마찬가지로, Promise.reject를 사용하여 즉시 거부된 Promise를 만들 수 있습니다. 다시 getUsers 함수에 대한 언급을 하자면, 사용자 ID 배열이 null이거나 정의되지 않았거나 배열이 아닌 경우 즉시 거부하고 싶을 수 있습니다.

//인자가 유효한 배열이 아닌 경우 에러 반환하기
function getUsers(userIds) {
  if (userIds == null || !Array.isArray(userIds)) {
    return Promise.reject(new Error('User IDs must be an array'));
  }
    
  // 바로 빈 배열 반환
  if (userIds.length === 0) {
    return Promise.resolve([]);
  }
    
  return Promise.allSettled(userIds.map(id => getUser(id)))
    .then(results => {
      return results
        .filter(result => result.status === 'fulfilled')
        .map(result => result.value);
    });
}

Promise.race 사용 방법

Promise.all이나 Promise.allSettled처럼, Promise.race 정적 메서드도 프로미스 배열을 받고, 새로운 프로미스를 반환합니다. 이름에서 알 수 있듯이, 작동 방식에는 약간의 차이가 있습니다.

Promise.race가 반환하는 프로미스는 주어진 프로미스들 중 가장 먼저 이행되거나 거부되는 것을 기다립니다. 그리고나서 해당 프로미스는 그 값과 동일하게 이행되거나 거부됩니다. 이러한 상황이 발생하면 다른 프로미스들의 이행되거나 거부된 값들은 손실됩니다.

Promise.any 사용 방법

Promise.any는 Promise.race와 비슷하게 작동하나 하나의 주요 차이점이 있습니다 – Promise.race는 어떤 프로미스라도 이행되거나 거부되는 즉시 완료되는 반면, Promise.any는 첫 번째로 이행되는 프로미스를 기다립니다.

async와 await 사용 방법

async와 await는 프로미스를 다루는 것을 단순화하는 특별한 키워드입니다. 콜백 함수와 then이나 catch 호출의 필요성을 없앱니다. try-catch 블록과도 함께 작동합니다.

이렇게 작동합니다. 프로미스에 then을 호출하는 대신, await 키워드를 그 앞에 두고 프로미스를 기다립니다. 이는 함수의 실행을 프로미스가 이행될 때까지 효과적으로 “일시 중지”합니다.

표준 프로미스를 사용한 예시입니다:

//then으로 프로미스 기다리기
getUsers().then(users => {
    console.log('Got users:', users);
});


await 키워드를 사용한 동등한 코드입니다:

//await으로 프로미스 기다리기
const users = await getUsers();
console.log('Got users:', users);


프로미스 체인도 좀 더 깔끔해집니다:

//await으로 프로미스 체이닝하기
const data = await readFile('sourceData.json');
const result = await processData(data);
await writeFile(result, 'processedData.json');


await을 사용할 때마다, 기다리고 있는 프로미스가 이행될 때까지 함수의 나머지 실행이 중단됩니다. 만약 병렬로 실행되는 여러 프로미스를 기다리고 싶다면, Promise.all을 사용할 수 있습니다:

//await으로 Promise.all 사용하기
const users = await Promise.all([getUser(1), getUser(2), getUser(3)]);


await 키워드를 사용하기 위해서는 함수를 async 함수로 표시해야 합니다. 함수 앞에 async 키워드를 붙임으로써 이를 수행할 수 있습니다:

//함수를 async로 표시하기
async function processData(sourceFile, outputFile) {
  const data = await readFile(sourceFile);
  const result = await processData(data);
  writeFile(result, outputFile);
}

async 키워드를 추가하는 것은 함수에 대해 또 다른 중요한 영향을 미칩니다. Async 함수는 항상 암시적으로 프로미스를 반환합니다. async 함수에서 어떤 값을 반환하면, 실제로는 그 값을 이행하는 프로미스를 반환하게 됩니다.

async function add(a, b) {
  return a + b;   
}

add(2, 3).then(sum => {
   console.log('Sum is:', sum); 
});

async와 await를 사용한 에러 처리

프로미스가 이행될 때까지 기다리기 위해 await를 사용하지만 에러 처리는 어떻게 할까요? 만약 기다리는 프로미스가 거부되면 에러가 발생합니다. 따라서 에러를 처리하기 위해서는 try-catch 블록에 넣을 수 있습니다:

//try-catch 블록으로 에러 처리하기
try {
    const data = await readFile(sourceFile);
    const result = await processData(data);
    await writeFile(result, outputFile);
} catch (error) {
    console.error('Error occurred while processing:', error);
}

Promise 안티 패턴

불필요하게 새로운 프로미스 생성하기

가끔은 새로운 프로미스를 만들어야 할 때가 있습니다. 그러나 이미 API로부터 반환된 프로미스를 다루고 있다면, 자체적으로 프로미스를 만들 필요는 보통 없습니다:

//불필요한 프로미스 생성의 예
function getUsers() {
  return new Promise(resolve => {
     fetch('https://example.com/api/users')
       .then(result => result.json())
       .then(data => resolve(data))
  });
}

이 예시에서는 Fetch API를 감싸기 위해 새로운 프로미스를 만들고 있습니다. 이는 필요 없는 일입니다. 대신에, Fetch API에서 바로 프로미스 체인을 반환하세요:

//기존 Fetch 프로미스 사용하기
function getUsers() {
  return fetch('https://example.com/api/users')
    .then(result => result.json());
}

두 경우에서, getUsers를 호출하는 코드는 동일하게 보입니다:

getUsers()
  .then(users => console.log('Got users:', users))
  .catch(error => console.error('Error fetching users:', error));

에러를 삼키기

getUsers 함수의 다음과 같다고 해 봅시다.

function getUsers() {
    return fetch('https://example.com/api/users')
    	.then(result => result.json())
    	.catch(error => console.error('Error loading users:', error));
}


fetch 에러를 삼키기
이 getUsers 함수를 호출한다면 결과에 놀랄 수도 있습니다:

//getUsers 호출하기
getUsers()
  .then(users => console.log('Got users:', users))
  .catch(error => console.error('error:', error);)

여러분은 “error”가 출력될 것으로 예상할지도 모르지만, 실제로는 “Got users: undefined”가 출력됩니다. 이는 catch 호출이 에러를 “삼켜” catch 콜백의 반환값으로 이행되는 새로운 프로미스를 반환하기 때문입니다. 콘솔 에러는 undefined를 반환합니다(getUsers에서 “Error loading users” 로그 메시지는 여전히 볼 수 있지만 반환된 프로미스는 이행될 것이고, 거부되지는 않습니다).

getUsers 함수 내에서 에러를 포착하고 여전히 반환된 프로미스를 거부하고 싶다면, catch 핸들러는 거부된 프로미스를 반환해야 합니다. 이를 위하여 Promise.reject를 사용할 수 있습니다.

//에러 처리 후 거부된 프로미스 반환하기
function getUsers() {
  return fetch('https://example.com/api/users')
    .then(result => result.json())
    .catch(error => {
      console.error('Error loading users:', error);
      return Promise.reject(error);
    });
}


이제 “Error loading users” 메시지는 여전히 받지만, 반환된 프로미스도 에러와 함께 거부될 것입니다.

프로미스 중첩

프로미스 코드를 중첩하는 것을 피하십시오. 대신에, 평평하게 연결된 프로미스 체인을 사용해 보세요.

이런 식으로 사용하지 마세요:

readFile(sourceFile)
  .then(data => {
    processData(data)
      .then(result => {
        writeFile(result, outputFile)
          .then(() => console.log('done');
      });
  });


이렇게 사용하세요:

readFile(sourceFile)
  .then(data => processData(data))
  .then(result => writeFile(result, outputFile))
  .then(() => console.log('done'));

요약

프로미스로 작업할 때의 핵심 포인트들은 다음과 같습니다:

  • 프로미스는 보류 중, 이행됨, 거부됨 상태를 가질 수 있습니다
  • 이행되거나 거부되면 프로미스는 해결된 것으로 간주됩니다
  • 이행된 프로미스의 값을 얻기 위해 then을 사용하세요
  • 에러를 처리하기 위해 catch를 사용하세요
  • 성공 케이스나 에러 케이스에서 필요한 정리 로직을 수행하기 위해 finally를 사용하세요
  • 순차적으로 비동기 작업을 수행하기 위해 프로미스를 함께 체인하세요
  • 모든 주어진 프로미스가 이행되었을 때 이행되는 프로미스를 얻기 위해 Promise.all을 사용하세요, 또는 주어진 프로미스 중 하나라도 거부될 때 거부되는 경우도 있습니다
  • 모든 주어진 프로미스가 이행되었거나 거부되었을 때 이행되는 프로미스를 얻기 위해 Promise.allSettled를 사용하세요
  • 주어진 프로미스 중 가장 먼저 이행되거나 거부되는 경우에 이행되거나 거부되는 프로미스를 얻기 위해 Promise.race를 사용하세요
  • 주어진 프로미스 중 첫 번째로 이행되는 것을 얻기 위해 Promise.any를 사용하세요
  • 프로미스의 이행 값을 기다리기 위해 await 키워드를 사용하세요
  • await 키워드를 사용할 때 에러를 처리하기 위해 try-catch 블록을 사용하세요
  • await를 내부에서 사용하는 함수는 async 키워드를 사용해야 합니다

프로미스에 대한 깊은 이해를 위해 이 글을 읽어주셔서 감사합니다. 새로운 것을 배우셨기를 바랍니다!