출시·고도화 중
JavaScript 안내서 · 6/6
JavaScript를 제대로 쓰려면 꼭 이해해야 하는 개념이 있습니다. 한 번에 한 작업만 실행하면서도 네트워크와 타이머를 막힘 없이 다루게 해 주는 이벤트 루프, 그 위에서 동작하는 콜백, Promise, async/await, 그리고 함수가 주변 변수를 기억하는 클로저와 헷갈리기 쉬운 this입니다.
JavaScript 코드는 하나의 스레드에서 실행됩니다. 오래 걸리는 작업(네트워크 요청, 파일 읽기, 타이머)은 브라우저나 Node.js가 뒤에서 처리하고, 끝나면 실행할 콜백을 대기열에 넣습니다. 이벤트 루프는 지금 실행 중인 코드(호출 스택)가 비면 대기열에서 다음 작업을 꺼내 실행합니다.
대기열은 두 종류입니다. Promise의 then 콜백 같은 마이크로태스크는 현재 코드가 끝나자마자 모두 실행되고, setTimeout 같은 태스크는 그다음에 하나씩 실행됩니다.
console.log("1: 동기 코드");
setTimeout(() => console.log("4: setTimeout(태스크)"), 0);
Promise.resolve().then(() => console.log("3: Promise(마이크로태스크)"));
console.log("2: 동기 코드");
// 출력 순서: 1 → 2 → 3 → 4지연 시간이 0이어도 setTimeout 콜백은 현재 코드와 마이크로태스크가 모두 끝난 뒤에 실행됩니다. 반대로 무거운 계산을 동기 코드로 오래 돌리면 그동안 화면이 멈추고 다른 요청도 처리되지 않으므로, 메인 스레드를 오래 붙잡지 않는 것이 중요합니다.
가장 오래된 비동기 방식은 작업이 끝나면 호출할 함수(콜백)를 넘기는 것입니다. Node.js의 전통적인 API는 첫 번째 인수로 오류를, 두 번째로 결과를 받는 "오류 우선 콜백" 규칙을 따릅니다.
import { readFile } from "node:fs";
readFile("notes.txt", "utf8", (error, text) => {
if (error) {
console.error("읽기 실패:", error.message);
return;
}
console.log(text.length, "글자");
});콜백 안에서 또 다른 비동기 작업을 하면 들여쓰기가 계속 깊어지고 오류 처리가 흩어집니다. 이를 흔히 "콜백 지옥"이라 부르며, Promise가 이 문제를 풀기 위해 등장했습니다.
Promise는 "나중에 완료될 값"을 나타내는 객체입니다. 상태는 대기(pending)에서 이행(fulfilled) 또는 거부(rejected) 중 하나로 한 번만 바뀝니다. then으로 결과를, catch로 오류를, finally로 정리 작업을 연결합니다.
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
delay(300, "완료")
.then((value) => value.toUpperCase())
.then((upper) => console.log(upper))
.catch((error) => console.error(error))
.finally(() => console.log("끝"));
// 여러 Promise 조합하기
const all = await Promise.all([delay(100, "a"), delay(200, "b")]); // ["a", "b"] — 하나라도 실패하면 실패
const fastest = await Promise.race([delay(100, "빠름"), delay(500, "느림")]); // "빠름"| 메서드 | 결과 |
|---|---|
Promise.all | 모두 성공하면 결과 배열, 하나라도 실패하면 즉시 실패 |
Promise.allSettled | 성공과 실패를 모두 기다려 각 결과를 알려 줌 |
Promise.race | 가장 먼저 끝난 하나의 결과(성공이든 실패든) |
Promise.any | 가장 먼저 성공한 결과, 모두 실패하면 AggregateError |
async 함수는 항상 Promise를 돌려주며, 그 안에서 await를 쓰면 Promise가 끝날 때까지 기다렸다가 결과를 받습니다. 비동기 코드를 동기 코드처럼 위에서 아래로 읽을 수 있게 해 줍니다. 서로 의존하지 않는 작업은 차례로 await하지 말고 Promise.all로 동시에 시작하면 더 빠릅니다.
async function loadDashboard(userId) {
// 차례대로: 앞 결과가 뒤 작업에 필요할 때
const user = await getUser(userId);
const team = await getTeam(user.teamId);
// 동시에: 서로 독립적인 작업
const [posts, alerts] = await Promise.all([getPosts(userId), getAlerts(userId)]);
return { user, team, posts, alerts };
}fetch는 브라우저와 최근 Node.js에 모두 내장된 HTTP 요청 함수입니다. 주의할 점은 404나 500 같은 오류 응답에서도 Promise가 거부되지 않는다는 것입니다. 네트워크 자체가 실패할 때만 거부되므로 response.ok를 직접 확인해야 합니다.
async function getJson(url) {
const response = await fetch(url, {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(5000), // 5초가 지나면 요청을 취소합니다
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${url}`);
}
return response.json();
}
try {
const repo = await getJson("https://api.github.com/repos/nodejs/node");
console.log(repo.full_name, repo.stargazers_count);
} catch (error) {
console.error("요청 실패:", error.message);
}함수는 자신이 만들어진 곳의 변수를 기억합니다. 바깥 함수가 끝난 뒤에도 안쪽 함수가 그 변수에 접근할 수 있는데, 이를 클로저라고 합니다. 클로저를 쓰면 외부에서 직접 바꿀 수 없는 상태를 만들 수 있습니다.
function createCounter() {
let count = 0; // 바깥에서는 접근할 수 없습니다
return {
increment: () => ++count,
reset: () => { count = 0; },
};
}
const counter = createCounter();
counter.increment();
console.log(counter.increment()); // 2
// let은 반복마다 새 변수를 만들므로 클로저가 각각의 i를 기억합니다
for (let i = 0; i < 3; i += 1) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}this는 함수가 정의된 곳이 아니라 호출된 방식에 따라 정해집니다. obj.method()처럼 부르면 this는 obj이지만, 메서드를 변수에 꺼내 따로 부르면 연결이 끊깁니다. 화살표 함수는 자신만의 this가 없고 바깥 범위의 this를 그대로 씁니다.
const timer = {
seconds: 0,
start() {
// 화살표 함수는 start의 this(timer)를 그대로 씁니다
setInterval(() => {
this.seconds += 1;
}, 1000);
},
report() {
return `${this.seconds}초`;
},
};
const report = timer.report;
// report(); // this가 timer가 아니므로 오류가 나거나 엉뚱한 값이 나옵니다
const bound = timer.report.bind(timer); // this를 timer로 고정
console.log(bound()); // "0초"async/await를 쓰고, 독립적인 작업은 Promise.all로 동시에 실행합니다.fetch는 HTTP 오류에서 거부되지 않으므로 response.ok를 확인합니다.this는 호출 방식에 따라 달라지므로 화살표 함수나 bind로 고정합니다.fs, node:test 등 내장 모듈의 API를 확인합니다.package.json 항목을 설명합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.