프론트엔드/Javascript

비동기 - 프로미스 (Promise)

syleemomo 2024. 3. 14. 18:19
728x90

 

* 프로미스와 현실상황 비교 

본인이 유명한 가수라고 가정해보자! 그리고 본인의 싱글 앨범이 언제 나오는지 밤낮으로 물어보는 팬들을 상대해야 한다고 해보자!

프로미스에 대한 비유

여러분은 앨범제작이 완료되면 즉시 팬들에게 소식을 전달할 수 있도록 할 것이다. 예를 들어, 구독리스트를 만들어서 팬들에게 전달하고, 이메일 주소를 기재하도록 부탁한다. 이렇게 하면 앨범제작이 끝났을때 팬들에게 이메일을 보내 앨범 관련 소식을 곧바로 알려줄 수 있다. 녹음 스튜디오에 불이 나서 앨범제작이 연기되더라도 관련된 소식을 팬들에게 전달할 수가 있다. 결과적으로 밤낮으로 질문하는 팬들이 사라지고, 팬들은 앨범출시일을 놓치지 않게 되었다. 

1. 제작코드(producing code)는 원격에서 스크립트를 로딩하는 등의 시간이 걸리는 작업을 한다. 
2. 소비코드(consuming code)는 '제작코드'의 작업이 끝나기를 기다렸다가 이를 소비한다. 
이때 소비주체는 다수일수 있다. 
3. 프로미스(promise)는 '제작코드'와 '소비코드'를 연결해주는 특별한 자바스크립트 객체이다.

위의 예시는 코드를 작성할때 자주 만나는 상황을 현실에 비유한 것이다. 제작코드는 비유에서 앨범제작에 해당된다. 소비코드는 비유에서 팬이 소식을 전달받는 상황에 해당한다. 프로미스는 제작코드(비동기)가 실행을 완료했을때, 모든 소비코드가 결과를 사용할 수 있도록 제작코드와 소비코드를 연결해주는 중재역할을 한다. 즉, 비유에서 구독리스트에 해당한다.

 

* 프로미스 문법 

/* promise : 프로미스 객체
   new Promise : 생성자 함수
   function(resolve, reject){} : executor(실행함수)
*/

const promise = new Promise(function(resolve, reject) {
    // executor (제작 코드, '앨범제작')
})

프로미스의 기본적인 문법은 위와 같다. 용어는 주석을 참고하자!

1. 생성자 함수(new Promise)가 프로미스 객체를 생성함과 동시에 executor 도 실행된다.
2. executor 는 작업에 시간이 걸리는 제작코드(비동기 코드)를 포함한다. 
3. executor 로 전달되는 파라미터 resolve, reject 은 자바스크립트에서 자체 제공하는 콜백함수이다.
4. executor 는 작업이 언제 끝나든 상관없이 작업이 종료되면 반드시 resolve, reject 중 하나를 호출해야 한다.
5. executor 의 resolve(value)는 작업이 성공적으로 끝난 경우 결과값인 value 와 함께 호출한다. 
6. executor 의 reject(error)는 작업 중에 에러가 발생되면 에러객체(error)와 함께 호출한다.

executor (실행함수)는 위와 같은 특징을 가진다. 

1. state - 처음에는 "pending" (보류)이다가 resolve 가 호출되면 "fullfilled" 로 변경된다. 
           처음에는 "pending" (보류)이다가 reject 가 호출되면 "rejected"로 변경된다. 
           
2. result - 처음에는 undefined 이다가 resolve(value) 가 호출되면 value 로 변경된다. 
            처음에는 undefined 이다가 reject(error) 가 호출되면 error 로 변경된다.

new Promise (생성자 함수)로 생성된 프로미스 객체(promise)는 내부 프로퍼티를 가지며 특징은 위와 같다. 이를 그림으로 나타내면 아래와 같다. 

프로미스 객체의 내부 프로퍼티 상태 변화

 

* 프로미스 예제 

const promise = new Promise(function(resolve, reject) {
    // 프라미스가 만들어지면 executor 함수는 자동으로 실행됩니다.

    // 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result는 '완료'가 됩니다.
    setTimeout(() => resolve("완료"), 1000)
})

해당 코드는 아래의 순서로 동작한다. 

1. new Promise (생성자 함수)가 실행된다.
2. executor 가 즉시 실행된다.
3. 프로미스 객체(promise)를 반환한다. 
4. 1초 후에 resolve("완료")를 실행한다.
5. resolve 가 호출되면 promise 객체의 state 가 "fulfilled" 로 변한다.

이때 프로미스 객체(promise)의 상태는 아래와 같이 변한다. 이와 같이 작업이 성공적으로 처리된(resolve 를 호출한) 프로미스는 약속이 이행된 프로미스(fullfilled promise) 라고 부른다. 

프로미스 객체의 내부 프로퍼티 상태 변화

const promise = new Promise(function(resolve, reject) {
    // 1초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보냅니다.
    setTimeout(() => reject(new Error("에러 발생!")), 1000)
})

해당 코드는 아래의 순서로 동작한다. 

