1.1 비동기처리
1. 개요
자바스크립트는 싱글 스레드로 동작한다. 즉, 한 번에 한 작업만 처리할 수 있다.
그런데 우리가 웹 브라우저에서 쇼핑몰 같은 페이지를 이용할 때, 여러 일이 동시에 일어나야 한다. 예를 들어:
- 사용자가 스크롤을 한다
- 버튼을 클릭한다
- 서버에 상품 정보를 요청한다
만약 동기 방식으로 서버 요청을 처리하면, 서버가 응답할 때까지 자바스크립트는 기다려야 한다. 그 동안 브라우저는 아무것도 할 수 없어서, 버튼 클릭, 스크롤 등 모든 동작이 멈춘다.
반면 비동기 처리(fetch + async/await)를 쓰면, 서버 응답을 기다리는 동안에도 브라우저는 다른 동작을 계속 수행할 수 있다.
- 화면 뼈대(HTML)는 바로 보여주고
- 서버에서 데이터를 가져와 나중에 필요한 부분만 렌더링
즉, 사용자는 기다리지 않고도 화면을 볼 수 있는 것처럼 느낀다.
결국, 비동기 처리를 활용하면 싱글 스레드 환경에서도 동시성(Concurrency) 효과를 낼 수 있는 것이다.
2. 주요 비동기 처리 방식
2.1 콜백 함수(Callback)
- 특정 작업이 완료된 후 호출되는 함수
setTimeout(() => {
console.log("1초 후 실행");
}, 1000);
- 단점은 콜백 지옥(Callback Hell)이 발생 가능하다는 것이다.
콜백지옥 (Callback Hell)
콜백함수는 즉시 실행되는 게 아니라, 다른 함수에서 “필요할 때” 실행하도록 넘겨주는 함수이다.
이 코드는 파일 3개를 순서대로 읽어와서 파일을 합쳐서 저장하는 코드이다. 파일 읽기 → 파일 읽기 → 파일 읽기 → 파일 저장을 순차적으로 처리하지만, 콜백이 계속 중첩돼서 들여쓰기가 깊어지고 가독성이 떨어진다.
파일을 읽는 작업(fs.readFile)은 비동기 API이다.
console.log('시작');
fs.readFile('data.txt', 'utf8', (err, data) => {
if (!err) console.log(data);
});
console.log('끝');
이 코드를 실행하면 출력 순서는 파일 읽기가 끝나기 전에 끝이 먼저 찍힌다.
즉, 파일 읽는 동안 다른 코드를 블록하지 않고 바로 다음 줄로 넘어가도록 설계돼 있다.
시작
끝
[파일 내용]
그런데 우리가 “파일1 → 파일2 → 파일3 순서대로 처리하고 마지막에 합쳐서 저장”하고 싶으면, 각 작업이 끝난 시점에 다음 작업을 실행해야 한다.
그래서 자연스럽게 이렇게 된다.
const fs = require('fs');
const path = require('path');
// 여러 파일을 순차적으로 읽고, 내용을 합쳐서 저장하는 예제
const file1 = path.join(__dirname, 'file1.txt');
const file2 = path.join(__dirname, 'file2.txt');
const file3 = path.join(__dirname, 'file3.txt');
fs.readFile(file1, 'utf8', (err, data1) => {
if (err) {
console.error('file1 읽기 실패', err);
return;
}
fs.readFile(file2, 'utf8', (err, data2) => {
if (err) {
console.error('file2 읽기 실패', err);
return;
}
fs.readFile(file3, 'utf8', (err, data3) => {
if (err) {
console.error('file3 읽기 실패', err);
return;
}
const combined = data1 + data2 + data3;
fs.writeFile('combined.txt', combined, (err) => {
if (err) {
console.error('파일 저장 실패', err);
} else {
console.log('모든 파일 합치기 완료!');
}
});
});
});
});
- file1 읽기 완료 → 콜백 실행
- 콜백 안에서 file2 읽기 요청 → 완료 시 콜백 실행
- 콜백 안에서 file3 읽기 요청 → 완료 시 콜백 실행
- 최종적으로 합치고 저장
즉, 콜백 자체는 비동기를 관리하기 위한 도구지만, 순서를 보장하려면 중첩 구조가 필요하게 되는 것이다
만약 그냥 fs.readFile(file1)
→ fs.readFile(file2)
→ fs.readFile(file3)
순으로 나열하면, 파일2 읽기가 파일1 끝나기 전에 시작되기 때문에 순서 보장이 안 된다. 그럼 이렇게 가시성이 떨어지는 callback 함수를 보기 좋게 바꾸려면 어떻게 해야 하는가?
이것을 Promise + async/await 형식으로 바꾸면 코드가 깔끔해져 가독성이 좋아지고, try/catch 하나로 모든 I/O 에러를 처리 가능하다.
const fs = require('fs').promises; // fs.promises 사용
async function readAndCombineFiles() {
try {
const data1 = await fs.readFile('file1.txt', 'utf8');
const data2 = await fs.readFile('file2.txt', 'utf8');
const data3 = await fs.readFile('file3.txt', 'utf8');
const combined = data1 + data2 + data3;
await fs.writeFile('combined.txt', combined);
console.log('모든 파일 합치기 완료!');
} catch (err) {
console.error('파일 처리 중 오류 발생', err);
}
}
readAndCombineFiles();
2.2 프로미스(Promise)
- Promise 는 "미래에 완료될 어떤 작업의 결과를 나타내는 객체"이다.
- 한 마디로 말하면 비동기 작업의 완료/실패를 객체로 표현한 것이며 쉽게 말해 미래에 일어날 일을 연결하는 도구이다.
- 비동기 작업이 성공하면 resolve 호출 → .then() 실행
- 실패하면 reject 호출 → .catch() 실행
const fs = require('fs').promises;
fs.readFile('file1.txt', 'utf8')
.then(data => { // .then() → 작업 성공 시 실행
console.log('파일1 읽음:', data);
return fs.readFile('file2.txt', 'utf8');
})
.then(data => {
console.log('파일2 읽음:', data);
})
.catch(err => { // .catch() → 오류 처리
console.error('오류 발생', err);
});
.then()
→ 작업 성공 시 그 다음 처리 내용.catch()
→ 오류 처리 : 사용자에게 서비스 에러가 날 때 보여줄 화면
2.3 async/await
async/await는 Promise를 더 직관적이고 순차적으로 쓰게 해주는 문법이다.
async함수 안에서 await를 사용하면 Promise가 처리될 때까지 기다렸다가 결과를 반환한다. 이렇게 하면 코드가 거의 동기 흐름처럼 읽혀서 그냥 Promise만 썼을 때보다 가시성이 좋다.
같은 작업을 async/await로 하면
const fs = require('fs').promises;
async function readFiles() {
try {
const data1 = await fs.readFile('file1.txt', 'utf8');
const data2 = await fs.readFile('file2.txt', 'utf8');
console.log(data1, data2);
} catch (err) {
console.error('오류 발생', err);
}
}
readFiles();
await fs.readFile(...)
→ 파일 읽기가 끝날 때까지 기다렸다가data1
에 저장- 순차적으로 코드를 작성할 수 있고 들여쓰기 깊이도 최소화됨
한 마디로 :
Promise는 “비동기 결과를 담는 그릇”이고,
async/await는 “그 그릇 안의 값을 동기 코드처럼 꺼내 쓰는 방법”이다.
2.4 fetch
- 브라우저 내장 API로, HTTP 요청을 비동기적으로 처리한다.
- Promise 기반이므로
.then()
또는async/await
사용 가능 - 예시:
async function getUser() {
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
if (!response.ok) throw new Error("서버 요청 실패");
const user = await response.json();
return user;
}
getUser().then(user => console.log(user));
- 특징:
- 네트워크 요청이 완료될 때까지 다른 코드 블로킹하지 않음
- JSON, 텍스트, Blob 등 다양한 데이터 타입 지원
- HTTP 메서드(GET, POST, PUT, DELETE) 지정 가능
fetch는 비동기 함수라서 바로 값을 반환하지 않고 Promise를 반환한다. 그래서 아래처럼 바로 콘솔 찍으면 undefined가 나온다.
const result = fetch('url'); // result는 Promise
console.log(result); // Promise {<pending>}
3. 결론
- 비동기처리 실무에서는 Promise + async/await + fetch 조합이 표준
- 들여쓰기가 콜백 지옥보다는 훨씬 덜하다. 중첩은 있지만 Promise 덕분에 한 단계씩 연결 가능하기 때문이다.
- 순차 처리가 간으하다. then()을 연결하면서 다음 파일 읽기는 이전 파일이 끝난 후 실행된다.
- 그리고 .catch() 하나로 모든 단계의 오류를 처리 가능하기 때문에 에러처리가 쉽다.