비동기 처리(Promise, async/await) 챕터
딥다이브 요약 GitHub
안녕하세요. 8주차입니다. 이번 주는 두 가지 큰 주제를 다룹니다. 클로저는 지난 주에 배운 렉시컬 스코프에서 자연스럽게 이어지는 개념으로, React의 useState 같은 훅이 어떻게 동작하는지를 이해하는 기반입니다. 비동기 처리는 서버에서 데이터를 가져오거나 파일을 읽는 등 시간이 걸리는 작업을 처리하는 JS의 핵심 방법입니다. 콜백에서 Promise, async/await으로 이어지는 발전 흐름을 이해하는 것이 목표입니다.
아래 10문제를 막힘없이 풀 수 있다면,
이번 주 학습 자료를 생략하거나, 모르는 부분만 선택적으로 읽으셔도 좋습니다.
바로 하단의 더 알면 좋을 것들을 확인해보세요.
클로저는 함수가 선언될 때의 렉시컬 환경을 기억하여, 그 환경의 변수에 계속 접근할 수 있는 특성입니다. 렉시컬 스코프의 자연스러운 결과물입니다.
function makeCounter() {
let count = 0; // 외부 함수의 변수
return function () {
// 내부 함수 — 클로저
count++;
return count;
};
}
const counter = makeCounter();
// makeCounter()는 이미 종료됐지만
console.log(counter()); // 1
console.log(counter()); // 2
console.log(
클로저를 활용하면 외부에서 직접 접근할 수 없는 private 변수를 만들 수 있습니다.
function createAccount(initialBalance) {
let balance = initialBalance; // 외부에서 직접 접근 불가
return {
deposit(amount) {
balance += amount;
},
withdraw(amount) {
if (amount > balance) return "잔액 부족";
balance -= amount;
},
getBalance() {
return balance;
},
};
}
const account = createAccount
React의 useState 훅이 클로저를 기반으로 동작합니다.
컴포넌트가 리렌더링될 때마다 이벤트 핸들러 내부의 count, setState 등이
클로저로 기억됩니다. useEffect의 의존성 배열을 빠뜨리면 오래된 클로저(stale
closure)를 참조하는 버그가 생기는 이유도 클로저 때문입니다.
서버에서 데이터를 가져오는 fetch, 타이머 setTimeout 등은 결과가 나중에
옵니다. 과거에는 콜백으로 처리했고, 중첩이 깊어지면 콜백 지옥
이 생겼습니다. Promise는 비동기 작업의 결과를 값(객체)으로
표현하여 이를 해결합니다.
// 콜백 지옥
fetch("/user", function (user) {
fetch("/posts?userId=" + user.id, function (posts) {
fetch("/comments?postId=" + posts[0].id, function (comments) {
// 점점 깊어짐...
});
});
});
// Promise 체이닝 — 수평으로 나열
fetch("/user")
.then((user) user
pending: 비동기 작업 진행 중 (초기 상태)fulfilled: 작업 성공 — .then() 콜백 실행rejected: 작업 실패 — .catch() 콜백 실행// 순차 실행 — 느림 (앞이 끝나야 다음 시작)
const a = await fetchA();
const b = await fetchB();
// 병렬 실행 — 동시에 시작하고 모두 완료 기다림
const [a, b] = await Promise.all([fetchA(), fetchB()]);
// 가장 먼저 완료된 것만 사용
const fastest = await Promise.race([fetchA(), fetchB()]);
async/await은 Promise를 기반으로 한
문법적 설탕(syntactic sugar)입니다. 비동기 코드를 마치 동기
코드처럼 읽기 쉽게 작성할 수 있게 해줍니다.
// Promise 체이닝
function getUser(id) {
return fetch(`/users/${id}`)
.then((res) => res.json())
.then((user) => user)
.catch((err) => {
throw err;
});
}
// async/await — 같은 동작, 더 직관적
async function
// 잘못된 예 — 순차 실행 (불필요하게 느림)
async function bad() {
const a = await fetchA(); // fetchA 완료 대기
const b = await fetchB(); // 그 다음에야 fetchB 시작
}
// 올바른 예 — 병렬 실행
async function good() {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
}
실무에서는 React 컴포넌트에서 데이터를 가져올 때 useEffect 안에서 async
함수를 정의하고 호출하는 패턴을 주로 사용합니다.
useEffect(() => {
async function fetchData() {
try {
const res = await fetch("/api/data");
const data = await res.json();
setData(data);
} catch (err) {
setError(err);
}
}
fetchData();
}, []);
아래 코드를 브라우저 콘솔에서 직접 실행해보고 결과를 입력해보세요.
function makeAdder(x) {
return function (y) {
return x + y;
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(3));
console.log(add10(3));
console.log(add5(add10(1)));function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counterA = makeCounter();
const counterB = makeCounter();
console.log(counterA()); // ?
console.log(counterA()); // ?
console.log(counterB()); // ?