지난 장에서는 콜백함수와, 콜백함수의 치명적인 단점인 콜백 헬에 대해서 알아보았습니다. 이번 장에서는 그의 대안으로 나온 Promise객체의 사용법에 대해 알아보겠습니다.

만약 저번 장을 읽지 않았다면 저번 장부터 읽고 오셔야 합니다. -> https://velog.io/@pp2lycee/%EC%BD%9C%EB%B0%B1%ED%95%A8%EC%88%98

| 콜백함수의 치명적인 단점

우선 콜백함수의 문제점에 대해 복습해보겠습니다.

세탁이 끝나면 건조기를 돌리고, 건조기가 끝나면 빨래를 개고, 빨래를 개면 빨래를 옷장에 넣는 로직을 작성한다고 가정해 봅시다. 그럼 아래와 같이 코드를 짤 수 있습니다.

function 빨래(){
	세탁(), function(){
    	건조(), function(){
        	빨래개기(), function(){
            	옷장에넣기()
            }
        }
    }
}

네, 아찔합니다. 가독성이 매우 떨어지죠? 사실 4가지 작업이 아닌 더 많은 작업이 다음과 같이 이루어질 수 있습니다. 이걸 우리는 callback hell(콜백헬)이라고 부릅니다.

인터넷에 콜백헬을 검색해 보시면 더욱 아찔한 예제들을 보실 수 있습니다.

이를 해결하기 위해 나온 새로운 es5, es6 문법으로 Promise와, async&await가 있습니다. 고로 다음 장에서는 Promise와 async&await를 배워보겠습니다.

TMI로 async&await에서 async는 asynchronus function, 즉 비동기 함수를 가리킵니다. 반면 동기 함수는 synchronus function이라고 불립니다.

| Promise 객체란?

비동기 처리를 하기 위한 새로운 방법입니다. 위 코드와 같은 아찔한 콜백헬을 다음과 같이 단순하게 만들 수 있습니다.

세탁() //A
.then((세탁의 결과물)=>건조(세탁의 결과물)) //B
.then((건조의 결과물)=>빨래개기(건조의 결과물)) //C
.then((개진 빨래) => 옷장에넣기(개진 빨래)) //D
.catch((애러)=>console.log(에러)) //E

.then()을 사용해서 줄줄이 엮어 나가는 방식입니다. 기존의 결과를 매개변수로 받으면서 계속해서 이어나갈 수 있습니다.

B에서는 A의 결과물을 매개변수로 받고 있고, C에서는 B까지의 결과물을 매개변수로 받고 있습니다. 이런 식으로 계속해서 이어나갈 수 있습니다.

A~D 과정 중 에러가 발생할 경우 E가 실행되어 에러를 출력합니다.

이렇게만 되면 참 좋겠지만 실전에서는 그렇지가 않습니다. 왜냐하면 이는 세탁()이라는 함수가 Promise 객체를 반환한다는 가정하에 만들어진 것이기 때문이죠. 다른 말로 표현하면 세탁()이라는 함수가 콜백함수가 아닌 Promise 함수일때나 가능한 구문입니다.

하지만 전에도 말했듯이 fs, http 등 nodeJs에서 기본적으로 제공되는 모듈(함수)들은 모두 콜백함수를 기반으로 합니다. 즉, 우리는 이러한 콜백함수를 Promise함수로 만들 필요가 있는 것이죠.

| fs.readFile

이를 설명하기 위해서 대표적인 nodeJs 기본 제공 모듈인 fs.readFile을 예로 들어보겠습니다.

const fs = require("fs"); //fs모듈을 import
fs.readFile("./읽고자 하는 파일의 경로", (err, data)=>{
	if (err) console.log(err);
    else console.log(data);   
}
)

먼저 fs모듈을 import합니다.

이후 fs.readFile함수를 실행합니다. 이 함수는 첫번째 인자로는 읽고자 하는 파일의 경로, 마지막 인자(여기서는 두번째 인자)로는 파일을 다 읽었을 때 실행될 콜백함수를 받습니다.

여기서 콜백함수는 두가지 매개변수를 받는데, 첫번째 매개변수는 에러(err), 두번째 매개변수는 파일읽기에 성공했을 때 가져오는 데이터입니다.

위 예시에서는 에러가 나왔을 경우 에러를 출력하고, 에러가 없을 경우 데이터를 출력합니다.

세탁(세탁물, (err, 세탁이 완료된 세탁물)=>{
	if (err) console.log(err)
    else console.log(세탁이 완료된 세탁물)
}
)

익숙한 빨래의 예시로 설명하면 위과 같을 겁니다.

그럼 이제부터 이 fs.readFile함수(또는 세탁함수)를 콜백함수에서 Promise 함수로 바꿔보겠습니다.

| Promise 만들기

전에도 말했듯이 fs.readFile 함수는 태생부터가 콜백함수기 때문에 fs.readFile 자체를 Promise 함수로 만들수는 없습니다.

대신 우리는 약간의 트릭을 사용할 건데, 바로 fs.readFile을 감싸는 함수를 하나 만드는 것이죠.

const fs = require("fs"); //fs모듈을 import
function IwantToReadFile(){
	fs.readFile("./읽고자 하는 파일의 경로", (err, data)=>{
		if (err) console.log(err);
    	else console.log(data);   
	});
};

이렇게 말입니다. 그리고 이제 IwantToReadFile이라는 함수를 Promise함수로 만들면 되겠죠?

