Callback 함수
아래와 같은 setTimeout
예제에서 최상위 레벨의 실행이 끝나면 타임아웃 이후 실행된다. 그러나 분명하게 실행 순서를 정하고 싶은 경우(thrid
함수를 정해진 시간 이후 실행시키고싶은 경우)라면 비동기 코딩 방법을 사용해야 한다.
function first() {
console.log(1)
}
function second() {
setTimeout(() => {
console.log(2)
}, 0)
}
function third() {
console.log(3)
}
// Execute the functions
first()
second()
third()
Output
1
3
2
여기서 타임아웃은 데이터를 갖고 있는 비동기 API 콜을 말한다. API 콜로부터 얻은 데이터를 활용하기 위해서는 그 데이터가 먼저 반환 되었는지 확인해야 한다.
이러한 문제를 다루는 전통적인 방법이 바로 callback(콜백) 함수이다. 콜백 함수는 특이한 문법을 가지지 않는다. 콜백 함수는 다른 함수의 인자로 전달되는 함수이다. 다른 함수를 인자로 갖는 함수를 고차 함수라고 부른다. 이러한 정의에 따르면, 그 어떤 함수라도 인자로 전해져서 콜백함수가 될 수 있다. 콜백 함수는 사용자가 비동기 목적으로 사용해야만 비동기로 동작한다. 콜백함수라고 해서 무조건 비동기로 동작하는 것은 아니다.
다음은 고차 함수와 콜백함수에 관한 간단 예제이다.
// 함수
function fn() {
console.log('Just a function')
}
// 다른 함수를 인자로 받는 함수 - 고차함수
function higherOrderFunction(callback) {
// 인자로 전해진 함수를 실행시킬 때 , 그 함수를 콜백 함수라고 부른다
callback()
}
// 함수를 인자로 전달하기
higherOrderFunction(fn)
이 코드에서 fn
함수와 callback
을 인자로 받는 고차 함수를 정의하고, fn
을 콜백으로써 고차 함수에 전달한다.
코드를 실행시키면 다음과 같은 결과가 나온다
Output
Just a function
setTimeout
예제로 돌아가보자
function first() {
console.log(1)
}
function second() {
setTimeout(() => {
console.log(2)
}, 0)
}
function third() {
console.log(3)
}
second
함수의 비동기 동작이 완벽하게 종료되고 나면 thrid
를 실행하고자 한다. 맨 위의 예제 처럼 first
,second
,third
를 순차적으로 실행시는 대신에 thrid
함수를 second
함수의 콜백 함수로 전달한다. second
함수는 비동기 동작이 완전히 끝난 이후 전달 받은 콜백 함수를 실행시킨다.
// Define three functions
function first() {
console.log(1)
}
function second(callback) {
setTimeout(() => {
console.log(2)
// Execute the callback function
callback()
}, 0)
}
function third() {
console.log(3)
}
first()
second(third)
Output
1
2
3
첫번째로 1
이 출력되고 타이머가 종료되면 차례로 2
와 3
이 출력된다. 함수를 콜백함수로써 전달했으므로 비동기 WebAPI(setTImeout
)이 완료되기 전까지 성공적으로 함수의 실행을 딜레이 시킬 수 있다.
여기서 가져가야할 키는 콜백함수는 비동기가 아니라는 것이다. setTimeout
은 비동기 동작을 핸들링 하는 비동기 WebAPI이다. 반면 콜백함수는 그저 컬비동기 함수의 성공, 실패에 따라 처리해야 할 정보를 알려주는 것이다.
여기까지 콜백 함수에 대해서 배웠고, 다음 섹션은 너무 많은 중첩 콜백 함수로 인한 문제를 다룬다.
중첩된 콜백과 콜백 지옥
콜백 함수는 다른 어느것이 완벽하게 완료될 때 까지 실행을 미룰 수 있는 효과적인 방법이다. 그러나 리턴된 데이터끼리 의존하는 요청들이 많다면 중첩된 콜백으로 인해서 코드가 지저분해진다. 이것은 초기 자바스크립트 개발자에게 큰 절망을 가져왔고 결과적으로 이런 중첩된 콜백들을 콜백 지옥 혹은 파멸의 피라미드(pyramid of doom)이라고 불린다.
다음은 중첩 콜백에 관한 예제이다.
function pyramidOfDoom() {
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
}, 500)
}, 2000)
}, 1000)
}
Output
1
2
3
각각의 새로운 setTimeout
는 고차 함수 안에 중첩되어있고 이는 점점 깊어지는 콜백 피라미드를 만든다. 위는 간단한 예제이지만 실제 세상의 비동기 코드에서는 훨씬 더 복잡해진다. 비동기 코드는 에러 처리를 수행한다음 각 응답으로 부터 얻은 데이터를 다른 요청으로 전달해야 한다. 이러한 작업을 콜백함수로 한다면 코드의 가독성과 유지보수를 어렵게 한다.
다음은 콜백 지옥의 예제이다.
// Example asynchronous function
function asynchronousRequest(args, callback) {
// Throw an error if no arguments are passed
if (!args) {
return callback(new Error('Whoa! Something went wrong.'))
} else {
return setTimeout(
// Just adding in a random number so it seems like the contrived asynchronous function
// returned different data
() => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
500,
)
}
}
// Nested asynchronous requests
function callbackHell() {
asynchronousRequest('First', function first(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest('Second', function second(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest(null, function third(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
})
})
})
}
// Execute
callbackHell()
이 코드에서 모든 함수에 대한 response
와 error
처리에 관한 모든 함수를 만들어야 하는데 이러한 과정을 통해서 callbackHell
은 매우 복잡해보이게 된다.
위 코드를 실행시킨 결과는 다음과 같다.
Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13
이러한 방식의 비동기 코드 처리는 따라가기에 어렵다. 그 결과 ES6에 들어서 promise라는 개념이 나타났다.
Promise
Promise(프로미스)는 비동기 함수의 완료를 나타낸다. 그것은 미래에 리턴할 오브젝트이다. 프로미스는 콜백 함수와 같은 목적을 가지지만 좀 더 가독성이 좋은 추가 기능들을 갖고있다. 자바스크립트 개발자로서 프로미스를 생성하는것 보다는 프로미스를 사용하는 것에더 많은 시간을 사용할것이다. 보통의 비동기 WebAPI들이 개발자가 사용하도록 프로미스를 리턴하기 떄문이다. 이 섹션에서는 프로미스를 생성하고 사용하는 두가지 방법 모두를 다룬다.
프로미스 생성하기
new Promise
를 통해서 새로운 프로미스를 생성하고, 꼭 함수로 초기화 시켜준다. 프로미스로 전달되는 함수는 resolve
와 reject
를 파라미터로 가진다. resolve
와 reject
함수는 성공과 실패 처리를 다룬다.
프로미스 선언하기
// Initialize a promise
const promise = new Promise((resolve, reject) => {})
초기화된 프로미스를 브라우저 콘솔에 찍어보면 프로미스가 pending
과 undefined
값을 가진 것을 확인할 수 있다.
Output
__proto__: Promise
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined
프로미스에 어떠한것도 셋팅되지 않기 때문에 영원히 pending
상태이다. 프로미스의 결과를 테스트 하기 위한 첫번째 단계는 값을 통해서 resolve하여 프로미스를 수행하는 것이다.
const promise = new Promise((resolve, reject) => {
resolve('We did it!')
})
다시 브라우저 콘솔에서 프로미스를 확인하면 상태가 fulfiled
인것을 볼 수 있고 resolve
로 전달된 값을 확인 가능하다.
Output
__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: "We did it!"
프로미스는 리턴한 값을 가진 오브젝트이다. 성공적으로 이행된 후 value
는 undefined
로 채워진다.
프로미스는 세개의 상태를 가진다.
- Pending: resolve 혹은 reject 되기 전의 초기 상태
- Fulfilled: 성공적으로 수행한 상태, 프로미스가 resolve되었음.
- Reject: 실패 상태, 프로미스가 reject 되었음.
프로미스가 fulfill되거나 reject된 이후에는 프로메스가 셋팅된다.
프로미스 사용하기
프로미스는 resolve
에 도달하면 실행되는 then
메소드를 가진다. then
은 인자로 프로미스의 값을 리턴한다.
promise.then((response) => {
console.log(response)
})
Output
We did it!
프로미스는 We did it!
라는 [[PromiseValue]]
를 가진다. 이 값은 익명 함수를 통해 response
로써 전달된다.
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})
// Log the result
promise.then((response) => {
console.log(response)
})
then
은 setTimeout
이 성공적으로 수행된 2000ms이후에 response
가 로그에 나타나도록 보장한다.
프로미스는 하나 이상의 비동기 동작을 수행하기 위해서 데이터를 체이닝 될수도 있다. then
에서 리턴된 값이 또 다른 then
의 값으로 전달된다.
// Chain a promise
promise
.then((firstResponse) => {
// Return a new value for the next then
return firstResponse + ' And chaining!'
})
.then((secondResponse) => {
console.log(secondResponse)
})
두번째 then
까지 수행한 결과는 다음과 같다.
Output
Resolving an asynchronous request! And chaining!
then
이 체이닝 될 수 있으므로 프로미스는 중첩되지 않았고 이는 콜백함수보다 동기적으로 소비되는것 처럼 나타난다. 이는 코드를 읽기 쉽게 해주고 이는 유지보수와 검증을 쉽게 만들어준다
프로미스 에러 처리
API가 다운되거나 허용되지 않은 요청이 보내질 경우 발생되는 에러를 처리해야할 때가 있다. 프로미스는 성공과 실패에 관한 모든 처리를 할 수 있다.
아래의 getUsers
함수는 프로미스에게 플래그를 전달하고 프로미스를 리턴한다.
function getUsers(onSuccess) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Handle resolve and reject in the asynchronous API
}, 1000)
})
}
만약 전달받는 onSuccess
가 true
일 경우, 타임아웃이이 어떠한 데이터와 함께 이행된다. false
일 경우 에러와 함께 reject된다.
function getUsers(onSuccess) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Handle resolve and reject in the asynchronous API
if (onSuccess) {
resolve([
{id: 1, name: 'Jerry'},
{id: 2, name: 'Elaine'},
{id: 3, name: 'George'},
])
} else {
reject('Failed to fetch data!')
}
}, 1000)
})
}
성공적인 결과에서는 샘플 유저 데이터를 담고 있는 자바스크립트 객체를 리턴한다.
에러를 처리하기 위해서는 catch
메소드를 사용해야 한다. 이는 error
파라미터를 받는 실패 콜백을 전달한다.
getUser
에 false
를 전달하고, 성공을 위해 then
, 에러 처리를 위해 catch
를 사용한다
// Run the getUsers function with the false flag to trigger an error
getUsers(false)
.then((response) => {
console.log(response)
})
.catch((error) => {
console.error(error)
})
에러가 트리거되었으므로 then
은 무시되고 catch
에서 에러를 처리한다.
Output
Failed to fetch data!
만약 플래그를 true
로 전달하면 catch
는 무시되고 then
이 실행된다.
// Run the getUsers function with the true flag to resolve successfully
getUsers(true)
.then((response) => {
console.log(response)
})
.catch((error) => {
console.error(error)
})
Output
(3) [{…}, {…}, {…}]
0: {id: 1, name: "Jerry"}
1: {id: 2, name: "Elaine"}
3: {id: 3, name: "George"}
프로미스 객체는 3개의 핸들러 메소드를 가진다.
- then() :
resolve
를 다룬다. 프로미스를 리턴하고onFulfilled
함수를 비동기적으로 호출한다. - catch() :
reject
를 다룬다. 프로미스를 리턴하고onReject
함수를 비동기적으로 호출한다. - finally() : 프로미스가 완료되면 호출된다. 프로미스를 리턴하고
onFinally
함수를 비동기적으로 호출한다.
보통 브라우저의 WebAPI나 서드파티 라이브러리들은 프로미스를 제공하므로, 개발자는 이를 사용할 줄 알아야 한다.
Async/Await
프로미스와 then
을 사용하여 콜백 피라미드 보다 더 쉬운 비동기 동작을 다룰 수 있지만, 어떤 개발자들은 비동기 코드를 동기적인 포맷으로 작성하는것을 선호했다. 이러한 니즈를 충족하기 위해서 ES7에서 async
함수와 await
키워드가 소개 됐다.
async
함수는 비동기 코드를 동기적으로 보이도록 작성할 수 있게 해준다. async
함수는 여전히 프로미스 아래서 동작하지만 더 전통적인 자바스크립트 문법을 보여준다.
async
함수는 async
키워드로 만들 수 있다.
// Create an async function
async function getUser() {
return {}
}
아직 함수가 비동기를 다루고 있지 않지만, 이는 전통적인 함수와 다르게 처리된다. 만약 함수를 실행하면 프로미스를 리턴받게 된다.
console.log(getUser())
Output
__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: Object
이 말은 async
함수를 then
을 사용하여 프로미스와 똑같이 처리할 수 있다는 것이다.
getUser().then((response) => console.log(response))
Output
{}
async
함수는 await
이라는 연산자를 통해서 프로미스를 다룰 수 있다. await
은 async
함수와 내에서 사용할 수 있으며 async
의 지정된 코드를 실행하기 전까지 기다리게 된다.
// Handle fetch with async/await
async function getUser() {
const response = await fetch('https://api.github.com/users/octocat')
const data = await response.json()
console.log(data)
}
// Execute async function
getUser()
await
은 data
가 완전히 채워지기 전까지 기록되지 않도록 한다. 이제 then
을 사용할 필요 없이 data
는 getUser
함수 안에서 처리가 가능하다.
Output
login: "octocat",
id: 583231,
avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4"
blog: "https://github.blog"
company: "@github"
followers: 3203
...
에러 처리는 then
과 catch
대신 try/catch
패턴을 사용할 수 있다.
// Handling success and errors with async/await
async function getUser() {
try {
// Handle success in try
const response = await fetch('https://api.github.com/users/octocat')
const data = await response.json()
console.log(data)
} catch (error) {
// Handle error in catch
console.error(error)
}
}
만약 에러가 있다면 catch
구문이 실행된다.
모던 비동기 자바스크립트 코드는 대게 async/await
문법을 사용하지만 이것이 어떻게 프로미스와 동작하는지 알고 있는것은 중요하다.
공부하기 위해 원문을 개인적으로 번역한 글입니다.