1. new Promise (생성자 함수)가 실행된다.
2. executor 가 즉시 실행된다.
3. 프로미스 객체(promise)를 반환한다. 
4. 1초 후에 reject(new Error("에러 발생!"))를 실행한다.
5. reject 가 호출되면 promise 객체의 state 가 "rejected"로 변한다.

이때 프로미스 객체(promise)의 상태는 아래와 같이 변한다. 이와 같이 작업이 실패한(reject 를 호출한) 프로미스는 약속이 거부된 프로미스 (rejected promise)라고 한다. 

프로미스 객체의 내부 프로퍼티 상태 변화

 

1. executor 는 보통 시간이 걸리는 작업을 수행한다.
2. resolve 나 reject 함수를 호출하면 프로미스 객체의 상태가 변한다.
3. 이행(resolved) 혹은 거부(rejected) 상태의 프로미스는 처리된(settled) 프로미스라고 한다. 
4. 처리된(settled) 프로미스의 반대는 대기(pending) 프로미스라고 한다.

지금까지의 내용을 요약하면 위와 같다.

 

* 프로미스의 특징

const promise = new Promise(function(resolve, reject) {
    resolve("완료")
  
    reject(new Error("…")) // 무시됨
    setTimeout(() => resolve("…")) // 무시됨
})

프로미스는 성공 또는 실패만 한다. 그러므로 작업이 끝나면 반드시 resolve, reject 중 하나를 호출해야 한다. 이때 변경된 상태는 더이상 변하지 않는다. 그러므로 처리가 끝난 (resolve, reject 중 하나를 호출한) 프로미스에 다시 resolve 혹은 reject 를 호출하더라도 무시된다. 또한, resolve 나 reject 함수는 인자를 받지 않거나 하나만 받을수 있다. 

const promise = new Promise(function(resolve, reject) {
    // 일을 끝마치는 데 시간이 들지 않음
    resolve(123) // 결과(123)를 즉시 resolve에 전달함
})

executor 는 일반적으로 시간이 걸리는 비동기 작업을 수행하지만, 위와 같이 resolve 나 reject 함수를 즉시 호출해도 된다. 이렇게 되면 프로미스 객체의 state 는 즉시 이행상태(fulfilled)가 된다. 또한, 프로미스 객체의 프로퍼티(state, result)는 내부 프로퍼티이므로 개발자가 직접 접근할 수 없다. 

 

* 소비자 - then, catch, finally 

프로미스 객체는 executor (제작코드)와 결과나 에러를 전달받을 소비함수(팬)를 이어주는 역할을 한다. 소비함수는 .then, .catch, .finally 메서드를 이용하여 등록(구독)한다. 

promise.then(
    function(result) { /* 결과(result)를 다룹니다 */ },
    function(error) { /* 에러(error)를 다룹니다 */ }
)

.then 은 프로미스에서 가장 중요하고 기본이 되는 메서드이다. 

1. .then 의 첫번째 인자는 작업이 성공했을때 실행할 콜백함수이다. 
2. .then 의 두번째 인자는 작업이 실패했을때 실행할 콜백함수이다.

.then 메서드는 위와 같이 사용한다. 

const promise = new Promise(function(resolve, reject) {
    setTimeout(() => resolve("완료!"), 1000);
})
  
// resolve 함수는 .then의 첫 번째 함수(인수)를 실행합니다.
promise.then(
    result => alert(result), // 1초 후 "완료!"를 출력
    error => alert(error) // 실행되지 않음
)

해당 코드는 작업이 성공했을때 .then 메서드의 첫번째 인자에 전달된 콜백함수가 실행된다. 

const promise = new Promise(function(resolve, reject) {
    setTimeout(() => reject(new Error("에러 발생!")), 1000);
})
  
// reject 함수는 .then의 두 번째 함수를 실행합니다.
promise.then(
    result => alert(result), // 실행되지 않음
    error => alert(error) // 1초 후 "Error: 에러 발생!"을 출력
)

해당 코드는 작업이 실패했을때 .then 메서드의 두번째 인자에 전달된 콜백함수가 실행된다. 

const promise = new Promise(resolve => {
    setTimeout(() => resolve("완료!"), 1000)
})
  
promise.then(alert) // 1초 뒤 "완료!" 출력

작업이 성공했을때만 구독(소비)하고 싶으면 위와 같이 .then 메서드에 인자를 하나만 전달하면 된다. 

const promise = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error("에러 발생!")), 1000)
})
  
// .catch(f)는 promise.then(null, f)과 동일하게 작동합니다
promise.catch(alert) // 1초 뒤 "Error: 에러 발생!" 출력

작업이 실패했을때만 구독(소비)하고 싶다면 위와 같이 .catch 메서드를 사용하면 된다. 

new Promise((resolve, reject) => {
    /* 시간이 걸리는 어떤 일을 수행하고, 그 후 resolve, reject를 호출함 */
  })
// 성공·실패 여부와 상관없이 프라미스가 처리되면 실행됨
.finally(() => 로딩 인디케이터 중지)
.then(result => result와 err 보여줌 => error 보여줌)

