프론트엔드/실전 리액트 프로그래밍

[스터디 with 실전 리액트 프로그래밍] 7편 - 프로미스

Junheehee 2022. 8. 6. 15:06

실전 리액트 프로그래밍

 

프로미스(promise)는 비동기 '상태'를 값으로 다룰 수 있는 객체이다.

프로미스를 사용하면 비동기 프로그래밍을 할 때 동기 프로그래밍 방식으로 코드를 작성할 수 있다.

동기? 비동기? 어려운 단어가 많이 나온다... 프로미스를 공부하기 전에 먼저 동기, 비동기에 대해 알아보자.

 

 

 

 

자바스크립트에서 동기, 비동기

 

동기, 비동기 개념에 대해 내가 쓴 블로그 글이다!!

 

Sync vs Async / Blocking vs Non-blocking

sync(동기)와 async(비동기)에 대하여 공부를 하다 보니 항상 따라오는게 있었다. 바로 blocking과 non-blocking이었다. 이 sync와 blocking, async와 non-blocking은 의미와 상황이 자주 혼동되지만 서로 다른 개..

junhee-hee.tistory.com

 

 

아래 영상에선 싱글 스레드인 자바스크립트 런타임이 왜 비동기처럼 작동하는지 잘 설명하고 있다.

동기, 비동기 뿐만 아니라 자바스크립트가 어떻게 실행되는지에 대한 기초적인 내용도 나와있으니 꼭꼭 봐보자!!(한글 자막도 있다)

 

 

자바스크립트의 이벤트 루프

 

 

 

 

 

 

콜백(callback) 패턴

 

프로미스가 등장하기 전에는 비동기 프로그래밍을 할 때, 콜백 패턴이 주로 사용되었다.

콜백 패턴은 비동기 함수가 끝난 뒤 그 결과값을 처리하기 위한 함수를 비동기 함수의 인자로 넣어주는 것이다.

이 때 비동기 함수에 인자로 넣어주는 함수를 콜백 함수라고 한다.

콜백 패턴은 큰 단점이 있다.

만약 비동기 처리를 중첩시킬 일이 있다면, 콜백 패턴도 중첩이 되는데 코드가 어엄청 복잡해진다.(이것을 콜백 지옥이라 부른다)

 

// 콜백 지옥
function1(() => {
	function2(() => {
		function3(() => {
			function4(() => {
				function5()
			})
		})
	})
})

 

 

 

 

프로미스(promise)

 

개발자들은 콜백 지옥에서 벗어나고 싶었고,

비동기 프로그래밍을 간결하게 해주는 프로미스가 등장하였다.

 

앞서 프로미스는 비동기 '상태'를 값으로 다룬다고 했다.

프로미스가 다루는 상태는 세가지다.

 

1. 대기중(pending): 결과를 기다리는 중.
2. 이행됨(fulfilled): 수행이 정상적으로 끝났고, 값을 가지고 있음.
3. 거부됨(rejected): 수행이 비정상적으로 끝났고, 값을 가지고 있음.

이 때 이행됨과 거부됨 상태를 처리됨(settled) 상태라고 부른다.

 

프로미스를 생성할 때는 new 키워드를 사용한다.

생상자에 입력되는 함수는 resolve와 reject를 매개변수로 갖는다.

 

// new 키워드로 프로미스 생성
const p1 = new Promise((resolve, reject) => {
    // 작업 수행
    resolve(response) // 작업이 정상적으로 수행되면 
    reject('error message') // 작업이 비정상적으로 이행된다면
})

 

 

 

처음 생성된 프로미스는 대기중 상태이다.

 

대기중

 

new 키워드로 프로미스를 생성하는 순간, 생성자의 입력함수가 실행된다.

위 사진을 보면 '작업수행'이 먼저 출력된 것을 확인할 수 있다.

 

 

작업 수행이 정상적으로 실행되었다면 첫번째 매개변수인 resolve를 호출하면 되고, 프로미스의 상태는 이행됨이 된다.

앞서 이행됨 상태는 결과값을 가진다고 했는데, 원하는 결과값을 resolve에 넘겨주면 된다.

아래 사진에서 resolve에 'success'를 넣어주니 프로미스의 결과값이 'success'가 된 것을 확인할 수 있다.

 

이행됨

 

 

비정상적으로 실행되었다면 두번째 매개변수인 reject를 호출하면 되고, 프로미스의 상태는 거부됨이 된다.

보통 에러 메시지를 rejected 프로미스의 값으로 넣는다.

 

거부됨

 

 

 

 

