[JS] promise

hoifoi·2023년 12월 12일
0

[JS] 비동기처리

목록 보기
2/3
post-thumbnail

들어가며

이전 포스팅에서 매력적인 콜백함수를 소개했지만
바로 위기가 위기에 봉착해버렸다..!

비동기처리와 콜백지옥이 바로 그것인데
콜백지옥은 앞서 설명한 바가 있으니 바로 비동기처리가 어떤 것인지 알아보자!

동기 / 비동기 처리

사전적인 설명을 붙이자면

동기처리

한 작업이 실행 및 작업을 종료할 때까지
다른 작업은 모두 멈춘 상태를 유지하고 기다리는 방식

비동기처리

특정 작업의 실행 및 종료까지 기다리지 않고
다른 작업도 바로 실행하는 방식

차이에 대한 예시(텍스트)

간단하게 로또명당과 은행창구의 차이라고 보면 된다!(언제 1등 되나..)

가끔 길을 걷다 토요일 저녁 즈음에
어디선가 부터 줄이 길게 늘어진 풍경을 본적이 있을 것이다!
그 곳은 높은 확률로 로또 명당이다!(추첨 마감 전에 살려고 그러는거)

이번 회차 로또를 사기위해선 토요일 오후 7시 45분 전에 사야하는데
한줄로 서서 차례를 기다릴 수 밖에 없기 때문에 줄을 선 의미가 없어진다!
(다음회차를 미리 구매할수도 없기 때문에 바로 판매가 마감된다)

반면 은행창구에서는 번호표를 뽑고 줄을 서는 개념은 비슷하지만
창구가 여러 곳이기 때문에 앞 사람의 은행업무가 길더라도
다른 창구에서 자리가 나면 바로 그곳으로 가서 업무를 보면 된다!
그렇기 때문에 은행에서는 보는 업무(예금, 대출 등)에 따라
먼저 업무를 보는 사람과 시간이 길어지는 사람 등으로 구별이된다!

차이에 대한 예시(코드)

// 일반적인 경우(로또명당과 같은 예시)
const firstFunc = () => {
  console.log('첫번째 작업')
}

const secondFunc= () => {
  console.log('두번째 작업')
}

const thirdFunc = () => {
  console.log('세번째 작업')
}

firstFunc()
secondFunc()
thirdFunc()
// 결과
// "첫번째 작업"
// "두번째 작업"
// "세번째 작업"

// 이반적인 경우(은행창구와 같은 예시)
const firstFunc = () => {
  console.log('첫번째 작업')
}

const secondFunc= () => {
  console.log('두번째 작업')
}

const thirdFunc = () => {
  console.log('세번째 작업')
}

firstFunc()
setTimeout(()=>secondFunc(), 3000)
thirdFunc()
// 결과
// "첫번째 작업"
// "세번째 작업"
// 3초 뒤..
// "두번째 작업"

설명없이 쓴 지연함수는
setTimeout(함수, 시간(밀리초 단위 3초는 3000))와 같이 쓰면 된다!
(백엔드로서는 쓸 일이 없을 것 같은데, 아마 프론트에서는 쓸일이 있지 않을까? 클릭 후 액션에 지연을 둔다거나?)

이렇게 지연함수를 쓰면(연산에 3초가 걸린다고 가정)
기다리지 않고 바로 다음 줄에 있는 코드(세번째 작업)를 실행시켜버린다!

간단하게 비동기처리는 먼저 할 수 있는걸 먼저 하자!라고 보면된다!
자바스크립트는 이 비동기처리에 특화되어있는 것!

좋은게 좋은걸까?

할 수 있는 걸 먼저하자는건 참 좋은데 여기서 문제가 생길 수도 있다!
(빛은 참 좋아보인다, 실제로도 그러니 이렇게 발전한거겠지?)
앞의 예시에서는 조금 억지로 만드느라 시간지연함수까지 썼지만

실제의 경우를 예로 들면
두번째 함수에서 나오는 결과 값이
세번째 함수에서 쓰여야 하는 경우가 있다!
(이때 두번째 함수의 역할은 재가공이나, DB에서 데이터를 끌어오는 경우)

그럼 이때에는 두번째 함수가 끝나기 전에
세번째 함수가 실행되어 버리면, 값을 받지 못한 세번째 함수는
필연적으로 오류가 생기는 것!