작업의 성공, 실패 여부와 상관없이 프로미스가 처리되면(resolve, reject 함수를 호출하면) finally 메서드는 무조건 실행된다. 즉, finally 메서드는 작업결과에 관계없이 마무리가 필요한 경우에 사용할 수 있다. 

new Promise((resolve, reject) => {
    setTimeout(() => resolve("결과"), 2000)
  })
.finally(() => alert("프라미스가 준비되었습니다."))
.then(result => alert(result)) // <-- .then에서 result를 다룰 수 있음

finally 메서드는 자동으로 다음 메서드(then 혹은 catch)에 결과나 에러를 전달한다. 해당 코드에서는 result 가 finally 를 거쳐서 then 메서드까지 전달된다. 

new Promise((resolve, reject) => {
    throw new Error("에러 발생!")
  })
.finally(() => alert("프라미스가 준비되었습니다."))
.catch(err => alert(err)) // <-- .catch에서 에러 객체를 다룰 수 있음

finally 메서드는 자동으로 다음 메서드(then 혹은 catch)에 결과나 에러를 전달한다. 해당 코드에서는 err 가 finally 를 거쳐서 catch 메서드까지 전달된다. 

 

* then/catch/finally 메서드의 특징

프로미스가 대기상태일때(resolve 혹은 reject 함수 호출하기 전) , then/catch/finally 메서드는 프로미스가 처리(resolve 혹은 reject 함수 호출)되길 기다린다. 프로미스가 처리되면 (resolve 혹은 reject 함수 호출), then/catch/finally 메서드가 즉시 실행된다. 

// 아래 프라미스는 생성과 동시에 이행됩니다.
let promise = new Promise(resolve => resolve("완료!"))

promise.then(alert) // 완료! (바로 출력됨)

 

* 프로미스의 활용

function loadScript(src, callback) {
    let script = document.createElement('script')
    script.src = src
  
    script.onload = () => callback(null, script)
    script.onerror = () => callback(new Error(`${src}를 불러오는 도중에 에러가 발생함`))
  
    document.head.append(script)
}

복습 차원에서 콜백 기반으로 작성된 기존의 함수를 살펴보자!

function loadScript(src) {
    return new Promise(function(resolve, reject) {
      let script = document.createElement('script')
      script.src = src
  
      script.onload = () => resolve(script)
      script.onerror = () => reject(new Error(`${src}를 불러오는 도중에 에러가 발생함`))
  
      document.head.append(script)
    })
}

이번에는 콜백함수 대신 프로미스로 다시 작성해보자!

새로운 함수에서는 스크립트 로딩이 성공적으로 끝났을때 resolve 함수를 호출하여 프로미스 객체의 state 를 fulfilled 로 변경한다. 반대로 스크립트 로딩에 실패하면 reject 함수를 호출하여 프로미스 객체의 state 를 rejected 로 변경한다. 

const promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js")

promise.then(
  script => alert(`${script.src}을 불러왔습니다!`),
  error => alert(`Error: ${error.message}`)
)

promise.then(script => alert('또다른 핸들러...'))

프로미스 객체의 state 가 변경되면(로딩이 완료되면) 이를 구독(소비)하는 메서드(then)가 즉시 실행된다. 성공하면 then 메서드의 첫번째 인자에 전달한 콜백함수가, 실패하면 then 메서드의 두번째 인자에 전달한 콜백함수가 실행된다. 

function loadScript(src) {
    return new Promise(function(resolve, reject) {
      let script = document.createElement('script')
      script.src = src
  
      script.onload = () => resolve(script)
      script.onerror = () => reject(new Error(`${src}를 불러오는 도중에 에러가 발생함`))
  
      document.head.append(script)
    })
}

const promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js")

promise.then(
  script => alert(`${script.src}을 불러왔습니다!`),
  error => alert(`Error: ${error.message}`)
)

promise.then(script => alert('또다른 핸들러...'))

전체코드는 다음과 같다. 프로미스 객체(promise)에는 구독(소비) 함수를 여러개 등록할 수 있다. 이렇게 하면 스크립트 로딩이 끝났을때 동시에 등록된 모든 소비자(then)에게 결과나 에러를 전달한다. 수업 초반에 살펴본 현실에서는 새로운 팬을 구독리스트에 추가하는 것과 같다.

 

* 연습과제 1

내장 함수 setTimeout은 콜백을 사용한다. 프라미스를 기반으로 하는 동일 기능 함수를 만들어보자! 함수 delay(ms)는 프라미스를 반환해야 한다. 반환되는 프라미스는 아래와 같이 then을 붙일 수 있도록 ms 이후에 이행되어야 한다. 

function delay(ms) {
  // 여기에 코드 작성
}

delay(3000).then(() => alert('3초후 실행'))

 

* 연습과제 2

 

 

728x90

'프론트엔드 > Javascript' 카테고리의 다른 글

비동기 - async, await  (0) 2024.03.18
비동기 - 프로미스 체이닝  (0) 2024.03.15
비동기 - 콜백  (0) 2024.03.13
프로토타입 상속  (0) 2024.03.08
요소의 좌표 계산하기  (0) 2024.02.26