logo
Posts
비동기 프로그래밍과 콜백

비동기 프로그래밍과 콜백

비동기 프로그래밍은 코드가 순차적으로 실행되지 않고, 작업이 완료될 때까지 기다리지 않고도 다른 작업을 진행할 수 있는 방식을 의미합니다. 이번 글에서는 비동기 프로그래밍과 콜백 함수에 대해 알아보겠습니다.

비동기 프로그래밍

컴퓨터 시스템은 사실 여러 개의 프로그램이 번갈아가며 실행되는 방식으로 동작합니다. 즉, 한 프로그램이 실행되는 동안 다른 프로그램이 대기하고, 대기하던 프로그램이 실행되는 방식입니다. 이 과정은 매우 빠르게 진행되기 때문에 우리에게는 마치 여러 프로그램이 동시에 실행되는 것처럼 보입니다. 하지만 실제로는 시간 분할 방식으로 번갈아가며 실행되는 것입니다.

이러한 단점을 해결하는 방법 중 하나로 멀티 프로세서 머신이 나왔지만, 여기선 잠시 제외하고 진행하겠습니다.

비동기 프로그래밍은 이러한 컴퓨터 시스템의 특성을 활용하여 작업을 수행하는 방식입니다. 프로그램은 보통 인터럽트라는 신호를 통해 프로세서의 주의를 끌어 특정 작업을 중단하고 다른 작업을 처리하도록 합니다.

여기서 자세한 기술적인 다루지 않겠습니다. 다만 프로그램이 네트워크 요청 같은 작업을 기다리는 동안 컴퓨터가 다른 일을 할 수 있도록 비동기적으로 실행된다는 것만 이해하면 됩니다. 네트워크 응답을 기다리는 동안 CPU 를 멈출 수는 없기 때문입니다. 또한 다른 작업을 수행할 수 있기 때문에 시간을 효율적으로 활용할 수 있습니다.

대부분의 프로그래밍 언어는 기본적으로 동기적 실행 방식을 따르며, 일부 언어는 비동기 처리를 지원하는 방법을 제공합니다. C, Java, C#, PHP, Go, Ruby, Swift, Python 등은 모두 기본적으로 동기적입니다. 일부 언어는 스레드 또는 프로세스를 생성해 비동기 작업을 처리하기도 합니다.

자바스크립트의 특성

자바스크립트는 동기적이며 단일 스레드로 동작하는 언어입니다. 즉, 자바스크립트는 병렬로 여러 작업을 수행할 수 없으며, 한 번에 하나의 작업만 처리할 수 있습니다.

예를 들어 보겠습니다.
아래 코드는 순차적으로 실행됩니다.

const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();

위 코드는 a, b, c 변수를 선언하고, c 변수에 a * b 값을 할당한 후, c 값을 출력하고 doSomething 함수를 호출합니다.

자바스크립트는 브라우저 환경에서 처음 개발되었습니다. 그리고 브라우저는 사용자와 상호작용하며, 사용자의 입력에 따라 동적으로 화면을 갱신해야 합니다. 이러한 상황에서 동기적 실행 방식은 사용자 경험을 저해할 수 있습니다.

이를 해결하기 위해 자바스크립트는 비동기 처리를 지원하는 API 를 제공합니다.

Node.js 로 오면서, 이 개념을 더욱 확장하게 되었습니다. 파일 시스템 접근, 네트워크 요청, 데이터베이스 접근 등 다양한 작업을 비동기적으로 처리할 수 있는 환경을 제공합니다.

콜백 함수

자바스크립트에서 비동기 처리를 위해 가장 많이 사용되는 방법은 콜백 함수입니다. 사용자가 버튼을 언제 클릭할지, 네트워크 요청이 언제 완료될지 알 수 없습니다. 따라서 우리는 버튼 클릭 이벤트, 네트워크 요청 완료 이벤트 등을 등록하고, 이벤트가 발생했을 때 실행할 함수, 즉 콜백 함수를 사전에 정의해 두는 것입니다.

