JS

[ JavaScript ] Promise

람연 2021. 8. 5. 01:08

JS는 싱글스레드 프로그래밍 언어이다. 싱글스레드는 한번에 하나의 일을 처리할 수 있는데, 이는 앞단의 작업이 끝날때 까지 어떤 작업도 수행할 수 없음을 의미한다. 이를 극복하기 위해 웹 브라우저를 포함한 다양한 JS 실행환경에서 비동기 방식으로 처리하기 위한 매커니즘을 제공한다. 그렇다면 JS를 동적으로 사용하기 위해서는 어떻게 해야할까?


비동기 처리 방식(Asynchronous)

비동기 처리 방식은 작업의 요청과 결과가 동시에 일어나지 않는 방식을 말한다. JS의 비동기 처리 방식은 아래와 같이 구현할 수 있다.

function func1() {
	setTimeout( () => {
		console.log( 'func1' )
	}, 10000 ); 
}

function func2() {
	setTimeout( () => {
		console.log( 'func2' )
	}, 1000 ); 
}

func1()
func2()

위 코드에서 func1을 먼저 호출하고, func2를 호출했다. 하지만 func2가 먼저 결과를 출력한다. 이와 같이, 요청과 결과가 동시에 일어나지 않는 방식이 바로 비동기 처리 방식이다.

 

동기 처리 방식(Synchronous)

동기 처리 방식은 작업과 요청의 결과가 동시에 일어나는 방식을 말한다. JS의 동기 처리 방식은 아래와 같이 구현할 수 있다.

// Callback 지옥도
function func1() {
	setTimeout( () => {
		console.log( 'func1' )
		setTimeout( () => {
			console.log( 'func2' )
		}, 1000 ); 
	}, 10000 ); 
}

func1()

위 코드는 실행 후 10초 뒤 func1을 출력하고 1초 뒤 func2를 출력한다. 이처럼 동기 처리 방식을 구현하기 위해 callback 함수를 사용했다. func1의 안쪽에서 callback을 갖는 setTimeout을 정의했다. callback의 안쪽에서 다시 callback을 갖는 setTimeout을 정의했다. 이와 같은 패턴을 콜백지옥 이라 한다. callback 뎁스가 많아질수록 코드 가독성이 떨어지며 유지보수 측면에서 좋지 않기 때문이다.


Promise

Promise는 콜백지옥을 개선하고자 제안된 패턴이며, ES6로 넘어오면서 정식 스펙으로 포함되었다. Promise는 비동기적으로 실행하는 작업의 결과를 갖는 객체로 상태 값과 다양한 메소드를 갖는다. 여기서 Promise가 객체라는 점이 가장 큰 장점이자 특징이 된다.

var promiseFunc = new Promise( 이행 함수 ( resolve, reject ) )

Promise는 객체이기 때문에, 생성자를 가지며 인자로 이행 함수를 받는다. 이때 이행 함수의 성공과 실패를 반환하기 위해 resolve, reject 메소드를 인자로 받는다. 

 

resolve, reject

Promise는 비동기 작업의 처리 결과를 resolve, reject메소드를 통해 핸들링 할 수 있다. 성공 시 resolve, 실패 시 reject 메소드를 사용하며 성공 실패에 대해 적절한 데이터를 함께 반환할 수 있다.

var promiseFunc = function func1 ( param ) {
	return new Promise( function ( resolve, reject ) {
		setTimeout( () => {
			console.log( "func1" );

			if ( param ) {
				resolve( { RS_CD: "S", RS_MSG: "Success" } );
			} else {
				reject( { RS_CD: "E", RS_MSG: "Error" } );
			}

		}, 10000 );
	} )
}
promiseFunc( true )

위 소스는 promiseFunc에 true인자 값을 전달해 resolve를 발생시켰고 적절한 데이터를 반환했다.

 

then

Promise에서 작업 처리의 결과를 resolve, reject메소드를 통해 반환하면 then메소드를 통해 성공과 실패 각각의 callback을 구현할 수 있다. then메소드는 resolve, reject 시 넘겨준 데이터를 인자로 받는 두개의 함수를 갖는다. 이때 실패 시 함수는 생략이 가능하다. 특이한 점은 then메소드는 Promise자신을 같이 반환한다.