Promise함수를 만드는 방법은 다음과 같습니다. IwantToReadFile이라는 함수가 Promise객체를 반환하게 하면 되죠.

여기서 Promise를 객체라고 부르는 이유는 말 그대로 Promise가 객체(클래스&객체 할때 그 객체 맞습니다.)이기 때문입니다. javascript는 Promise라는 이름의 class를 제공하고, 우리는 Promise class의 객체를 찍어내면 됩니다.

function IwantToReturnPromise(){
	return new Promise(프로미스 객체에 맞는 코드들)
}

즉 이렇게 만들면 된다는 것이죠. 그럼 지금부터 이제 "프로미스 객체에 맞는 코드들"을 적어봐야 겠죠?

function IwantToReturnPromise(){
	return new Promise((resolve, reject)=>{코드를 작성하세요})
}

저 자리에는 함수가 하나 들어갑니다. 이 함수는 resolve와 reject라는 매개변수를 받죠.(매개변수의 이름을 바꿀 수도 있겠지만 별로 추천하지 않습니다.)

resolve와 reject는 각각 함수로 resolve(반환할 값), reject(반환할 값) 이런 식으로 사용할 수 있습니다.

resolve는 Promise 안에서의 작업이 성공적으로 완료되었을 때 반환할 값으로 .then() 안의 함수가 받는 매개변수가 됩니다.

reject는 Promise 안에서의 작업이 실패했을 때 반환할 값으로 .catch() 안의 함수가 받는 매개변수가 됩니다.

세탁()
.then((세탁의 결과물)=>건조(세탁의 결과물)) //A
.catch((애러)=>console.log(에러)) //B

여기서 세탁이라는 함수가 Promise를 반환한다면 A에서 세탁의 결과물에는 resolve() 안의 값이 들어가는 것이고, B에서 에러에는 reject() 안의 결과물이 들어가는 것이죠.

그럼 이를 바탕으로 fs.readFile함수를 감싸는 IwantToReadFile함수를 Promise 함수로 바꿔보겠습니다.

const fs = require("fs"); //fs모듈을 import
function IwantToReadFile(){
	return new Promise((resolve, reject)=>
    fs.readFile("./읽고자 하는 파일의 경로", (err, data)=>{
		if (err) reject(err);
    	else resolve(data);   
	});
 )};

이렇게 바꿀 수 있습니다. 먼저 IwantToReadFile은 Promise 객체를 반환합니다.

이 Promise 객체 안에 들어있는 함수는 resolve, reject를 매개변수로 받죠. fs.readFile이 성공했을 때는 resolve(data)를 반환하고, 실패했을 때는 reject(err)를 반환합니다.

| 만들어진 Promise 함수 사용하기

이제 아까 만든 IwantToReadFile함수를 직접 활용해 보겠습니다.

const fs = require("fs"); //fs모듈을 import

function IwantToReadFile(){
	return new Promise((resolve, reject)=>
    fs.readFile("./읽고자 하는 파일의 경로", (err, data)=>{
		if (err) reject(err); //A
    	else resolve(data);  //B
	});
 )};
 
 IwantToReadFile() //실행하는 부분
 .then((data)=>console.log(data))
 .catch((err)=>console.log(err));

아래 실행하는 부분을 봐주시면 됩니다.

우선 Promise함수로 만들어진 IwantToReadFile을 호출합니다.

그리고 .then을 사용하여 IwantToReadFile이 성공적으로 실행되었을 때의 데이터를 가져옵니다. 즉, B로부터 반환된 데이터를 출력합니다.

마지막으로 .catch를 사용하여 IwantToReadFile이 실패했을 경우 에러를 가져옵니다. 즉, A로부터 반환된 에러를 출력합니다.

| 애초에 Promise로 만들어진 함수는?

여기까지 공부했다면 한가지를 느끼셨을 겁니다. Promise 객체를 활용하는 것이 매우 복잡하다고요.

사실 애초부터 Promise 객체를 반환하는 Promise 기반 함수들을 활용할 때는 그냥

가져온함수()
.then(코드)
.catch(코드)

이런 식으로만 활용해주면 됩니다. 엄청 간단하죠. 사실 NodeJs에서 기본으로 주는 모듈(함수)이 아닌 Express나 Mongoose 등 써드파티 모듈(따로 npm install해서 사용하는 애들)들은 보통 함수를 제공할 때 Promise 함수를 제공합니다.

그렇기 때문에 너무 무서워할 필요는 없다, 이 정도의 말씀을 드리고 싶었습니다.

| Promise Hell

사실 Promise 객체로 함수들을 이어가는 것도 가독성이 엄청나게 좋지는 않습니다.

세탁() //A
.then((세탁의 결과물)=>건조(세탁의 결과물)) //B
.then((건조의 결과물)=>빨래개기(건조의 결과물)) //C
.then((개진 빨래) => 옷장에넣기(개진 빨래)) //D
.catch((애러)=>console.log(에러)) //E

이런 식으로 코드를 이어나가는 것도 나름 복잡하다는 것이죠. (사실 이 정도면 저는 충분하다고 생각합니다만..)

이렇게 코드들이 세로로 이어져 있는 것을 다시 Promise 지옥이라고 부릅니다. 그리고 이를 보완하기 위해 자동적으로 함수가Promise 객체를 반환하도록 하는 async와, 해당 구문이 시행될때까지 뒤의 코드를 실행되지 않게 하는 await 문법이 등장합니다.

네, 다음 장에는 async&await을 공부해보겠습니다

profile
HTML 개발자

0개의 댓글