then

 

콜백 패턴에서 콜백 함수를 인자로 넘겨주면서 비동기 프로그래밍을 했다면, 프로미스를 사용할 때는 어떻게 비동기 프로그래밍을 할까?

then 메서드를 사용하면 된다.

 

프로미스가 처리됨 상태(이행됨이나 거부됨)가 되면 then 메서드가 실행된다.

이행됨이라면 then의 첫번째 인자 함수가 호출되고, 거부됨이라면 두번째 인자 함수가 호출된다.

 

아래의 사진에서 프로미스는 대기중 상태이므로 then 메서드가 호출되지 않았다.

 

대기중일 때 then

 

 

반면에 이행됨이거나 거부됨 상태라면 then 메서드가 호출되었다.

 

처리됨일 때 then

 

 

then의 중요한 특징은 항상 프로미스를 반환한다는 것이다.

이를 이용하면 then 메서드를 연속적으로 이용할 수 있다.

만약 프로미스가 아닌 값을 반환한다면, then 메소드는 그 반환값을 가진 이행됨 상태 프로미스를 반환한다.

 

then의 연속 사용

 

 

위의 콜백 지옥 코드와 비교해보자.

비동기 함수를 연속적으로 사용했지만 코드가 훨씬 직관적이고 깔끔하다. 콜백 지옥에서 성공적으로 벗어났다!!!

 

위 사진을 보면, then의 인자로 이행됨일 때 호출할 함수 하나만 전달하였다.

만약 프로미스가 거부됨 상태라면 호출할 두번째 인자 함수가 있는 then까지 이동한다.

거부됨 상태일 때 호출할 함수가 없다면 아래 사진처럼 에러가 발생한다.

 

에러 처리를 하지 않는다면

 

따라서 에러 처리는 필수적이다.

위에서 나온대로 then에 두번째 인자를 이용하여 에러 처리를 해도 되지만, 다른 방법도 있다.

 

 

 

 

catch

 

catch는 거부됨 상태인 프로미스를 처리하기 위한 메서드다.

 

// then으로 에러 처리
sampleFunction()
	.then((res) => console.log(res), (err) => console.log(err))


// catch로 에러 처리
sampleFunction()
	.then((res) => console.log(res))
	.catch((err) => console.log(err))

 

then과 비슷하게 사용하면 된다.

catch도 then처럼 promise를 반환하기 때문에, catch 이후에 then을 붙이는 등 상황에 맞게 연속적으로 사용하면 된다.

 

 

 

 

finally

 

finally는 프로미스 체인 마지막에 이용된다.

then, catch와 달리 사용된 프로미스를 그대로 반환하기 때문에, 데이터를 건드리지 않고 추가 작업이 필요한 경우에 사용하면 된다.

아래는 예시다.

 

sampleFunction()
	.then(res => {
		// ...
	})
	.catch(err => {
		// ...
	})
	.finally(() => {
		// ...
	})

 

 

 

 

 

Promise.all

 

Promise.all은 여러개의 프로미스를 병렬로 처리할 때 사용하는 함수다.

then 메서드를 이용하면 프로미스를 직렬적으로 처리할 수 있지만,

비동기 함수간의 의존성이 없다면 직렬적으로 처리하는 것보단 병렬젹으로 처리하는 것이 효율적이다.

이 때 Promise.all 함수를 사용한다.

 

Promise.all([sampleFunction1(), sampleFunction2()])
	.then(([res1, res2]) => {
		// ...
	})

 

인자로 들어오는 프로미스들이 모두 처리됨 상태가 되어야 Promise.all이 처리됨 상태가 된다.

만약 하나라도 거부됨 상태이면 Promise.all도 거부됨 상태가 된다.

 

비동기 함수들간의 의존성은 없고, 비동기 함수들이 다 끝나고 한번에 결과 처리를 하고 싶을 때 사용해도 좋을 것 같다.

 

 

 

 

 

Promise.race

 

Promise.race는 이름에서 알 수 있듯이 여러 개의 프로미스 중에서 가장 빨리 처리된 프로미스를 반환하는 함수다.

즉 하나의 프로미스라도 처리됨 상태가 되면 Promise.race도 처리됨 상태가 된다.

Promise.all 과 반대다.

 

Promise.race([sampleFunction1(), sampleFunction2()])
	.then(([res1, res2]) => {
		// ...
	})

 

 

Promise.race를 이용하면 비동기 함수의 실행 시간에 대한 조건을 추가할 수 있다.

 

