본문으로 바로가기

[JavaScript] Promise로 callback 지옥 무찌르자

promise는 JavaScript에서 비동기 작업을 수행하는 객체입니다. promise를 학습하고 callback 지옥을 탈출해봅시다.

Promise

◆ mini Promise 3가지 상태

Promise 3가지 상태가 있습니다.

1. 대기 (Pending) : 초기 상태

2. 이행 (Fulfilled) : 성공

3. 거부 (Rejected) : 실패

 

대기에서 연산이 시작되면 이행이 되거나 거부가 됩니다. 즉 pending -> fulfilled or rejected 로 이해하시면 됩니다. 이제 각각의 상태를 한번 만들어봅시다. 상태를 만들어 보기 전 promise는 아래와 같이 사용됩니다.

new Promise((resolve, reject) => {
  // ... 특정 연산;
  if(..조건..) resolve(data);
  else reject(new Error("error msg"))
});

Promise는 callback 함수 인자로 resolve, reject를 가지게 됩니다. 성공(이행)하였을 때는 resolve 함수를 호출하고, 실패(거부)하였을 때는 reject 함수를 호출합니다. resolve, reject 함수에는 인자로 값을 넘길 수 있습니다. 그럼 이제 각각의 상태를 만들어봅시다.

◆ 대기 (Pending)

new Promise((resolve, reject) => {
  ... 특정 연산 ... 
});

위 예시에서 본 것이랑 비슷하죠? 이렇게 new Promise를 하게 되면 대기 상태가 됩니다. 특정 연산이 끝나면 결과가 나올 것입니다. 특정 연산에 대한 성공 or 실패 겠지요? 앞서 언급하였듯이 성공한 경우는 resolve, 실패한 경우는 reject 함수를 사용하면 됩니다.

◆ 이행 (Fulfilled / 성공) | 거부 (Rejected / 실패)

new Promise((resolve, reject) => {
  // ... 특정 연산 (서버에서 list를 가져오는 연산이라고 가정);
  const data = [list item 100개];

  // 특정 연산에 대한 성공
  if(data.length) resolve(data);

  // 특정 연산에 대한 실패
  else reject(new Error("fetch error"))
});

(서버는 list가 무조건 있다고 가정) 위 data 값이 서버에서 list를 가져온 결과라고 해봅시다. data 배열 크기가 있는 경우는 연산에 성공하여 resolve를 하는 것이고 아닌 경우는 reject로 error를 생성합니다.

위 코드를 조금 더 현실적으로 작성하면 아래와 같이 작성할 수 있습니다.

const searchList = () => {
  return new Promise((resolve, reject) => {
    fetch("url ...")
    .then(res => resolve(res))
    .catch(error => reject(new Error(error)));
  })
}

searchList()
.then(console.log) // 성공하여 list 데이터 출력
.catch(console.error) // 실패하여 error 메시지 출력

위와 같이 성공에 대한 처리, 실패(에러)에 대한 처리를 할 수 있습니다.

Promise Chaining

Promise는 단독으로 사용하기 보다는 여러 단계를 하는 경우가 많이 있습니다. 이러한 경우 chain처럼 연결한다고 해서 promise chaining이라고 합니다.

예를들어 친구 집에 도착해서 벨을 누른 뒤 친구가 문을 열어준다고 해봅시다. 그럼 아래와 같이 작성할 수 있습니다.

const arrived = () => {
  return new Promise((resolve, reject) => {
    setTimeout(()=>resolve("도착"), 1000);
  })
};

const calling = () => {
  return new Promise((resolve, reject) => {
    setTimeout(()=>resolve("띵동"), 1000);
  })
}

const open = () => {
    return new Promise((resolve, reject) => {
    setTimeout(()=>resolve("문열림"), 1000);
  })
}

arrived()
  .then(console.log)
  .then(calling)
  .then(console.log)
  .then(open)
  .then(console.log)

너무 코드 현실이 아닌 생활 현실이어서 이해가 안될 수도 있는데 로그인 하기 전 id, pw 확인 -> user 데이터 가져오기 -> ... 기타 작업 이렇게 이해하셔도 됩니다.

Error 핸들링 잘하기

사실 성공할 때는 크게 문제될 것이 없습니다. 성공하면 에러가 발생하지 않기 때문이죠. 위 예시는 실패는 없고 성공만 있습니다. 항상 문제는 성공했을 때가 아닌 실패하여 error가 발생했을 때 나타납니다. 이럴 때 error 처리를 얼마나 잘해주는 지에 따라 서비스의 질이 달라집니다. 한번 알아봅시다.

위 예시를 변경해보려고 했으나 ... 생각이 나지 않아 다른 예시로 만들어보았습니다. 꿩 대신 닭을 만들어 보겠습니다. 꿩을 잡아야 하는데 잡지 못 해 닭을 넘겨 주는 것이죠. 아래 코드를 확인해봅시다.

const hunt = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("꿩 잡기 성공");
    }, 1000);
  });
};

const move = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error("꿩이 도망갔다. 꿩 대신 닭 먹어야지"));
    });
  });
};