콜백 함수는 다른 함수의 인자로 전달되어, 특정 이벤트가 발생했을 때 실행됩니다. 잠시 콜백 함수를 사용한 예시를 살펴보겠습니다.

function fetchData(callback) {
	setTimeout(() => {
		const data = "Hello, world!";
		callback(data);
	}, 1000);
}

위 코드는 fetchData 함수를 정의하고, 1초 후에 "Hello, world!" 라는 데이터를 콜백 함수에 전달합니다.

혹은 다음과 같이 사용할 수 있습니다.

document.getElementById("btn").addEventListener("click", () => {
	fetchData((data) => {
		console.log(data);
	});
});

위 코드는 버튼 클릭 이벤트가 발생했을 때 fetchData 함수를 호출하고, 콜백 함수를 전달합니다.

콜백 함수는 하나의 값처럼 다른 함수에 전달되고, 특정 이벤트가 발생할 때 실행됩니다. 자바스크립트는 일급 함수(first-class functions) 를 지원하므로, 함수를 변수에 할당하거나 다른 함수의 인자로 전달할 수 있습니다. 이를 통해 콜백 함수는 유연하게 다양한 상황에서 사용될 수 있습니다. 일반적으로 웹 페이지 로딩이 완료되었을 때 실행할 코드를 window 객체의 load 이벤트 핸들러에 넣는 것이 일반적입니다.

window.addEventListener("load", () => {
  // 페이지가 완전히 로드되었을 때 실행될 코드
});

타이머 함수의 비동기 처리

자바스크립트에서 타이머 함수는 비동기적으로 동작합니다. setTimeout 함수는 지정된 시간이 지난 후 콜백 함수를 실행합니다. 이는 시간 기반의 비동기 작업을 처리할 때 유용하게 사용됩니다.

setTimeout(() => {
  console.log("2초 후에 실행됩니다.");
}, 2000);

위 예제에서 setTimeout 은 2000밀리초(2초) 후에 콜백 함수를 실행합니다. 이 방식은 주로 지연된 작업을 처리하거나, 네트워크 요청과 같은 비동기 작업 후에 추가적인 작업을 처리할 때 사용됩니다.

이 외에도, 콜백은 다양한 상황에서 사용됩니다. 방금 사용했던 것처럼 사용했던 것처럼 타이머 함수도 콜백을 통해 비동기적으로 동작하기도 하고, 서버에서 데이터를 받아오는 AJAX 요청도 콜백을 통해 처리합니다.

콜백에서의 에러 처리

콜백 함수를 사용할 때는 에러 처리가 매우 중요합니다. 네트워크 요청이나 파일 읽기 작업에서 발생할 수 있는 오류를 처리하지 않으면, 사용자에게 불편을 초래할 수 있습니다. 이를 위해 일반적으로는 에러-우선 콜백(error-first callback) 패턴을 사용합니다.

Node.js 에서는 이 패턴을 자주 사용하며, 콜백 함수의 첫 번째 인자로 에러 객체를 전달합니다. 에러가 없으면 null을 전달하고, 에러가 발생하면 해당 에러 객체가 포함됩니다.

const fs = require("node:fs");
 
fs.readFile("/file.json", (err, data) => {
  if (err) {
    // 오류 발생 시 처리
    console.error("파일을 읽는 중 오류 발생:", err);
    alert("파일을 읽는 중 문제가 발생했습니다. 다시 시도해 주세요.");
    return;
  }
 
  // 오류가 없을 때 데이터 처리
  console.log(data);
});

에러 처리 시, 사용자에게 에러 메시지를 표시하거나 재시도를 안내하는 방식으로 사용자 경험을 개선할 수 있습니다. 예를 들어 네트워크 요청이 실패한 경우, alert 창을 통해 오류 내용을 전달하거나 로그를 저장해 문제를 추적할 수 있습니다. 이런 방식으로 문제 상황을 명확히 알리고 대응할 수 있게 해주는 것이 중요하다고 볼 수 있습니다.

콜백의 문제점