Promise.race([
	sampleFunction(),
        new Promise((resolve, reject) => setTimeout(reject, 5000))
])
	.then(res => {
		// sampleFunction이 5초 안에 완료되면 호출됨
	})
	.catch(err => {
		// sampleFunction이 5초 안에 완료되지 못하면
		// Promise.race도 거부됨 상태가 되니까 catch 메서드가 호출됨
	})

 

 

 

 

 

 

그밖에 then은 항상 프로미스를 반환한다고 했고, 처리됨 프로미스는 결과값을 포함한다고 했다.

때문에 then 안에 return을 생략한다면 undefined가 프로미스의 결과값으로 반환된다.

 

return의 필요성

 

첫번째 사진은 arrow function의 특성상 중괄호를 생략하면 return이 자동으로 되어 프로미스에 결과값이 잘 들어있는 모습이다.

두번째 사진은 return이 없으므로 undefined가 결과값으로 된 모습이다.

따라서 결과값이 필요한 경우 return을 빼먹지 않도록 주의하자.

 

 

 

 

async

 

프로미스와 then, catch 메서드로 가독성이 좋은 비동기 프로그래밍을 할 수 있게 되었지만, 개발자들은 더 원했다.

더 간결한, 더 좋은 방법이 없을까 하던 와중 async와 await가 추가되었다.

 

 

then 메서드와 마찬가지로 async 함수도 프로미스를 반환한다.

 

async 함수의 프로미스 반환

 

 

프로미스를 반환하기 때문에 then, catch 메서드도 사용 가능하다.

 

async 이후 then 사용

 

 

만약 async 함수 내에서 에러가 발생하면 거부됨 상태의 프로미스가 반환된다.

 

async 안에서 에러 발생

 

 

 

 

await

 

async 함수의 가장 중요한 특징은 await 키워드를 사용할 수 있다는 점이다.

await 키워드는 async 함수내에서만 사용이 가능하며,

await 오른쪽에 프로미스를 입력하면 그 프로미스가 처리됨 상태가 될 때까지 기다린다.

 

await으로 비동기 처리

 

위 사진에서, await 키워드 덕분에 프로미스 p1이 생성되고 처리됨 상태가 되기 전까지 자바스크립트 런타임은 다음 실행을 기다렸다.

5초후 처리됨 상태가 되니 '세번째'를 출력하였다.

 

await을 이용하면 위처럼 비동기 함수여도 동기적으로 작동해서, 코드를 순차적으로 실행시킬 수 있다.

 

 

await을 사용하면 가독성이 좋아진다는 장점이 있다.

 

// then 메서드로 비동기 처리
const asyncFunction = () => {
	getData1()
		.then(data => {
			return getData2(data)
		})
		.then(data => {
			return getData3(data)
		})
		.then(data => {
			console.log(data)
		})
}


// async, await으로 비동기 처리
const asyncFunction = async () => {
	const data1 = await getData1()
	const data2 = await getData2(data1)
	const data3 = await getData3(date2)
	console.log(data)
}

 

위 코드는 똑같이 getData1, getData2, getData3 이후 결과값을 출력하는 비동기 함수이다.

then 메서드 대신 await을 사용하니 코드가 훨씬 간결해졌다.

 

 

async 함수에서 에러 처리는 try catch 구문을 주로 이용한다.

 

const asyncFunction = async () => {
	try {
		await getData()
	} catch (err) {
		// err 처리
	}
}

 

 

 

 

setTimeout

 

헷갈리면 안되는게 setTimeout은 비동기적으로 작동하지만, 프로미스를 반환하지는 않는다.

 

Combination of async function + await + setTimeout

I am trying to use the new async features and I hope solving my problem will help others in the future. This is my code which is working: async function asyncGenerator() { // other code ...

stackoverflow.com

 

setTimeout의 반환값을 출력해보면 Promise가 아닌 timeoutId가 출력한다.

 

setTImeout 반환값 출력

 

 

JavaScript setTimeout

In this tutorial, you will learn how to use the JavaScript setTimeout() that sets a timer and executes a callback function after the timer expires.

www.javascripttutorial.net

 

 

따라서 setTimeout에 await을 사용하고 싶다면, 아래처럼 해야한다.

 

// 원하는대로 x
await setTimeout(sampleFunction, 3000)

// 요렇게 해야함
await new Promise((resolve, reject) =>
	setTimeout(() => resolve(() => sampleFunction()), 3000)
)

 

 

 

 

어려워서 미루던 프로미스를 공부하니 너무 뿌듯하다.

더 열심히 공부하자!!