Section 4 (함수·일급 객체·매개변수) 챕터 — 3:25:25부터, 클로저 이해의 전제
딥다이브 요약 GitHub
안녕하세요. 8주차입니다. 이번 주는 두 가지 큰 주제를 다룹니다. 클로저는 지난 주에 배운 렉시컬 스코프에서 자연스럽게 이어지는 개념으로, React의 useState 같은 훅이 어떻게 동작하는지를 이해하는 기반입니다. 비동기 처리는 서버에서 데이터를 가져오거나 파일을 읽는 등 시간이 걸리는 작업을 처리하는 JS의 핵심 방법입니다. 콜백에서 Promise, async/await으로 이어지는 발전 흐름을 이해하는 것이 목표입니다.
이번 주 비대면 자료는 대면 8주차의 API와 통신 주제와 바로 이어집니다. Promise와 async/await은 문법 자체보다, 서버에 요청하고 응답을 받아 화면 상태로 연결할 때 가장 많이 쓰입니다. 그래서 이번 주는 "비동기 문법을 안다" 에서 끝내지 말고, 요청 성공·실패·로딩 상태를 어떻게 다룰지까지 같이 생각해보면 좋습니다.
아래 10문제를 막힘없이 풀 수 있다면,
이번 주 학습 자료를 생략하거나, 모르는 부분만 선택적으로 읽으셔도 좋습니다.
바로 하단의 더 알면 좋을 것들을 확인해보세요.
먼저 한 줄 리마인드 —
렉시컬 스코프는 "함수가 선언된 위치"를 기준으로 접근 가능한 상위 변수가 정해지는 규칙
이었어요(week6 복습). 클로저는 바로 이 렉시컬 환경을
함수가 끝난 뒤에도 계속 기억하는 현상입니다. 렉시컬 스코프의
자연스러운 결과물이고, 이 특성이 없으면 React의 useState 같은 것도 동작 못
해요.
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를 배우지 않은 상태에서 전부 이해할 필요 없습니다.
"React도 클로저 위에 얹혀 돌아가는구나" 정도만 감 잡고, 자세한 이야기는 week10
React 주차에서 다시 만납니다. 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() 콜백 실행아래 코드에 await가 미리 등장합니다.
await는 3번 섹션에서 정식으로 설명합니다 — 지금은
"Promise가 완료될 때까지 기다렸다가 값을 꺼낸다" 정도로만 읽어주세요.
// 순차 실행 — 느림 (앞이 끝나야 다음 시작)
async function sequential() {
const a = await fetchA();
const b = await fetchB();
return [a, b];
}
// 병렬 실행 — 동시에 시작하고 모두 완료 기다림
async function parallel() {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
return [a b
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
함수를 정의하고 호출하는 패턴을 주로 사용합니다. 아래 코드는
React 문법이라 지금 당장 이해 안 해도 됩니다. "async 함수를
만들고 그 안에서 await를 쓴다" 패턴만 눈에 익혀두세요. React는 week10에서
만납니다.
useEffect(() => {
async function fetchData() {
try {
const res = await fetch("/api/data");
const data = await res.json();
setData(data);
} catch (err) {
setError(err);
}
}
fetchData();
}, []);
fetch()를 사용할 때 가장 많이 하는 실수는 "응답이 왔으니 성공"이라고
생각하는 것입니다. fetch()는 네트워크 자체가 실패한 경우에는 reject되지만,
404나 500 같은 HTTP 에러 응답은 Promise reject로 처리하지 않습니다
. 서버가 응답을 보내긴 했기 때문입니다. 그래서 res.ok나 res.status를 직접
확인해야 합니다.
async function fetchUser(userId) {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
const data = await res.json();
return data;
}
화면에서는 보통 데이터를 가져오는 동안의 로딩 상태, 실패했을 때의 에러 상태, 성공했을 때의 데이터 상태를 나눠서 다룹니다. 아직 React를 배우기 전이라도 이 세 가지 이름은 꼭 기억해두세요.
let loading = true;
let error = null;
let data = null;
try {
data = await fetchUser(1);
} catch (err) {
error = err;
} finally {
loading = false;
}
요청끼리 서로 의존하지 않는다면 Promise.all()로 병렬 실행할 수 있습니다.
예를 들어 사용자 정보와 알림 개수는 동시에 가져와도 됩니다. 반대로 두 번째
요청이 첫 번째 요청 결과에 의존한다면 순차적으로 await해야 합니다.
// 서로 독립적인 요청 — 병렬 가능
const [user, notifications] = await Promise.all([
fetchUser(userId),
fetchNotifications(userId),
]);
// 앞 요청 결과가 뒤 요청에 필요 — 순차 실행
const user = await fetchUser(userId);
const posts = await fetchPostsByUser(user.id);
이 패턴들이 대면 8주차의 API 계약 이야기와 연결됩니다. 프론트엔드 개발자는 단순히 요청을 보내는 것에서 끝나지 않고, 실패 응답이 어떤 형태로 오는지, 빈 데이터는 어떻게 오는지, 어떤 요청을 병렬로 묶을 수 있는지까지 백엔드와 맞춰야 합니다.
아래 코드를 브라우저 콘솔에서 직접 실행해보고 결과를 입력해보세요.
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()); // ?