자바스크립트의 비동기 처리(콜백함수

Kiwoong Park·2022년 9월 11일
0

자바스크립트의 코드 동작 방식(동기? 비동기?)

동기(synchronous)

말 그대로 어떤 요청을 보낸 후 그 요청에 대한 응답을 받아야 다음 동작을 실행하는 것을 동기라고 한다.
예를 들어 c언어로 아래와 같이 코드를 작성했을 때 콘솔에 산출되는 예상 결과는 당연히 end \n Let's start로 윗줄부터 작성된 for loop가 먼저 실행되어, 코드가 완료된 후, 그 다음 코드가 실행되는 것을 동기적으로 작동하는 코드라고 한다.

for(int i=0; i<1000; i++){
	if (i==999) printf("end\n");
}
printf("Let's start")
//end
//Let's start

비동기(asynchronous)

요청을 보낸 후 응답에 관계없이 다음 동작을 실행하는 것을 비동기라고 한다. 즉, 맨 윗줄부터 차례로 실행시킨 코드 중 응답이 오래 걸리는 코드가 있다면 그 코드의 응답여부와 상관없이 다음 코드가 실행되는 것을 비동기적으로 작동하는 코드라고 한다.

동기? 비동기?

위의 동기와 비동기 정의에서 알 수 있듯이 자바스크립트 역시 기본적으로는 동기적으로 동작한다. 즉, 여느 프로그래밍 언어와 같이 순차적으로 코드를 수행하고 응답을 받은 후 다음 코드를 실행하는 것이 기본이다.

하지만 우리(클라이언트)가 어떤 서버로 데이터를 요청(Ex. 웹서버 내 HTML 파일 조회)을 하였는데, 해당 데이터 용량이 너무 커서 응답시간까지 오래걸린다. 이때 자바스크립트가 동기적으로 작동한다면 웹 브라우저 화면에 아무것도 표시가 되지 않다가 모든 데이터가 다 응답해 화면에 뜰 때까지 사용자는 그냥 기다려야 하는 상황이 올 수 있다. 즉, 사용자 경험이 저하되는 것이고 이를 해결하기 위해 동기적으로 실행하지 않고, 빠른 작업이 있다면 먼저 응답을 받을 수 있도록 비동기 처리를 해줘야 하는 것이다.

비동기 처리?

설명과 같이, 특정 코드의 실행 완료 여부와 관계없이 다음 코드를 수행하는 것을 비동기라고 하였다. 하지만 이랬을 때의 문제는 클라이언트가 서버에 데이터를 요청했을 때, 그 데이터가 다 도착도 하기 전에 그냥 화면에 먼저 띄워서 없는 데이터를 사용자에게 보여주는 경우가 생길 수 있다.

이러한 비동기의 문제를 처리하는 작업이 비동기 처리이다.
자바스크립트의 대표적인 비동기 처리 방법은 **콜백 함수, Promise, async await` 등이 있다.

콜백 함수

콜백; Callback은 단어 그대로 해석하자면, "답신전화"로 의미를 부여하자면 나중에 실행되는 코드를 의미한다.
A()라는 함수가 있을 때, 인자로 변수가 아닌 어떤 함수를 넣어주었다고 가정하자.
자바스크립트에서 함수는 *'일급 객체'이기 때문에 인자로 함수를 넣어주는 것이 가능하다.
A 함수의 모든 명령을 실행한 후 마지막으로 넘겨 받은 인자 callback을 실행하는 메커니즘이 콜백이고, 여기서 인자로 들어가는 함수를 콜백 함수라고 한다.
일급 객체: 다른 객체에 일반적으로 적용할 수 있는 연산을 모두 지원하는 객체. 일급 객체는 다른 함수의 인자(파라미터)로 넣을 수 있고, 반환 값으로 쓰일 수도 있고, 변수 안에 넣을 수도 있음.

즉, 함수 안에서 실행하는 또 다른 함수, 다른 함수에게 매개변수로 전달되는 함수를 콜백 함수라고 하며 콜백 함수 밖의 함수의 모든 명령을 실행한 후 나중에 실행하는 함수로 정의할 수 있겠다.

  • 일반적으로 비동기적인 작업이 완료되거나 특정 이벤트가 발생했을 때 호출하는 역할을 수행한다.

주요 특징

1.비동기적인 동작

콜백 함수는 비동기적인 작업에서 주로 사용됨. 예를 들어 파일을 읽거나, 네트워크 요청을 보낼 때, 작업이 완료되면 콜백 함수가 호출되어 결과를 처리하거나 동작을 수행한다.

2.매개변수로 전달

콜백 함수는 다른 함수의 매개변수로 전달되는데 이를 통해 원래 함수는 콜백 함수를 내부적으로 호출하여 작업이 완료되었을 때 적절한 처리를 할 수 있음.

3.이벤트 처리

이벤트가 발생했을 때 시스템 또는 프레임워크에서 등록된 콜백 함수를 호출하여 해당 이벤트에 대한 처리를 수함함.

4.제어의 역전

제어의 역전은 함수의 호출 시점과 흐름을 호출하는 측에서 결정하게 되는 것을 으미한다. 이를 통해 비동기적인 작업의 효율적인 처리와 동시성을 구현할 수 있음.

setTimeout(()=> { //내장함수 setTimeout(callback, delayTime)
  console.log('todo: First work!');
}, 3000);

setTimeout(()=> {
  console.log('todo: Second work!');
}, 2000);

// 결과
// todo: Second work!
// todo: First work!

setTimeout() 함수는 자바스크립트 내장 함수로,
콜백 함수와 지체할 시간을 인자로 받아 인자로 받은 시간만큼 기다렸다가 콜백 함수를 실행하는 함수.
자바스크립트는 이벤트 중심 언어이기 때문에 어떤 이벤트가 발생하고, 그에 대한 결과가 올 때까지 기다리지 않고 다음 이벤트를 계속 실행한다.

  • 위의 예제처럼 First work! 로그를 찍는 함수를 먼저 호출했지만 지체할 시간을 3을 받아 3초가 걸리고,
  • Second work! 로그를 찍는 함수는 2초가 걸리기 때문에 Second work! 로그를 찍는 함수를 먼저 실행되어 화면에 표시된다.

즉, 비동기 처리는 동시에 작업을 시작하면서 먼저 끝나는 작업부터 노출

setTimeout(() => {
  setTimeout(() => {
    console.log('todo: Second work!');
  }, 2000);
  console.log('todo: First work!');
}, 3000)
// 결과
// todo: First work!
// todo: Second work!

SetTimeout() 안에 콜백 함수로 두번째 SetTimeout()이 있기 때문에 3초를 기다리고 두번째 SetTimeout()과 함께 todo: First work!를 실행한다.
실행 결과 3초 후 'First work!'가 출력되고,
바로 다음 2초 후 'Second work!'가 출력된다.

여기서 'First work!' 로그를 찍는 setTimeout() 함수와 'Second work!'를 로그로 찍는 setTimeout() 함수는 순차적으로 처리되지 않기 때문에 '비동기' 작업. First work! 로그를 찍는 함수의 작업이 끝난 뒤 Second work! 로그를 찍는 함수를 실행하고 싶으면 위 예제처럼 '콜백 함수'를 이용해 비동기 작업을 동기적으로 처리해주어야 한다.

function fakeSetTimeout(callback) {
  callback();
}

console.log(0);
fakeSetTimeout(function () {
  console.log('hello');
});

console.log(1);
// 실행 결과
// 0
// hello
// 1

fakeSetTimeout() 함수는 사용자 정의 함수. 인자로 콜백 함수를 받아 실행해줌.
세 가지 실행부
1. 0을 출력,
2. fakeSetTimeout()에서 콜백으로 'hello'를 넘겨주는 코드,
3. 1을 출력하는 코드가 있음.
실행 결과에 따라 동기적으로 처리됨. 위 세 가지 실행부는 모두 동기적이기 때문에, 콜백 큐를 거치지 않고 모두 콜 스택을 거쳐서 실행됨.

console.log(0);
setTimeout(function () {
  console.log('hello there');
}, 0);

console.log(1)

setTimeout() 함수는 웹 브라우저에서 제공하는 API로 자바스크립트는 인터페이스만 제공할 뿐 동작은 외부에서 받아오게 된다.
setTimeout()에 인자로 넘겨준 console.log('hello there')가 비동기적으로 처리된다.
0 초 뒤 setTimeout()을 스택에 넣고 실행하는 것이 아닌 0초 뒤에 콜백 큐에 넣게된다.
콜 스택에 있는 작업(0과 1을 출력하는 작업)이 모두 끝난 후,
이벤트 루프콜백 큐에 있는 작업을 살펴보고
작업을 콜 스택에 올려 실행

fakeSetTimeout() 함수처럼 자바스크립트 내부에서 처리되는 연산이면 동기적으로 처리되고,
setTimeout() 함수처럼 외부에서 처리되는 연산이면 비동기적으로 처리됨.
외부적으로 처리되는 함수는 서버에서 데이터 가져오기, 타이머 등의 외부 API 등이 있음.

Promise

Promise는 코드의 중첩이 많아지는 콜백 지옥을 벗어나게 해주는 객체.
Ex. 얼리어답터 A씨가 새로 출시되는 맥북을 구매하고 싶은데 출시 일, 시간을 정확히 알려주지 않아 출시되었는지 사이트에 접속하여 확인을 한다면? 출시될 때까지 시간 날때마다 접속하여 사이트를 확인하는 낭비가 발생한다.
만약 맥북이 출시되었다고 응답이 오면 바로 맥북을 구매할 수 있게 된다.
이런 맥북을 구매한 행동은 어떤 로직이 요청에 대한 응답을 받은 후 실행한 함수(작업)에 비유할 수 있다.
즉, 애플에서 맥북이 출시되면 문자를 보내주는 서비스를 제공한다면?, 알림을 보내준다고 약속을 하는 것과 같은 역할이 Promise 객체.

// work 함수는 sec 변수와 callback 함수를 인자로 받아서 setTimeout() 비동기 함수를 실행하는 함수이다.
function work(sec, callback) {
  setTimeout(() => {
    callback(new Date().toISOString());
  }, sec * 1000);
};

work(1, (result) => {
  console.log('첫 번째 작업', result);
});

work(1, (result) => {
  console.log('두 번째 작업', result);
});

work(1, (result) => {
  console.log('세 번째 작업', result);
});
// 실행 결과
// "첫 번째 작업", "2022-09-19T08:04:36.066Z"
// "두 번째 작업", "2022-09-19T08:04:36.066Z"
// "세 번째 작업", "2022-09-19T08:04:36.067Z"

이와 같이 코드를 짜면 각 작업마다 1초의 시간이 걸리는 일이 있을 때, 1, 2, 3 모두 같은 작업시간에 끝나버린다.

function work(sec, callback) {
  setTimeout(() => {
    callback(new Date().toISOString());
  }, sec * 1000);
};

work(1, (result) => {
  console.log('첫 번째 작업', result);

  work(1, (result) => {

    work(1, (result) => {
      console.log('세 번째 작업', result);
    });

    console.log('두 번째 작업', result);
    
  });
  
});
// 실행 결과
// 첫 번째 작업 2022-09-11T14:57:04.000Z
// 두 번째 작업 2022-09-11T14:57:05.018Z
// 세 번째 작업 2022-09-11T14:57:06.022Z

비동기적으로 동작하기 때문에 1 -> 2 -> 3 순서대로 작업이 이뤄진다. 하지만 3개의 inner tab으로 인해 가독성이 떨어져 실제 결과를 확인하는 것이 어렵다.
이때 필요한 것이 Promise객체.
Promise는 콜백 지옥을 탈출하게 해주는 자바스크립트 API. Resolve()는 다음 작업으로 넘어가게 해주고, Reject()는 다룰 수 있는 오류를 호출한다.

function workP(sec) {
  // Promise의 인스턴스를 반환하고
  // then에서 반환한 것을 받는다.
  return new Promise((resolve, reject) => {
    // Promise 생성시 넘기는 callback = resolve, reject
    // resolve 동작 완료시 호출, 오류 났을 경우 reject
    setTimeout(() => {
      resolve(new Date().toISOString());
    }, sec * 1000);
  });
}

workP(1).then((result) => {
  console.log('첫 번째 작업', result);
  return workP(1);
}).then((result) => {
  console.log('두 번째 작업', result);
});

// 실행 결과
// 첫 번째 작업 2022-09-11T15:07:48.716Z
// 두 번째 작업 2022-09-11T15:07:49.731Z

workP()라는 함수는 new 키워드를 통해 Promise의 인스턴스를 생성하여 반환.
Promise를 생성할 때 resolvereject를 넘기게 되는데,
여기서 resolve는 위에서 배운 콜백 함수와 비슷한 것으로 workP()의 요청이 성공하게 되는 경우 resolve 함수를 호출하고,
실패할 경우 reject 함수를 호출하게 됨.
마지막 호출부에서 볼 수 있듯이 workP()를 호출하고 반환되는 Promise의 인스턴스를 넘겨 받아 resolve를 통해 받은 결과 값을 사용할 수 있게 됨.
then, catch 메서드가 또 Promise 객체를 반환하는 경우 또 then, catch로 연결하여 사용할 수 있다. 이를 Method Chaining라 한다.
Method Chaining을 이용해 callback을 순차적으로 지정해 비동기 처리를 해줌.
첫 번째 then()에서, 두 번째 then()에서 받고 싶은 결과 값을 반환하고 두 번째 then()에서 이를 받음.
이렇게 되면 첫 번째 then()이 반드시 끝나고 무언가를 반환해주어야 다음 then()에서 받은 결과로 무언가를 실행할 수 있으니 then()을 붙인 순서대로 처리된다.

async/await

위에서 보다시피 콜백 지옥을 벗어낫으나 익숙하지 않은 나에겐 Promise 지옥같은 느낌이랄까..
ES7.6부터 사용할 수 있는 문법으로 Promise의 단점인 가독성이 좋지 않은 점을 보완해주어 로직을 파악하기 쉽고 디버깅을 쉽게 만들어준다.
Promise와 전혀 다른 개념은 아니고 Promise를 사용하여 구현하는 패턴. async/await는 비동기 코드의 겉모습과 동작을 좀 더 동기코드와 유사한 형태로 만들어준다.

// getJson 함수:  Load JSON-encoded data from the server using a GET HTTP request.
// promise 사용패턴
const makeRequest = () =>
	getJSON()
	.then(data => {
      console.log(data);
      return "done";
    })

makeRequest();

// async/await 패턴으로 작성된 코드
const makeRequest = async () => {
  console.log(await getJSON());
  return "done";
}

makeRequest();

async/await를 사용하면 new PromisePromise 객체를 선언하고 resolve, reject를 넘겨주는 부분을 숨기기 때문에 Promise를 사용하는 것보다 코드 양도 줄일 수 있다.
또한 try/catch를 통해 오류를 다룰 수도 있고 중첩 현상도 해결이 가능하다.

function workP(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date().toISOString());
    }, sec * 1000);
  });
}

function justFunc() {
  return 'just Function';
}

async function asyncFunc() {
  return 'async Function';
}

console.log(justFunc());
console.log(asyncFunc());
console.log(workP());

// 실행 결과
// just Function
// Promise { 'async Function' }
// Promise { <pending> }

asyncFunc()workP()는 둘 다 Promise 객체를 반환하는 것을 볼 수 있다.

function workP(sec) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('workP function');
    }, sec * 1000);
  });
}

async function asyncFunc() {
  const result_workP = await workP(3);
  console.log(result_workP);
  return 'async Function';
}

asyncFunc().then((result) => {
  console.log(result)
})

// 실행 결과
// 3초를 기다린 후 아래 구문이 차례대로 출력
// workP function 
// async Function

await 사용법은 async 키워드를 붙인 함수 안에 lock을 걸어 놓고 싶은 부분에 await를 붙이기만 하면 됨.
위 코드에서는 workP() 함수를 호출하는 부분에 await를 붙임.
원래 workP() 함수는 setTimeout() 함수를 이용하기 때문에 비동기적으로 처리되지만 await를 붙여 workP(3) 함수가 완료되기 전까지 그 밑의 구문인 return 'async Function'은 실행하지 않게 된다.
async/await를 이용하게 되면 비동기로 처리하고 싶은 함수에 async를 붙이고, 비동기 처리를 할 특정 부분에 await를 붙이기만 하면 되니 Promise보다 직관적이다.

비동기 상황에서의 예외 처리

오류(Error) vs 예외(Exception)

오류는 메모리 부족, 스택 오버플로우 등이 발생하게 되면 복구하기 쉽지 않은 심각한 오류를 말하고, "예외"는 발생하더라도 수습할 수 있을 정도의 심각하지 않은 오류를 말함.

// 사용자 정의 오류
function sum(a, b) {
  if (typeof a !== 'number' || typeof y !== 'number') {
    throw 'type of arguments must be number type';
  }
  console.log( a + b )
}

sum(1, '4');
// Uncaught type of arguments must be number type

Uncaught 즉, 오류가 있는데 잡지 못했다. = 예외 처리를 해주지 않았다는 의미.
이렇게 발생한 오류에 대해 어떤 처리를 해줘야만 프로그램이 정지하지 않고 다음 코드로 넘어갈 수 있음.

일반적인 예외 처리

// catch해 주지 않은 부분은 실행되지 않음
function f2() {
  console.log('this is f2 start');
  throw new Error('오류'); // Error 객체 - 해당하는 콜 스택 정보가 담겨 있습니다.
  console.log('this is f2 end'); // 실행되지 않습니다.
}

function f1() {
  console.log('this is f1 start');
  try {
    f2();
  } catch (e) {
    console.log(e);
  }
  console.log('this is f1 end');
}

f1();

// 실행 결과
// this is f1 start
// this is f2 start
// Error: 오류
//    at f2 (<anonymous>:3:9)
//    at f1 (<anonymous>:10:5)
//    at <anonymous>:17:1
// this is f1 end

f1 함수에서 f2를 호출하는데,
f2 함수에서 오류가 발생했으면 오류가 발생한 지점 이후의 콜 스택에 쌓인 작업은 실행되지 않는다( 위 예제에서는 console.log('this is f2 end'); )
따라서 예외가 발생할 수 있는 부분을 try() 함수에 넣고, try() 내부, 즉 f2()에서 예외가 발생한다면,
그 예외를 어떻게 처리할 것인지 catch() 부분에 작성하여 catch 부분에서 처리를 해준다.
그리고 오류를 throw를 통해 발생시킬 때, "Error occur!"와 같이 string으로 작성하지 않고,
new 를 통해 Error 객체를 생성하고 그 안에 인자로 메시지를 작성해준다.
오류 객체로 오류를 발생시키면 해당하는 콜 스택 정보가 담겨 있기 때문에 어떤 파일의 몇 번째 줄에 오류가 발생했는지 확인할 수 있게 된다.

profile
You matter, never give up

0개의 댓글