서론: 비동기 처리, 왜 중요할까요?
웹 개발의 핵심은 사용자에게 부드럽고 반응성이 뛰어난 경험을 제공하는 것입니다. 이를 위해선 사용자 인터페이스(UI)가 멈추지 않고 원활하게 작동해야 하며, 동시에 서버와의 통신이나 데이터 처리와 같은 시간이 소요되는 작업도 효율적으로 처리해야 합니다. 이 모든 것을 가능하게 하는 것이 바로 비동기 처리입니다.
예를 들어, 사용자가 웹 페이지에서 버튼을 클릭했을 때 서버로부터 데이터를 가져와야 한다고 가정해 보겠습니다. 만약 이 과정이 동기적으로 처리된다면, 데이터를 완전히 가져올 때까지 웹 페이지는 멈춰버리고 사용자는 아무런 조작도 할 수 없게 됩니다. 이는 매우 불편한 사용자 경험을 초래합니다. 반면, 비동기 처리를 사용하면 데이터를 가져오는 동안에도 웹 페이지는 계속 작동하며, 사용자는 다른 작업을 수행할 수 있습니다. 데이터 로딩이 완료되면 그 결과를 화면에 표시하여 자연스러운 흐름을 유지할 수 있습니다.
자바스크립트는 이러한 비동기 처리를 위해 콜백 함수, 프로미스, async/await와 같은 다양한 방법을 제공합니다.
1. 콜백 함수: 비동기 처리의 기본
콜백 함수는 자바스크립트에서 비동기 처리를 구현하는 가장 기본적인 방법입니다. 다른 함수의 인자로 전달되어 특정 이벤트가 발생하거나 비동기 작업이 완료되었을 때 호출되는 함수를 말합니다.
1.1 콜백 함수의 동작 원리
- 함수 정의: 먼저, 비동기 작업이 완료된 후 수행할 작업을 정의한 콜백 함수를 작성합니다.
- 비동기 함수 호출: 비동기 작업을 수행하는 함수를 호출할 때, 1단계에서 정의한 콜백 함수를 인자로 함께 전달합니다.
- 콜백 함수 호출: 비동기 작업이 완료되면, 비동기 함수 내부에서 전달받은 콜백 함수를 호출하여 결과를 처리합니다.
1.2 콜백 함수 예제
기본 예제:
function greeting(name, callback) {
const message = `안녕하세요, ${name}!`;
callback(message);
}
function logMessage(message) {
console.log(message);
}
// greeting 함수를 호출할 때 logMessage 함수를 콜백으로 전달합니다.
greeting("철수", logMessage); // 출력: 안녕하세요, 철수!
greeting
함수는 인사말을 생성한 후, 인자로 받은 callback
함수를 호출합니다. logMessage
는 콘솔에 메시지를 출력하는 함수입니다. greeting
을 호출할 때 logMessage
를 콜백으로 전달하면, "안녕하세요, 철수!"가 콘솔에 출력됩니다.
파일 읽기 예제 (Node.js):
const fs = require('fs'); // Node.js의 파일 시스템 모듈
function readFileContent(filePath, callback) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
callback(err, null);
} else {
callback(null, data);
}
});
}
readFileContent('example.txt', (err, data) => {
if (err) {
console.error('파일 읽기 에러:', err);
} else {
console.log('파일 내용:', data);
}
});
fs.readFile
함수는 비동기적으로 파일을 읽습니다. 읽기가 완료되면 지정된 콜백 함수가 호출되고, 에러가 발생하면 err
객체가, 성공하면 파일 내용(data
)이 전달됩니다.
사용자 입력 처리 예제 (브라우저):
function waitForUserInput(callback) {
const input = prompt("이름을 입력하세요:");
callback(input);
}
waitForUserInput((userInput) => {
console.log("입력된 이름:", userInput);
});
prompt
함수는 사용자 입력을 받는 브라우저 내장 함수입니다. 사용자가 입력을 완료하고 확인을 누르면, 전달된 콜백 함수가 호출되어 입력값을 처리합니다.
1.3 AJAX와 콜백
AJAX(Asynchronous JavaScript and XML)는 웹 애플리케이션에서 서버와 비동기적으로 데이터를 주고받을 수 있도록 해주는 기술입니다. XMLHttpRequest
객체를 사용하여 서버에 요청을 보내고, 응답을 받으면 콜백 함수를 호출하여 처리합니다.
GET 요청 예제:
function fetchData(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
if (xhr.status === 200) {
// 응답 데이터가 성공적으로 로드되면 callback을 호출
callback(null, JSON.parse(xhr.responseText));
} else {
// 에러가 발생하면 에러와 함께 callback을 호출
callback(new Error("데이터 로드 실패: " + xhr.status), null);
}
};
xhr.onerror = function () {
// 네트워크 에러가 발생하면 에러와 함께 callback을 호출
callback(new Error("네트워크 에러"), null);
};
xhr.send();
}
fetchData("https://api.example.com/data", function (error, data) {
if (error) {
console.error(error);
} else {
console.log("받은 데이터:", data);
}
});
POST 요청 예제:
function postData(url, data, callback) {
const xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onload = function () {
if (xhr.status === 201) {
callback(null, JSON.parse(xhr.responseText));
} else {
callback(new Error("데이터 전송 실패: " + xhr.status), null);
}
};
xhr.onerror = function () {
callback(new Error("네트워크 에러"), null);
};
xhr.send(JSON.stringify(data));
}
const newData = { name: "새로운 사용자", age: 30 };
postData("https://api.example.com/users", newData, (error, response) => {
if (error) {
console.error(error);
} else {
console.log("서버 응답:", response);
}
});
postData
함수는 서버에 POST 요청을 보내고, 성공하면 응답 데이터를, 실패하면 에러 객체를 콜백 함수에 전달합니다.
1.4 콜백 지옥: 중첩된 콜백의 늪
콜백 함수는 비동기 처리를 간편하게 구현할 수 있지만, 중첩해서 사용할 경우 코드의 가독성이 떨어지고 유지보수가 어려워지는 콜백 지옥(Callback Hell) 문제가 발생합니다.
asyncFunction1(function (result1) {
asyncFunction2(result1, function (result2) {
asyncFunction3(result2, function (result3) {
asyncFunction4(result3, function(result4) {
// ... 계속 중첩되는 콜백 함수 ...
});
});
});
});
위 코드처럼 여러 비동기 작업을 순차적으로 처리해야 할 때 콜백 함수가 계속 중첩되면 코드의 깊이가 깊어지고, 이해하기 어려워집니다. 이러한 콜백 지옥은 코드의 품질을 저하시키는 주요 원인이 됩니다.
2. 프로미스: 콜백 지옥 탈출을 위한 구조선
프로미스(Promise)는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 프로미스를 사용하면 비동기 코드를 더 체계적으로 구성하고, 콜백 지옥 문제를 효과적으로 해결할 수 있습니다.
2.1 프로미스의 세 가지 상태
프로미스는 다음 세 가지 상태 중 하나를 갖습니다.
- 대기(Pending): 비동기 작업이 아직 완료되지 않은 초기 상태입니다.
- 이행(Fulfilled): 비동기 작업이 성공적으로 완료되어 결과 값을 가진 상태입니다.
- 거부(Rejected): 비동기 작업이 실패하여 에러 이유를 가진 상태입니다.
2.2 프로미스 생성
Promise
생성자를 사용하여 새로운 프로미스 객체를 생성할 수 있습니다. 생성자는 resolve
와 reject
두 개의 함수를 인자로 받는 콜백 함수를 인자로 받습니다.
const myPromise = new Promise((resolve, reject) => {
// 비동기 작업 수행 (예: 서버 요청, 타이머 등)
const success = true; // 비동기 작업의 성공 여부를 가정
if (success) {
resolve("작업 성공!"); // 작업이 성공하면 resolve 함수를 호출하여 결과를 전달
} else {
reject("작업 실패!"); // 작업이 실패하면 reject 함수를 호출하여 에러 이유를 전달
}
});
resolve(value)
: 프로미스를 이행 상태로 변경하고,value
를 결과 값으로 설정합니다.reject(error)
: 프로미스를 거부 상태로 변경하고,error
를 에러 이유로 설정합니다.
2.3 프로미스 사용: .then()
과 .catch()
프로미스가 이행되거나 거부되면, .then()
또는 .catch()
메서드를 사용하여 각각의 경우에 대한 처리 로직을 작성할 수 있습니다.
myPromise
.then((result) => {
console.log(result); // "작업 성공!" 출력
})
.catch((error) => {
console.error(error); // "작업 실패!" 출력
});
.then(callback)
: 프로미스가 이행되었을 때 호출될 콜백 함수를 등록합니다. 콜백 함수는 프로미스의 결과 값을 인자로 받습니다..catch(callback)
: 프로미스가 거부되었을 때 호출될 콜백 함수를 등록합니다. 콜백 함수는 에러 이유를 인자로 받습니다.
setTimeout
을 사용한 프로미스 예제:
const delayedPromise = (success) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
resolve("2초 후 작업 성공");
} else {
reject("2초 후 작업 실패");
}
}, 2000);
});
};
delayedPromise(true)
.then((result) => console.log(result))
.catch((error) => console.error(error));
delayedPromise(false)
.then((result) => console.log(result))
.catch((error) => console.error(error));
이 예제는 2초 후에 성공 또는 실패하는 프로미스를 생성합니다. success
값에 따라 resolve
또는 reject
가 호출됩니다.
2.4 여러 프로미스 다루기: Promise.all()
, Promise.race()
, Promise.allSettled()
Promise.all()
: 여러 개의 프로미스를 동시에 실행하고, 모든 프로미스가 이행될 때까지 기다립니다.
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, "foo"));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, "bar"));
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // [3, "foo", "bar"] 출력
})
.catch((error) => {
console.error("에러 발생:", error); // 하나라도 거부되면 에러 발생
});
Promise.all()
은 입력받은 모든 프로미스가 이행되면 결과 값들의 배열을 반환하고, 하나라도 거부되면 즉시 거부됩니다.
Promise.race()
: 여러 프로미스 중 가장 먼저 완료(이행 또는 거부)된 프로미스의 결과(또는 에러)를 반환합니다.
const racePromise1 = new Promise((resolve) => setTimeout(resolve, 500, "첫 번째"));
const racePromise2 = new Promise((resolve) => setTimeout(resolve, 100, "두 번째"));
Promise.race([racePromise1, racePromise2])
.then((value) => {
console.log("가장 빠른 프로미스:", value); // "두 번째" 출력
});
Promise.allSettled()
: 모든 프로미스의 결과(이행 또는 거부)를 배열로 반환합니다. 각 프로미스의 상태와 결과(또는 에러 이유)를 확인할 수 있습니다.
const settledPromise1 = Promise.resolve(3);
const settledPromise2 = new Promise((resolve, reject) => setTimeout(reject, 100, "에러 발생"));
Promise.allSettled([settledPromise1, settledPromise2])
.then((results) => {
console.log(results);
// [
// { status: 'fulfilled', value: 3 },
// { status: 'rejected', reason: '에러 발생' }
// ]
});
2.5 프로미스로 fetchData
함수 개선하기
function fetchData(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error("데이터 로드 실패: " + xhr.status));
}
};
xhr.onerror = function () {
reject(new Error("네트워크 에러"));
};
xhr.send();
});
}
fetchData("https://api.example.com/data")
.then((data) => {
console.log("받은 데이터:", data);
})
.catch((error) => {
console.error(error);
});
이제 fetchData
함수는 프로미스를 반환합니다. 이를 통해 .then()
과 .catch()
를 사용하여 비동기 작업의 결과를 더 깔끔하게 처리할 수 있습니다.
3. async/await: 비동기 코드의 미래
async/await는 ES8(ECMAScript 2017)에 도입된 최신 비동기 처리 문법입니다. 프로미스를 기반으로 동작하며, 비동기 코드를 마치 동기 코드처럼 작성할 수 있도록 해줍니다.
3.1 async/await 기본 사용법
async
: 함수 선언 앞에async
키워드를 붙이면 해당 함수는 항상 프로미스를 반환하는 비동기 함수가 됩니다.await
:async
함수 내부에서만 사용할 수 있으며, 프로미스 앞에await
키워드를 붙이면 해당 프로미스가 완료(이행 또는 거부)될 때까지 기다립니다.
async function getData() {
try {
const data = await fetchData("https://api.example.com/data"); // fetchData는 프로미스를 반환
console.log("받은 데이터:", data);
} catch (error) {
console.error("에러 발생:", error);
}
}
getData();
getData
는 async
함수이므로 항상 프로미스를 반환합니다. await
키워드는 fetchData
프로미스가 완료될 때까지 기다립니다. try...catch
블록을 사용하여 에러를 처리할 수 있습니다.
3.2 여러 await
사용하기
async function processData() {
try {
const data1 = await fetchData("https://api.example.com/data1");
console.log("첫 번째 데이터:", data1);
const data2 = await fetchData("https://api.example.com/data2");
console.log("두 번째 데이터:", data2);
const result = await process(data1, data2); // process는 data1과 data2를 처리하는 비동기 함수
console.log("처리 결과:", result);
} catch (error) {
console.error("에러 발생:", error);
}
}
processData();
이 예제는 여러 await
를 사용하여 비동기 작업을 순차적으로 처리합니다. 각 await
는 이전 프로미스가 완료될 때까지 기다립니다.
3.3 Promise.all
과 await
함께 사용하기
async function getAllData() {
try {
const [data1, data2, data3] = await Promise.all([
fetchData("https://api.example.com/data1"),
fetchData("https://api.example.com/data2"),
fetchData("https://api.example.com/data3"),
]);
console.log("데이터 1:", data1);
console.log("데이터 2:", data2);
console.log("데이터 3:", data3);
} catch (error) {
console.error("에러 발생:", error);
}
}
getAllData();
Promise.all
을 await
와 함께 사용하면 여러 프로미스를 동시에 실행하고, 그 결과를 한 번에 받아올 수 있습니다.
3.4 async/await의 장점
- 가독성 향상: 비동기 코드를 동기 코드와 유사한 흐름으로 작성할 수 있어 코드의 가독성이 크게 향상됩니다.
- 간편한 에러 처리:
try...catch
블록을 사용하여 비동기 작업에서 발생하는 에러를 쉽게 처리할 수 있습니다. - 디버깅 용이: 동기 코드와 유사한 방식으로 디버깅할 수 있어 개발 효율성이 높아집니다.
결론: 어떤 비동기 처리 방법을 선택해야 할까요?
자바스크립트는 비동기 처리를 위해 콜백, 프로미스, async/await와 같은 다양한 방법을 제공합니다. 각 방법은 장단점이 있으며, 상황에 따라 적절한 방법을 선택하는 것이 중요합니다.
- 콜백 함수:
- 장점: 가장 기본적인 방법으로, 간단한 비동기 작업에 적합합니다.
- 단점: 콜백 지옥 문제가 발생할 수 있어 복잡한 비동기 처리에는 적합하지 않습니다.
- 프로미스:
- 장점: 콜백 지옥 문제를 해결하고 코드의 가독성을 높여줍니다.
.then()
,.catch()
,Promise.all()
등을 활용하여 유연한 비동기 처리가 가능합니다. - 단점: 여전히
.then()
체이닝이 길어질 수 있습니다.
- 장점: 콜백 지옥 문제를 해결하고 코드의 가독성을 높여줍니다.
- async/await:
- 장점: 비동기 코드를 동기 코드처럼 작성할 수 있어 가독성이 매우 뛰어나고, 에러 처리도 간편합니다.
- 단점: 최신 문법이므로, 오래된 브라우저에서는 지원되지 않을 수 있습니다. (Babel과 같은 트랜스파일러를 사용하면 해결 가능)
결론적으로, 최신 자바스크립트 개발에서는 async/await를 사용하는 것이 가장 권장됩니다. async/await는 가독성, 유지보수성, 에러 처리 등 모든 면에서 가장 뛰어난 방법입니다.
'프로그래밍 > Javascript' 카테고리의 다른 글
자바스크립트 개발 도구: 브라우저 콘솔의 모든 것 (0) | 2025.02.11 |
---|---|
자바스크립트 모듈 완벽 가이드: 개념, 사용법, 그리고 실전 활용 (0) | 2025.02.11 |
DOM (Document Object Model): 웹 페이지의 구조와 상호작용을 위한 모든 것 (0) | 2025.02.11 |
자바스크립트 배열 정복: 개념부터 다차원 배열, 유용한 메소드까지 총정리! (0) | 2025.02.10 |
자바스크립트 함수 완전 정복: 표현식, 클로저, 콜백까지 파헤치기 (0) | 2025.02.10 |