Promise.then( 성공 시 함수(resolve 반환 데이터), 실패 시 함수(reject 반환 데이터) )

Promise.then은 아래와 같이 구현할 수 있다.

var promiseFunc = function func1 ( param ) {
	return new Promise( function ( resolve, reject ) {
		setTimeout( () => {
			console.log( "func1" );

			if ( param ) {
				resolve( { RS_CD: "S", RS_MSG: "Success" } );
			} else {
				reject( { RS_CD: "E", RS_MSG: "Error" } );
			}

		}, 10000 );
	} )
}
promiseFunc( true ).then( function success ( res ) {
	console.log( "resolve RS_CD:", res.RS_CD + " " + res.RS_MSG ) 
}, function error ( res ) {
	console.log( "reject RS_CD:", res.RS_CD + " " + res.RS_MSG ) 
} )

 

catch

catch는 Promise에서 반환된 결과 중 reject만을 처리한다. 따라서 catch는 reject(실패)시 로직만을 구현할 수 있다. 특이한 점은 catch메소드는 then과 마찬가지로 Promise자신을 같이 반환한다.

Promise.catch( 실패 시 함수(reject 반환 데이터) )

Promise.catch는 아래와 같이 구현할 수 있다.

var promiseFunc = function func1 ( param ) {
	return new Promise( function ( resolve, reject ) {
		setTimeout( () => {
			console.log( "func1" );

			if ( param ) {
				resolve( { RS_CD: "S", RS_MSG: "Success" } );
			} else {
				reject( { RS_CD: "E", RS_MSG: "Error" } );
			}

		}, 10000 );
	} )
}
promiseFunc( false ).catch( function error ( res ) {
	console.log( "reject RS_CD:", res.RS_CD + " " + res.RS_MSG ) 
} )

 

Promise Chanining

웹 개발 초기에는 버튼 클릭 이벤트 콜백 등 간단한 기능이 주를 이뤘지만, 웹 성능이 발전함에 따라 웹에서 복잡한 기능을 구현했고 이는 웹 개발자에게 콜백지옥을 선사했다. 콜백지옥을 개선하고자 ES6부터 Promise는 공식 스펙으로 등록되었고 Promise Chanining을 통해 콜백지옥을 개선할 수 있었다.

 

우선 아래 코드를 보자.

// Callback 지옥도
try {
	function func1() {
		try {
        	setTimeout( () => {
				console.log( 'func1' )
				setTimeout( () => {
					console.log( 'func2' )
				}, 1000 ); 
			}, 10000 ); 
		} catch ( err ) {
			throw new exception( "뎁스 1에서 발생한 에러" );
		}
	}
  
	func1()
} catch ( err ) {
	console.log( err.message )
}

위 소스에 콜백은 두 개 뿐이지만 충분히 보기 어렵다. 또, 각 구간에서 에러가 발생하면 어디에서 발생한 에러인지 찾기가 어렵다. 이제 Promise로 구현한 아래 소스를 보자.

var promiseFunc = function finc1( param ) {
	return new Promise( function ( resolve, reject ) {
		setTimeout( () => {
			console.log( "func1" );

			if ( param ) {
				resolve( { RS_CD: "S", RS_MSG: "Success" } );
			} else {
				reject( { RS_CD: "E", RS_MSG: "Error" } );
			}

		}, 10000 );
	} )
}
promiseFunc( true ).then( res => { 
	console.log( "then RS_CD:", res.RS_CD + " " + res.RS_MSG ) 
} ).then( () => {
	console.log( "Promise Chanining 1" )
} ).then( () => {
	console.log( "Promise Chanining 2" )
} ).then( () => {
	console.log( "Promise Chanining 3" )
} ).catch( err => {
	console.log( "then RS_CD:", err.RS_CD + " " + err.RS_MSG ) 
} )