const cook = bird => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`${bird} 요리 완성`);
    }, 1000);
  });
};

hunt(true)
  .then(console.log)
  .then(move)
  .catch(err => {
    console.error(err);
    return "닭";
  })
  .then(cook)
  .then(console.log);

아주 간단한 코드입니다. 꿩을 잡았으나, 집에 가지고 오던 중 꿩이 도망쳐 어쩔 수 없이 닭으로 요리를 해먹는 것입니다. 만약 중간에 catch로 예외처리를 해주지 않았다면 어떻게 될까요? 꿩만 날라가고 나서 닭으로 요리는 하지 못 했을 것입니다. 따라서 예외가 발생했을 때 중간 중간 핸들링을 잘해주는 것이 중요합니다. 그런데 만약 중간에 실패를 한 경우 다음 과정을 진행하지 않고 끝내버리고 싶으면 어떻게 할까요? 마지막에 catch를 넣으면 됩니다. 아래 코드를 참고해봅시다.

hunt(true)
  .then(console.log)
  .then(move)
  .then(cook)
  .then(console.log)
  .catch(error => {
    console.error(error);
    console.log("중간에 실패한 자는 밥을 먹을 이유가 없다.");
  });

위와 같이 작성하면 요리를 거치지 않고 밥을 굶게됩니다. 그렇다면 끝내고 싶진 않고 그 다다음 단계로 가고 싶으면 어떻게 할까요? 아래 코드를 확인해봅시다.

const promise = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("성공");
    }, 1000);
  });
};

promise()
  .then(() => {
    console.log("1단계 성공");
  })
  .then(() => {
    console.log("2단계 성공");
  })
  .then(() => {
    console.log("3단계 실패");
    throw new Error("3단계 실패");
  })
  .then(() => {
    console.log("4단계 패스 됨");
  })
  .catch(err => {
    console.error(err);
  })
  .then(() => {
    console.log("4단계 패스되고 5단계 진입");
  })
  .then(() => {
    console.log("6단계 끝");
  });

이해가 되시나요? 이렇게 내가 원하는 지점에 에외처리를 잘 해주어야 합니다. 추가로 성공하던 실패하던 마지막으로 해주고 싶은 작업이 있을 수 있습니다. 그 작업을 하기 위해서는 finally를 사용하면 됩니다. 아래 코드를 확인해봅시다.

promise().then().catch().finally()

위와 같이 사용하면 성공하여 resolve된 경우 then -> finally / 실패하여 reject된 경우 catch -> finally 로 어쨌든 finally를 실행하게 됩니다. 마지막 정리할 작업이 있으면 여기서 하면 됩니다.

지난 시간 callback 지옥 무찌르기

지난 callback, callback 지옥 게시글에서 마지막 부분을 보면 코드가 아래처럼 잔인하게 되어 있습니다.

// callback 지옥 코드
eat(
  "밥먹기",
  () => {
    brushTeeth(
      "양차히기",
      () => {
        washingFace(
          "세수하기",
          () => {},
          () => {
            console.log("에러 처리");
          }
        );
      },
      () => {
        console.log("에러 처리");
      }
    );
  },
  () => {
    console.log("에러 처리");
  }
);

해당 코드를 이번 시간에 확인해본 promise를 이용하여 깔끔하게 수정해보겠습니다.

const eat = food => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (food === "초밥" || food === "고기") resolve(food);
      else reject(new Error("초밥, 고기 외 먹지 않습니다."));
    }, 1000);
  });
};

const brushTeeth = food => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (food === "초밥") resolve(`${food}을(를) 맛있게 먹고 양치까지 끝`);
      else reject(`${food}를 먹고 체해서 양치를 하지 못 했음`);
    }, 1000);
  });
};

const washingFace = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("세수 깔끔하게 성공"), 1000);
  });
};

// eat("초밥")
//   .then(brushTeeth)
//   .then(console.log)
//   .then(washingFace)
//   .then(console.log)
//   .catch(console.error);

// eat("고기")
//   .then(brushTeeth)
//   .then(console.log)
//   .then(washingFace)
//   .then(console.log)
//   .catch(console.error);

eat("샌드위치")
  .then(brushTeeth)
  .then(console.log)
  .then(washingFace)
  .then(console.log)
  .catch(console.error);

코드를 보면 아시겠지만 eat, brushTeeh 함수도 매우 깔끔해졌습니다. 그리고 마지막 호출하는 부분에서 callback 지옥이 해결된 것을 볼 수 있습니다. promise 만으로도 분명히 좋은 코드를 작성할 수 있습니다. 그러나 promise chaining이 계속된다면 어떻게 될까요? 코드가 계속 이어서 작성되어져 가독성이 떨어질 것입니다. 다음 게시글에서는 async await을 이용하여 더 깔끔하게 비동기 코드를 작성하는 방법을 확인해봅시다.

코드 자세히 확인해보기

마지막

해당 내용은 틀릴 수도 있습니다. 틀린 내용이 있으면 조언 부탁드립니다.

반응형