즉, 특정 작업에서는
이전에 실행된 함수가 값을 받아오는 작업의 종료까지
기다려야 하는 상황이 생기는 것이다!

조금 조금 더 비슷한 코드 예시

const depositTask = (clienNum, amount) => {
   console.log(`${clienNum}번 고객님의 계좌에 ${amount}원 입금 완료!`)
}

const loanTask = (clienNum) => {
   console.log(`${clienNum}번 고객님의 신용도를 조회중입니다!`)
   setTimeout(()=>{
      let amount = Math.floor(Math.random()*clienNum)*1000
   }, 5000)
   console.log(`${clienNum}번 고객님은 ${amount}원 대출 가능합니다!`)
}


depositTask(1,1000)
depositTask(2,loanTask(2))
depositTask(3,7000)

// 결과
// "1번 고객님의 계좌에 1000원 입금 완료!"
// "2번 고객님의 신용도를 조회중입니다!"
// "3번 고객님의 계좌에 7000원 입금 완료!"
// "2번 고객님은 undefined원 대출 가능합니다!"
// "2번 고객님의 계좌에 undefined원 입금 완료!"

그럼 함수를 세세하게 쪼개서
그런 경우가 없게끔 하면 되지 않나 싶을 수 있는데
밀리 밀리 밀리.. 초 단위에서 이루어지기 때문에
이 경우에는 콜백의 구조의 잘못이라기 보다는 그냥 어쩔 수 없는 경우인 것
(짧은 식견에 의한 결론이라 틀렸을 수 있다.. 그땐 도와주세요 :D)

아..
그럼 Promise는 기다리기로 약속을 맺는건가?(맞다!)

자 그럼 이 특징으로 어떻게 콜백함수의 문제를 해결할지
Promise에 대해서 알아보자!

Promise란?

비동기 작업이 성공 또는 실패했을 때의 결과를 나타내는 객체
이를 통해 비동기 작업을 보다 간결하게 처리할 수 있게 된다!

Promise의 상태 설명


MDN에서 그림으로 설명해주는 부분이 있어 일부 발췌했습니다
언제나 제일 상세한 설명은 공식문서가 좋으니 MDN은 꼭 북마크..!

Promise는 다음과 같은 세 가지 상태를 가진다

  • Pending (대기): 비동기 작업이 완료되지 않은 상태.
  • Fulfilled (이행): 비동기 작업이 성공적으로 완료된 상태.
  • Rejected (거부): 비동기 작업이 실패한 상태.

그리고 코드로 설명하자면 아래와 같이 실행이 된다고 생각하면 된다

const promiseFunc = new Promise((resolve, reject) => {
  // 비동기 작업 시뮬레이션
  setTimeout(() => {
    const success = true;

    if (success) {
      const data = 'This is the successed data';
      resolve(data);
    } else {
      const error = 'Failed to get data';
      reject(error);
    }
  }, 3000);
});

promise로 만든 함수가
내가 정한 일련의 연산이 성공하면 resolve가 호출되고
연산 실패시 reject가 호출된다
즉 성공/실패시 액션을 따로 정할 수 가 있는 것!

아직까진 두루뭉술한 느낌이니 바로 적용해보자!

Promise의 사용

Promise를 사용할 땐 .then() / .catch() 메서드를 이용하여
성공과 실패에 대한 각각의 대처를 하면 된다!

// 연산을 성공했을 때!
promiseFunc
  .then((data) => console.log(data))
  .catch((error) => console.log(error))
// 결과
// 'This is the successed data'

// 연산에 실패했을 때!
promiseFunc
  .then((data) => console.log(data))
  .catch((error) => console.log(error))
// 결과
// 'Failed to get data'

then 즉, 연산이 끝난 뒤에 실행하게끔 하고
catch 즉, 오류가 생겼을 경우 잡아서 원하는 액션을 취할 수 있게끔 할 수 있다!

조금 더 현실적으로
위의 코드에다가 적용해보자!

const depositTask = (clientNum, amount) => {
   console.log(`${clientNum}번 고객님의 계좌에 ${amount}원 입금 완료!`);
};

const loanTask = (clientNum) => {
   console.log(`${clientNum}번 고객님의 신용도를 조회중입니다!`);

   return new Promise((resolve, reject) => {
      setTimeout(() => {
         let amount = Math.floor(Math.random() * clientNum) * 1000;
         console.log(`${clientNum}번 고객님은 ${amount}원 대출 가능합니다!`);
         resolve(amount);
      }, 5000);
   });
};