기존 디자인 패턴보다 코드 가독성이 뛰어나며, 유지보수 측면에서도 수월하다. 이와 같은 구조가 가능한 이유는 then, catch가 Promise자신을 반환하기 때문이다. then에서 Promise객체 자신을 반환했기 때문에, 그 뒤에 다시 then을 사용할 수 있으며 에러가 발생해도 최상위 catch까지 에러가 전달되어 예외처리가 용이하다. 이러한 구조를 Promise Chanining이라 한다.

 

Promise 상태 값

Promise는 앞서 설명한 개념들을 통해 비동기 작업의 처리 결과를 객체의 형태로 관리한다. 이때 Promise 객체는 작업의 상태에 따라 각각 상태 값을 갖는다.

 

pending

작업의 성공 또는 실패가 되기 이전 상태로, 실행 중 또는 대기 중을 의미한다.

var promiseFunc = function func1 ( param ) {
	return new Promise( function ( resolve, reject ) {
		setTimeout( () => {
			console.log( "func1" );

			if ( param ) {
				resolve( { RS_CD: "S", RS_MSG: "Success" } );
			} else {
				reject( { RS_CD: "E", RS_MSG: "Error" } );
			}

		}, 10000 );
	} )
}
promiseFunc( true ).then( res => {
	console.log( "resolve RS_CD:", res.RS_CD + " " + res.RS_MSG ) 
} ).catch( err => {
	console.log( "reject RS_CD:", err.RS_CD + " " + err.RS_MSG ) 
} )

Promise 객체는 실행 후 resolve 또는 reject를 만나기 전까지 pending 상태를 갖는다. 위 소스에서 약 10초간 pending 상태를 갖게 될 것이다.

 

fulfilled

작업이 정상적으로 실행되어 종료된 상태이다. 

var promiseFunc = function func1 ( param ) {
	return new Promise( function ( resolve, reject ) {
		setTimeout( () => {
			console.log( "func1" );

			if ( param ) {
				resolve( { RS_CD: "S", RS_MSG: "Success" } );
			} else {
				reject( { RS_CD: "E", RS_MSG: "Error" } );
			}

		}, 10000 );
	} )
}
promiseFunc( true ).then( res => {
	console.log( "resolve RS_CD:", res.RS_CD + " " + res.RS_MSG ) 
} ).catch( err => {
	console.log( "reject RS_CD:", err.RS_CD + " " + err.RS_MSG ) 
} )

Promise 객체는 실행 후 resolve를 만나면 성공적인 종료를 의미하며 fulfilled 상태를 갖는다.

 

rejected

작업이 실패된 상태이다.

var promiseFunc = function func1 ( param ) {
	return new Promise( function ( resolve, reject ) {
		setTimeout( () => {
			console.log( "func1" );

			if ( param ) {
				resolve( { RS_CD: "S", RS_MSG: "Success" } );
			} else {
				reject( { RS_CD: "E", RS_MSG: "Error" } );
			}

		}, 10000 );
	} )
}
promiseFunc( false ).then( res => {
	console.log( "resolve RS_CD:", res.RS_CD + " " + res.RS_MSG ) 
} ).catch( err => {
	console.log( "reject RS_CD:", err.RS_CD + " " + err.RS_MSG ) 
} )

Promise 객체는 실행 후 reject를 만나면 작업의 실패를 의미하며 rejected 상태를 갖는다.

 

settled

작업의 성공, 실패 여부를 떠나 종료된 상태이다.

var promiseFunc = function func1 ( param ) {
	return new Promise( function ( resolve, reject ) {
		setTimeout( () => {
			console.log( "func1" );

			if ( param ) {
				resolve( { RS_CD: "S", RS_MSG: "Success" } );
			} else {
				reject( { RS_CD: "E", RS_MSG: "Error" } );
			}

		}, 10000 );
	} )
}
promiseFunc( false ).then( res => {
	console.log( "resolve RS_CD:", res.RS_CD + " " + res.RS_MSG ) 
} ).catch( err => {
	console.log( "reject RS_CD:", err.RS_CD + " " + err.RS_MSG ) 
} )

비동기 작업의 종료를 의미한다. 작업의 성공과 실패에 관계 없이 작업을 이행하고자 할때 등 간간히 사용하는 부분이 있다.