1. 비동기 처리
우선 동기란, 하나의 작업이 실행되는 동안은 다른 작업을 수행하지 않는 방식으로, 다시 말해 하나의 작업이 완료될 때까지 다른 작업을 실행하지 않고, 앞에 진행하고 있는 작업이 종료된 다음 작업을 할 수 있는 순차적인 방식을 말한다.
반대로 자바스크립트에서 비동기(Asynchronous)쉽게 말해서 기다리지 않고 다음 일을 하는 것이다. 자바스크립트는 원래 한 번에 하나씩 처리하는 싱글 스레드 언어이다. 때문에 한 번에 하나의 작업만 수행할 수 있지만 비동기 처리(Asynchronous Processing)를 통해 마치 동시에 여러 작업을 수행하는 것처럼 보이게 만들어 사용자 경험을 부드럽게 유지할 수 있다.
1.1 setTimeout()
자바스크립트에서 작업을 비동기로 처리하는 방법에는 여러 가지 방법이 있다. 그 중에서 가장 기본적인 방식은 바로 setTimeout()이라는 내장 함수를 사용하는 방법이다. setTimeout 함수의 매개변수로는 콜백함수와 delayTime 이렇게 총 2개의 매개변수가 들어간다. setTimeout 내장 함수를 사용하면, 전달된 콜백 함수를 이 delayTime(ms) 만큼 기다린 다음에 수행한다.
setTimeout(() => {
console.log('비동기');
}, 3000);
console.log('종료');
함수가 3초 후에 수행될 수 있도록 delayTime으로는 3000을 전달하는 코드를 작성하고 그 아래 ‘종료’라는 단어를 출력하는 코드를 작성해보았다.
‘종료’가 먼저 출력이 되고, 3초 후에 ‘비동기’가 출력된다.
그러면 다음과 같은 코드의 출력을 예측해보자.
const workA = () => {
setTimeout(() => {
console.log('workA');
}, 5000);
};
const workB = () => {
setTimeout(() => {
console.log('workB');
}, 3000);
};
const workC = () => {
setTimeout(() => {
console.log('workC');
}, 10000);
};
const workD = () => {
console.log('workD');
};
workA();
workB();
workC();
workD();
.
.
.
.
.
.
.
이 코드는 가장 마지막에 호출한 workD가 제일 먼저 출력이 되고, 그 다음은 동일하게 workB, workA, workC의 순서로 출력이 된다. workD는 동기로 처리한 함수이기 때문에 다른 함수들과 상관 없이 가장 먼저 출력되고, 이후에 비동기 함수들이 순서대로 출력된다.
2. 프로미스 객체
자바스크립트에는 작업을 비동기로 처리할 때 사용하는, 프로미스(Promise)라는 객체가 있다. 프로미스 객체를 사용하면 자바스크립트에서 비동기 작업을 좀 더 편리하게 처리할 수 있다. 프로미스 객체는 new 키워드와 생성자를 사용해 생성할 수 있다. 프로미스 객체는, 객체 생성 시 인수로 executor라는 실행 함수를 전달하고, 실행 함수에는 매개변수로 resolve와 reject라는 콜백 함수를 전달한다.
const executor = (resolve, reject) => {
//코드
};
const promise = new Promise(executor);
console.log(promise); // Promise{<pending>}
여기서 executor란 실행 함수를 의미하며 프로미스 생성자에 반드시 전달해야 하는 함수로, 프로미스 객체가 생성될 때 자동으로 실행되는 함수이다. 프로미스 객체가 생성됨과 동시에 executor가 실행되고, executor에서 원하는 작업이 처리된다. 프로미스 객체의 executor는 작업 처리의 성공 여부에 따라 성공했을 경우 resolve를, 실패했을 경우 reject를 호출한다. 생성자를 통해 생성된 프로미스 객체는 state와 result라는 프로퍼티를 갖는다.
프로미스 객체가 생성되면, 바로 executor 함수가 실행되고 이때 state 값은 pending, result 값은 undefined가 할당된다. 이후 만약 특정 작업이 성공해서 resolve 함수가 호출되면, state는 fulfilled의 값을 갖고 result는 resolve 함수에 전달된 value의 값을 갖는다. 반대로 특정 작업이 실패해서 reject 함수가 호출된다면 state는 rejected 실패의 값이, 그리고 result에는 error가 할당된다. 다음 코드를 봐보자.
const executor = (resolve, reject) => {
setTimeout(() => {
resolve('성공');
reject('실패');
}, 3000);
};
const promise = new Promise(executor);
promise.then(
(result) => {
console.log(result);
},
(error) => {
console.log(error);
}
);
3초 후에 ‘성공’을 전달하는 비동기 함수를 resolve를 사용해서 작성하고, 오류가 발생했을 때에는 ‘실패’를 전달하는 코드를 reject를 사용해서 작성했다. 콜백 함수에 전달된 값을 프로미스 객체의 then메서드를 사용해 출력해보겠습니다. then 메서드는 promise 객체의 메서드로 콜백 함수를 전달할 수 있다. then 메서드의 첫 번째 인수는 프로미스 객체가 fulfilled 상태일 때 실행되는 함수이고, 두 번째 인수는 프로미스 객체가 rejected 상태일 때 실행되는 함수이다. 첫 번째 함수로는 result를 출력하는 함수를 작성하고, 두 번째 함수로는 error를 출력하는 함수를 작성하였다.
코드를 실행하면 함수 호출이 성공했기 때문에 프로미스 객체가 fulfilled로 변경되어 3초 후에 ‘성공’이 출력된다.
2.1 콜백 지옥
콜백 지옥을 설명하기 위해 workA, workB, workC 함수가 순서대로 실행될 수 있도록 코드를 다음과 같이 작성하였다.
const workA = (value, callback) => {
setTimeout(() => {
callback(value + 5);
}, 5000);
};
const workB = (value, callback) => {
setTimeout(() => {
callback(value - 3);
}, 3000);
};
const workC = (value, callback) => {
setTimeout(() => {
callback(value + 10);
}, 10000);
};
workA(10, (resA) => {
console.log(`workA : ${resA}`);
workB(resA, (resB) => {
console.log(`workB : ${resB}`);
workC(resB, (resC) => {
console.log(`workC : ${resC}`);
});
});
});
자바스크립트에서 비동기 함수의 결괏값을 또 다른 비동기 함수에서 사용하기 위해서는, 다음과 같은 코드와 같이 콜백 함수 안에 콜백 함수를 전달하는 방식으로 작성을 해줘야한다. 비동기 함수를 이러한 방식으로 처리하면 함수의 실행 순서를 알기 쉽고, 유연한 프로그래밍을 할 수 있다는 장점이 있지만, 코드가 매우 복잡해지고 이렇게 가독성이 떨어지면서 많은 오류를 발생시킬 수 있다는 단점이 있다. 우리가 작성한 코드와 같이 > 이러한 모양으로 복잡하게 작성된 코드를 바로 콜백지옥이라고 부른다.
이를 해결하기 위해 앞서 배운 프로미스 객체를 사용해서 이 코드를 수정해보겠다. workA, workB, workC 함수는 매개변수로 value 를 전달받고, 함수 내부에서는 프로미스 객체를 생성하고 resolve 함수를 사용해서 알맞은 값을 전달해보겠다.
const workA = (value) => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(value + 5);
}, 5000);
});
return promise;
};
const workB = (value) => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(value - 3);
}, 3000);
});
return promise;
};
const workC = (value) => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(value + 10);
}, 10000);
});
return promise;
};
workA(10).then((resA) => {
console.log(`workA : ${resA}`);
workB(resA).then((resB) => {
console.log(`workB : ${resB}`);
workC(resB).then((resC) => {
console.log(`workC : ${resC}`);
});
});
});
하지만 여전히 콜백 지옥 코드의 모양과는 비슷한 것 같다. 그럼 이번에는 then 메서드를 조금 다른 방식으로 사용해 코드를 더 깔끔하게 수정해보겠다.
...
workA(10)
.then((resA) => {
console.log(`1. ${resA}`);
return workB(resA);
})
.then((resB) => {
console.log(`2. ${resB}`);
return workC(resB);
})
.then((resC) => {
console.log(`3. ${resC}`);
})
.catch((error) => {
console.log(error);
});
이렇게 계속해서 프로미스 객체를 반환해 then 메서드를 연속으로 사용하는 방식을 프로미스 체이닝(Promise Chaining)이라고 부다. 프로미스 체이닝을 사용해 코드를 작성하면 코드를 아래쪽으로 계속 작성할 수 있기 때문에 훨씬 더 직관적으로 코드를 해석할 수 있고 코드를 깔끔하게 정리할 수 있다.
3. async와 await
async와 await는 비동기 작업을 더 쉽게 다룰 수 있게 도와주는 문법이다. 자바스크립트에서 async는 비동기 작업을 처리할 때 사용되는 키워드로, 비동기 작업을 포함하고 있기 때문에 프로미스 객체를 반환하는 함수에 작성하는 키워드이다. async를 작성하면 코드를 훨씬 직관적으로 해석할 수 있다.
const delay = (ms) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('3초가 지났습니다.');
}, ms);
});
};
const start = async () => {
delay(3000).then((res) => {
console.log(res);
});
};
start();
async는 비동기를 수행할 함수의 이름 오른쪽에 작성한다. 어떠한 함수에 async 키워드를 작성하면, 해당 함수는 항상 자동으로 프로미스 객체를 반환하게 다. 실제로 async 키워드가 붙은 함수 start에 이렇게 마우스를 올려보면 프로미스 객체를 반환하는 함수라는 것을 알 수 있다.
await 키워드에 대해서도 알아보겠다. await은 async 키워드가 작성된 함수의 내부에서 사용하는 키워드다. await 키워드가 포함된 코드가 실행되면, 해당 작업이 종료될 때까지 프로그램의 실행이 중단된다.
const delay = (ms) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('3초가 지났습니다.');
}, ms);
});
};
const start = async () => {
let result = await delay(3000);
console.log(result);
};
start();
await은 ‘기다리다’라는 뜻으로, 프로미스가 처리될 때까지 기다리면서, 그동안은 함수의 실행을 중단하는 역할을 한다. 그렇기 때문에 start 함수를 호출하면, delay 함수의 프로미스 객체의 처리가 완료될 때까지 잠시 중단되었다가 프로미스 객체의 처리가 완료되면 코드가 순서대로 다시 실행된다. 이후 result 변수에 delay 함수의 반환값인, 실행 완료된 프로미스 객체가 할당되어 3초 후에 ‘3초가 지났습니다’ 문장이 출력된다.
3.1 Promise all()
함수의 내부에 프로미스 객체를 반환하는 코드를 작성해주고 resolve에는 각각 함수의 이름을 전달해주겠습니다. 그리고 이번엔 start 함수에 async와 await을 사용해서 이 비동기 함수들을 호출하는 코드는 다음과 같다.
const workA = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('workA');
}, 5000);
});
};
const workB = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('workB');
}, 3000);
});
};
const workC = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('workC');
}, 10000);
});
};
const start = async () => {
try {
let resultA = await workA();
let resultB = await workB();
let resultC = await workC();
console.log(resultA);
console.log(resultB);
console.log(resultC);
} catch (err) {
console.log(err);
}
};
start();
코드를 실행하면, workA, workB, workC 이렇게 값이 순서대로 출력되지만, 비동기로 작업을 처리했음에도 불구하고 await 키워드를 사용해서 작업을 순차적으로 처리하고 있기 때문에 시간이 매우 오래 걸리는 것을 볼 수 있다. await 키워드는 각 프로미스가 해결될 때까지 잠깐 멈추고 다음 코드를 실행하지 않기 때문에, 모든 작업을 순차적으로 기다리게 된. 따라서 이렇게 10초, 5초, 3초를 더한 18초가 걸리게 된다.
각 작업을 병렬처럼 실행해서 실행 시간을 단축하기 위해서는 Promise.all을 사용한다.
const workA = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('workA');
}, 5000);
});
};
const workB = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('workB');
}, 3000);
});
};
const workC = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('workC');
}, 10000);
});
};
const start = async () => {
try {
let results = await Promise.all([workA(), workB(), workC()]);
results.forEach(result => console.log(result));
} catch (err) {
console.log(err);
}
};
start();
다음과 같이 results 라는 변수에 Promise.all을 사용해서 3개의 함수를 전달했다. 그 다음, results 변수에 담긴 값들을 forEach 메서드를 사용해서 각각의 값을 출력해주었다. 이렇게 코드를 작성하면 await 키워드를 사용하더라도 비동기 작업들이 병렬 처럼 실행되기 때문에 이전보다 훨씬 빠르게 10초만에 모든 작업이 완료되어 workA, workB, workC가 순서대로 출력이 된다.
Promise.all은 자바스크립트에서 제공하는 메서드로, 여러 개의 프로미스를 병렬로 실행하고, 모든 프로미스가 완료될 때까지 기다렸다가 결과를 한 번에 반환하는 역할을 한다. Promise.all은 전달된 프로미스 배열이 모두 fulfilled, 성공될 때까지 대기하며, 하나라도 rejected 실패되면 즉시 실패 상태가 된다.
4. API 호출
API는 프로그램끼리 데이터를 주고받기 위한 약속된 방법이다. 즉, 서버에 원하는 데이터를 요청하고 전달받는 방법을 제공한다. 여기서 JSON은 데이터를 저장하고 주고받기 위한 텍스트 형식이다.
JSONplaceholder에서 제공하는 API 중 하나인, 이 https://jsonplaceholder.typicode.com/users 주소를 사용해서 API를 직접 호출해보자. 자바스크립트에서는 fetch라는 내장 함수를 사용해서 API를 호출할 수 있는데 이렇게 fetch 메서드의 괄호 안에는 API의 주소를 작성한다.
let response = fetch('https://jsonplaceholder.typicode.com/users');
console.log(response);
reponse 변수를 출력하면, state 프로퍼티가 fulfilled인 프로미스 객체가 출력되는 것을 볼 수 있다.
앞서 배웠던 것처럼, 프로미스 객체를 반환하는 함수는 비동기 처리 함수이고, 이러한 함수는 then 메서드를 사용해 결괏값을 출력할 수 있다. 작성한 코드에서 비동기 함수의 결괏값을 then 메서드를 사용해서 출력해보고, catch 메서드를 사용해서 에러도 함께 출력해보도록 하겠다.
let response = fetch('https://jsonplaceholder.typicode.com/users')
.then((res) => console.log(res))
.catch((err) => console.log(err));
console.log(response);
코드를 실행하면, fetch 함수는 비동기 함수이기 때문에 가장 아래에 작성된 response를 출력하는 코드가 먼저 출력이 되고, 이후 프로미스에서 resolve 함수를 통해 전달된 값을 then 메서드를 사용해서 출력했다.
then 메서드를 통해 출력된 API 호출 결괏값을 자세하게 살펴보면, JSONplaceholder 사이트에서 확인했었던 JSON 형식의 데이터가 아니라 Response라는 객체가 출력된것을 볼 수 있다. 이 Response 객체는 API 호출에 성공했을 때 반환되는 API 성공 객체로, 이렇게 fetch 메서드를 사용해 API를 호출하면 API 성공 객체 자체가 반환이 된다.
fetch 메서드를 사용해서 API를 호출했을 때, 우리가 원하는 JSON 형식의 데이터를 얻으려면 어떻게 해야 하는지 알아보기 전에, 조금 더 직관적인 코드 해석을 위해 앞서 배웠던 async와 await을 사용해서 코드를 수정해보겠다. API를 호출하는 getData라는 함수를 생성하고, 함수의 내부에 작성해두었던 코드를 복사해서 붙여넣어준 후 getData 함수의 옆에는 async를, fetch 메서드의 옆에는 await을 작성해주고 then과 catch 메서드는 삭제했다.
const getData = async () => {
let response = await fetch('https://jsonplaceholder.typicode.com/users');
console.log(response);
};
getData();
자바스크립트에서 API를 호출하면, 보통 JSONplceholder 사이트에서 확인했던 것처럼 JSON 데이터를 전달받는다. 하지만 이 JSON 형식의 데이터는 자바스크립트에서 객체 형태의 데이터를 가독성 좋게 나타내기 위한 하나의 표기법이기 때문에, 자바스크립트에서 JSON 데이터를 활용하기 위해서는 JSON 문자열을 파싱해서 객체 형태로 반환해야 한다. 자바스크립트에서 JSON 형식의 데이터를 자바스크립트가 활용할 수 있는 어떠한 객체의 형태로 변환하기 위해서는, json() 이라는 메서드를 사용다. json 메서드를 사용해서 코드를 작성해보겠다.
const getData = async () => {
let response = await fetch('https://jsonplaceholder.typicode.com/users');
let data = await response.json();
console.log(data);
};
getData();
4.1 API 에러 핸들링
API 호출은 필요한 데이터를 전달 받기 위해 데이터를 요청하는 작업이다. 데이터를 요청할 때에는 네트워크 오류 혹은 인터넷 속도 등의 다양한 이유로, 데이터 요청에 실패할 수 있다는 점을 주의해야 한다.
const getData = async () => {
try {
let response = await fetch('https://jsonplaceholder.typicode.com/users');
let data = await response.json();
console.log(data);
} catch (err) {
console.log(err);
}
};
getData();
API URL을 이상하게 수정한 다음, 실제로 코드를 실행하면 이렇게 catch 문을 통해 에러 메세지가 알맞게 출력되는것 까지 확인해볼 수 있다.
'프론트' 카테고리의 다른 글
[JS] DOM과 DOM API (0) | 2025.07.01 |
---|---|
[JS] 자바스크립트 기초(2) (1) | 2025.05.25 |
[JS] 자바스크립트 기초(1) (0) | 2025.05.01 |
CSS 기초 (1) | 2025.04.29 |
HTML 기초 (1) | 2025.04.04 |