depositTask(1, 1000);
loanTask(2)
   .then((loanAmount) => depositTask(2, loanAmount))
   .catch((error) => console.error("대출 오류:", error));

depositTask(3, 7000);
// 결과
// "1번 고객님의 계좌에 1000원 입금 완료!"
// "2번 고객님의 신용도를 조회중입니다!"
// "3번 고객님의 계좌에 7000원 입금 완료!"
// "2번 고객님은 1000원 대출 가능합니다!"
// "2번 고객님의 계좌에 1000원 입금 완료!"

자 이제 조금 더 나아가보자!

Promise 체이닝

여러 액션을 엮어서 쓰는 것을 체이닝이라고 하는데
이 방법을 쓰면 콜백 지옥을 조금 더 깔끔하게 해결 할 수 있다!
여러 개의 비동기 작업을 순차적으로 처리하고 싶을 땐
어떻게 Promise 체이닝을 할지 간단히 예를 들어보겠다
(지금부터는 req,res처럼 res,rej로 줄여쓰겠다! 이게 컨벤션이기도 하고)

const promiseFunc1 = (num) => {
   return new Promise((res,rej)=>{
      setTimeout(() => {
         res(num * 3)
         rej('error')
      }, 3000)
   })
}
const promiseFunc2 = (num) => {
   return new Promise((res,rej)=>{
      setTimeout(() => {
         res(num * 2)
         rej('error')
      }, 3000)
   })
}

promiseFunc1(3)
   .then(data => promiseFunc2(data))
   .then(newData => console.log(newData))
   .catch(error => console.log(error))
// 결과
// 18

이렇게 then을 한번 더 써줌으로서
1차 함수의 결과물을 받아 넘겨 2차 함수 실행이 가능해진다
진정한 의미의 체이닝은 이 부분이라고 보면 된다!

또한 코드 상의 형식은
new Promise(성공, 실패) 형식만 맞춰주면 되기 때문에
new Promise(a,b)라고 써도 상관은 없지만 알아는 들어야 하기 때문에!
new Promise(res, rej)라 써주는 습관을 들이면 좋을 것이고,
.then(출력된 데이터 => 이후 액션)의 형식만 맞춰주면 되기 때문에
data라고 써도 되고 result라고 써도 되니 형식만 잘 맞춰주면 된다!

그리고 catch 이후의 추가 액션도 가능하다!

// 연산 실패시
promiseFunc1(3)
   .then(data => promiseFunc2(data))
   .then(newData => console.log(newData))
   .catch(error => console.log(error))
   .then(() => console.log('do action after error occurred'))
// 결과
// 'error'
// 'do action after error occurred'

결론

Promise는 비동기 코드를 효율적으로 다룰 수 있도록 도입된 패턴 중 하나이다
비동기 작업의 성공과 실패에 대한 처리를 보다 간결하게 할 수 있고
여러 작업을 순차적으로 처리할 때 유용한 Promise 체이닝이란 기술도 있다
콜백함수를 쓸 때는 Promise를 통해 코드의 가독성과 유지보수성이 향상되므로
비동기 작업을 다룰 때마다 고려해볼 만한 가치가 있다(지만 무조건 쓰는게 좋을 것 같다!)

++

아쉽게도 나는 이 패턴을 강의로서 들을 수 있는 기회가 없었다!
프론트의 코드를 보다가 알게 된 패턴인데, 그럼 백엔드는 쓸 필요가 없냐고?
아니 전혀.. 그저 강사님의 주관 차이였던걸로..
그렇다면 백엔드는? 아니.. 다른 방법은 무엇이 있을까?
바로 다음 포스팅에서 다뤄보겠다!

+++

내가 이미 아는 것보다 이후에 안 것을 먼저 포스팅한 이유는..
이 흐름을 알고나서야 지금 나온 최신 기술(ES6)이 왜 이렇게 작동하고
왜 쓰는 건지 알 수 있기 때문이었다!(이거 보여줄려고 어ㄱ..)
결과 적으로 제일 최신 기술을 쓰게 될 것 이긴 하지만
역사를 알고 쓰면 더 재밌을테니 다음 포스팅을 기대해주세요 :D

++
발돋움 중인 예비 개발자 입니다.
태클 및 의견 공유 언제나 환영 :D

profile
컨텐츠 기획자 출신 백엔드 개발자 :D

0개의 댓글