콜백 함수는 비동기 처리를 위해 많이 사용되지만, 여러 개의 비동기 작업을 순차적으로 처리할 때 중첩 구조를 만들게 됩니다. 이런 중첩이 많아질수록 코드가 점점 복잡해지며, 이를 콜백 지옥(callback hell) 이라고 부릅니다.

잠시 간단한 예제로 살펴보겠습니다.
단순하다고도 볼 수 있는 4단계 중첩 구조입니다.

window.addEventListener("load", () => {
  document.getElementById("button").addEventListener("click", () => {
    setTimeout(() => {
      items.forEach(item => {
        // 여기서 처리할 코드
      });
    }, 2000);
  });
});

프로그래밍을 하다 보면 위 코드보다 훨씬 더 복잡한 중첩 구조를 볼 수 있습니다. 이러한 중첩 구조는 코드를 이해하기 어렵게 만들고, 따라서 디버깅 또한 어렵게 만듭니다.

콜백의 대안

콜백 지옥을 해결하기 위해 다양한 방법이 제안되었습니다. ES6 부터 자바스크립트는 콜백을 사용하지 않고, 비동기 처리를 위한 다양한 방법을 제공합니다. Promise(ES6), async/await(ES2017) 등이 대표적인 예시입니다.

Promise

Promise 는 비동기 작업이 완료된 이후에 실행될 작업을 정의할 수 있는 객체입니다. Promise 는 다음 세 가지 상태를 가집니다.

  • 대기(pending): 비동기 작업이 아직 완료되지 않은 상태
  • 이행(fulfilled): 비동기 작업이 성공적으로 완료된 상태
  • 거부(rejected): 비동기 작업이 실패한 상태

Promise 객체는 then 과 catch 메서드를 사용하여 작업 성공 또는 실패에 대한 결과를 처리합니다. 작업이 완료되면 resolve 가 호출되고, 실패하면 reject 가 호출됩니다.

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = "Hello, world!";
      resolve(data);
    }, 1000);
  });
}
 
fetchData()
  .then((data) => {
    console.log(data); // "Hello, world!"
  })
  .catch((error) => {
    console.error(error);
  });

위 예제에서 fetchData 함수는 Promise 를 반환하며, 작업이 성공하면 resolve 를 호출하고, 실패하면 reject 를 호출합니다.

async/await

async/await 는 Promise 를 더욱 간결하게 사용할 수 있도록 해주는 문법입니다. async 함수는 항상 Promise 를 반환하며, await 키워드는 Promise 가 완료될 때까지 기다립니다. 이 방식은 마치 동기 코드를 작성하는 것처럼 비동기 코드를 작성할 수 있어 가독성이 매우 좋아집니다.

async function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = "Hello, world!";
      resolve(data);
    }, 1000);
  });
}
 
async function displayData() {
  try {
    const data = await fetchData();
    console.log(data); // "Hello, world!"
  } catch (error) {
    console.error(error);
  }
}
 
displayData();

위 코드는 async 와 await 를 사용하여 Promise 기반의 비동기 작업을 보다 직관적으로 처리합니다. await 는 Promise 가 해결될 때까지 기다리고, 그 후 결과를 반환합니다. 또한, try/catch 블록을 사용하여 오류를 처리할 수 있습니다.

결론

비동기 프로그래밍은 프로그램의 성능을 향상시키고, 사용자 경험을 개선하는 데 중요한 역할을 합니다. 콜백 함수는 자바스크립트의 비동기 처리를 위한 중요한 개념이지만, 콜백 지옥과 같은 문제점을 가지고 있습니다. 이를 해결하기 위해 Promise 와 async/await 같은 대안들이 등장했습니다.

Promise 와 async/await 는 이러한 콜백 지옥 문제를 해결하고, 비동기 코드를 더 직관적이고 가독성 있게 작성할 수 있게 해줍니다. 이를 통해 비동기 프로그래밍에서 발생할 수 있는 여러 문제를 해결하면서도 효율적인 코드 작성을 가능하게 합니다.