다른 프로그래밍 언어에서 넘어온 개발자라면 코드 실행을 잠시 멈추거나 지연시키기 위해 sleep()이라는 기본 내장 메서드를 사용해 본 경험이 있을 것입니다. 하지만 자바스크립트(JavaScript)에는 이러한 네이티브 sleep() 메서드가 존재하지 않습니다.
이 글에서는 자바스크립트에서 sleep 기능을 흉내 낼 수 있는 두 가지 방법, 즉 Promise와 async/await를 중심으로 자세히 알아보겠습니다.
비동기 함수 이해하기
Promise나 async/await를 다루기 전에 먼저 setTimeout() 메서드를 간단히 짚고 넘어가야 합니다. 이 메서드를 통해 자바스크립트에 왜 Promise가 필요한지 자연스럽게 이해할 수 있기 때문입니다.
자바스크립트는 싱글 스레드 언어
자바스크립트가 싱글 스레드(single-threaded) 언어라고 말할 때, 이는 자바스크립트가 하나의 호출 스택(call stack)과 하나의 메모리 힙(memory heap)만 가진다는 의미입니다. 쉽게 말해 자바스크립트는 코드를 한 번에 한 줄씩 순서대로 읽으며, 현재 줄의 실행이 끝나야 다음 줄로 넘어갈 수 있습니다. 그렇기 때문에 자바스크립트는 기본적으로 동기(synchronous) 방식으로 동작하며, 때로는 코드를 비동기(asynchronous)로 처리하기 위한 우회 방법이 필요합니다.
동기 코드 vs 비동기 코드
간단한 예제를 살펴보겠습니다.
// 동기 코드
console.log("첫 번째로 출력됩니다")
console.log("두 번째로 출력됩니다")
console.log("세 번째로 출력됩니다");결과는 아주 직관적입니다. 각 console.log는 순서대로 실행되기 때문에 작성된 순서 그대로 출력됩니다.
그런데 만약 두 번째와 세 번째 줄의 출력 순서를 바꾸고 싶다면 어떻게 해야 할까요? 이때 setTimeout()을 활용할 수 있습니다.
// setTimeout 사용 예시
console.log("첫 번째로 출력됩니다")
setTimeout(() => {
console.log("세 번째로 출력됩니다")
}, 1000);
console.log("두 번째로 출력됩니다");setTimeout()은 스레드를 차단(blocking)하지 않고 자바스크립트 함수를 실행할 수 있게 해줍니다. 첫 번째 인수는 일정 시간이 지난 후 실행될 콜백 함수이며, 두 번째 인수는 밀리초(ms) 단위의 대기 시간입니다.
setTimeout()은 다음 과정을 거쳐 다른 언어의 sleep 메서드와 유사하게 동작합니다.
- 자바스크립트 엔진이
setTimeout()을 실행하는 순간 백그라운드에 타이머를 설정합니다. - 타이머가 카운트다운되는 동안 나머지 코드를 계속 실행합니다.
- 타이머가 0이 되면 콜백 함수를 실행합니다.
setTimeout()의 동작 원리를 이해하는 것은 Promise와 async/await를 제대로 파악하기 위한 필수 단계입니다. 이제 Promise에 대해 살펴보겠습니다.
Promise
Promise 생성하기
Promise는 비동기 로직을 수행하는 한 가지 방법입니다. Promise 생성자는 resolve와 reject라는 두 개의 매개변수를 가진 콜백 함수를 인수로 받습니다. 이 콜백 함수 안에는 실행이 끝난 후 결과값과 함께 resolve 또는 reject 함수를 호출하는 로직이 들어갑니다.
console.log("promise 이전")
let promise = new Promise((resolve, reject) => {
let resolvedFlag = false;
// 테스트를 위해 결과를 의도적으로 조작하기 위한 플래그입니다.
console.log("first");
console.log("second")
console.log("third")
console.log("fourth");
resolvedFlag = true; // 모든 console.log가 끝나면 true로 변경
if(resolvedFlag) { // true면 resolve 함수 호출
resolve("Promise resolved");
console.log("promise 이후");
} else { // false면 에러 객체와 함께 reject 함수 호출
reject(new Error("Promise failed"));
console.log("promise 이후");
}
});위 코드는 간단한 Promise 예제입니다. Promise는 다음 세 가지 상태를 가질 수 있습니다.
pending(대기) – 아직 resolve도 reject도 되지 않은 상태로, Promise의 초기 상태입니다.
resolved(이행) – 성공적으로 실행이 완료된 상태입니다.
rejected(거부) – 실행 중 오류가 발생한 상태입니다.
위 예제에서 resolvedFlag 값을 true에서 false로 바꿔보면서 Promise가 이행되는 경우와 거부되는 경우의 차이를 직접 확인해 보세요.
핵심은 이 Promise가 담고 있는 함수가 스크립트 실행을 일시적으로 멈추고, Promise가 resolve 또는 reject 되었을 때 다시 실행을 재개한다는 점입니다.
Promise 응답 사용하기
Promise 인스턴스를 생성한 후에는 then()과 catch()를 사용해 반환된 Promise로부터 응답을 받았을 때의 후속 로직을 정의합니다. 일반적으로 하나의 문장으로 연결해서 작성하며, 전체적인 구조는 다음과 같습니다.
promise.then(func).catch(func);
then과 catch 메서드를 호출하는 괄호 안에는 응답 값을 매개변수로 받는 익명 함수가 들어갑니다.
promise // 앞선 코드에서 생성한 promise
.then(response => { // 성공 응답일 때 실행
// 성공했을 때의 로직
console.log(response);
console.log("promise 이후");
})
.catch(error => { // 여기서 모든 에러를 잡아냅니다.
// 실패했을 때의 로직
console.log(error.message);
console.log("promise 이후");
})Promise는 데이터베이스에 요청을 보내거나 HTTP 요청을 수행할 때 특히 많이 사용됩니다.
Async/Await 함수
sleep()을 흉내 내는 마지막 방법은 async/await 함수를 사용하는 것입니다. 사실 async/await는 Promise에서 사용하던 것과 동일한 비동기 로직을 더 적은 코드로 작성할 수 있게 해주는 문법적 설탕(syntactic sugar)에 가깝습니다.
async 키워드는 ES6 이전의 function 선언식에서는 function 키워드 앞에, ES6 이상의 화살표 함수에서는 매개변수 앞에 위치합니다. 다음 로직으로 넘어가기 전에 반드시 완료해야 하는 작업이 있다면 이 블록 안에 넣으면 됩니다.
const firstFunction = () => { // 비동기로 처리하고 싶은 로직
let resolved = true;
if(resolved) {
let respObj = { message: "Resolved!"};
return respObj.message;
} else {
let errObj = { message: "Error!"};
return errObj.message;
}
}
const asyncExample = async () => {
// 화살표 함수의 매개변수 위치 앞에 async 키워드를 붙입니다.
console.log("함수 호출 중...");
let result = await firstFunction();
// await는 해당 함수의 로직이 담긴 함수 호출 앞에 위치합니다.
// 함수 실행이 끝나기 전까지는 다음 줄로 넘어가지 않습니다.
console.log(result);
return result;
}
asyncExample() // async/await 함수 호출async/await를 사용하면 Promise 기반의 비동기 처리 과정을 거의 동일하게 구현하면서도 코드량을 크게 줄일 수 있습니다.
마무리
이번 글에서는 다른 프로그래밍 언어에 기본으로 존재하는 sleep() 함수를 자바스크립트에서 구현하는 방법을 살펴보았습니다. 먼저 setTimeout()을 통해 자바스크립트 비동기 처리의 개념을 이해하고, 이를 바탕으로 Promise와 async/await를 활용하는 방법까지 알아보았습니다. 이 세 가지 도구를 상황에 맞게 잘 활용하면 자바스크립트에서도 실행 지연과 비동기 흐름 제어를 손쉽게 구현할 수 있습니다.