C-3: 함수 어드밴스
안녕하세요, 홍순구 튜터입니다. 지난 시간 우리는 반복되는 코드를 함수로 묶어 재사용하는 법을 배웠어요. "좋아요 안내하기"를 announceLike로, "숫자 예쁘게 다듬기"를 formatLikeCount로 묶어두고, 이름만 불러서 백 번이고 천 번이고 다시 썼죠.
그런데 마무리에서 한 가지 궁금증을 던졌어요. 함수 안에서 만든 변수는 함수 밖에서도 보일까요? formatLikeCount 안에서 쓴 count를 함수 바깥에서 부르면 어떻게 될까요? 오늘은 바로 이 질문부터 시작합니다.
오늘 배울 건 세 가지예요. 변수가 보이는 범위인 스코프(scope), 함수가 자기 변수를 계속 기억하는 신기한 능력인 클로저(closure), 그리고 함수를 다른 함수에 값처럼 넘겨주는 콜백(callback)이죠. 함수를 "그냥 부르는 것"을 넘어, 함수의 새로운 차원을 여는 시간이에요.
스코프는 "변수가 어디까지 보이는가"를 다루는 개념이에요. 가장 바깥을 전역, 함수 안을 함수 스코프, if나 for의 중괄호 안을 블록 스코프라고 부르는데, 마치 러시아 인형처럼 안쪽에 차곡차곡 들어 있어요.
전역 스코프 (코드 어디서나 보임)
┌──────────────────────────────────┐
│ const appName = "인스타 클론" │
│ ┌────────────────────────────┐ │
│ │ 함수 스코프 (함수 안에서만) │ │
│ │ const greeting = ... │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ 블록 스코프 (if/for 안)│ │ │
│ │ │ let i = ... │ │ │
│ │ └──────────────────────┘ │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
안쪽에선 바깥이 보이지만, 바깥에선 안쪽이 안 보여요
오늘 만든 함수가 지난 시간보다 훨씬 똑똑해질 거예요. 마지막엔 이 셋을 다 모아서, 게시물을 카테고리별로 걸러내는 피드 필터링 함수를 직접 완성합니다.
💡 오늘 수업의 핵심 — "변수는 자기가 태어난 범위(스코프) 안에서만 보인다. 함수는 자기 변수를 기억할 수 있고(클로저), 다른 함수에 값처럼 넘겨질 수 있다(콜백)." 🎯
🎯 학습 목표
- 스코프가 무엇인지 이해하고, 전역 스코프와 함수 스코프를 구분합니다.
- 블록 스코프(
if/for중괄호 안)와 스코프 체인(안쪽이 바깥 변수를 찾는 규칙)을 이해합니다. - 클로저를 이해하고, 함수가 자신의 변수를 계속 기억하는 모습을 카운터로 확인합니다.
- 클로저를 활용해 좋아요 토글의 상태를 함수 안에 숨겨봅니다.
- 콜백 함수를 다른 함수에 인자로 넘기고, 그 안에서 불러 쓰는 패턴을 익힙니다.
- 스코프·클로저·콜백을 모두 활용해 피드 필터링 함수
filterPosts를 완성합니다.
Step 1: 스코프란 무엇인가 — 전역과 함수 스코프
지난 시간 마무리에서 던진 질문을 다시 가져와볼게요. 함수 안에서 만든 변수는 함수 밖에서도 보일까요? 정답부터 말하면, 안 보여요. 변수에는 "여기서부터 여기까지만 보인다"는 범위가 있는데, 이 범위를 스코프(scope, 범위)라고 불러요.
스코프는 크게 두 가지로 나눠 시작해요. 함수 바깥에서 만들어 코드 어디서나 보이는 전역 스코프(global scope), 그리고 함수 중괄호 안에서 만들어 그 함수 안에서만 보이는 함수 스코프(function scope)예요.
// instagram-clone-frontend/js/main.js
const appName = "인스타 클론"; // 전역 변수
function showAppName() {
const greeting = "환영합니다"; // 함수 안에서만 사는 변수
console.log(appName + "에 " + greeting); // 전역 appName 은 안에서도 보임
}
showAppName(); // 인스타 클론에 환영합니다
console.log(appName); // 인스타 클론 (전역은 함수 밖에서도 보임)
// console.log(greeting); // ReferenceError! 함수 안 변수는 밖에서 안 보여요
콘솔 출력은 이래요.
인스타 클론에 환영합니다
인스타 클론
appName은 함수 바깥에 있는 전역 변수라서 어디서든 보여요. 함수 안에서도(showAppName 안의 console.log), 함수 밖에서도(맨 아래 console.log(appName)) 잘 찍히죠.
반면 greeting은 showAppName 함수 안에서 만든 변수예요. 그래서 함수 안에서는 보이지만, 함수 밖에선 존재 자체를 몰라요. 맨 아래 주석을 풀어서 console.log(greeting)을 실행하면 어떻게 될까요?
ReferenceError — "그런 변수 없는데요?"
ReferenceError라는 에러가 나요. "그런 이름의 변수를 못 찾겠다"는 뜻이에요. greeting은 함수 안에 갇혀 있어서, 함수 밖에서는 닿을 수가 없거든요.
function showAppName() {
const greeting = "환영합니다"; ← 이 방 안에서만 보임
} ← 함수가 끝나면 greeting 도 사라짐
console.log(greeting); ✗ 방 밖에선 greeting 을 찾을 수 없음 → ReferenceError
함수를 하나의 방이라고 생각해보세요. 방 안에서 꺼낸 물건(변수)은 그 방 안에서만 쓸 수 있고, 방을 나오면(함수가 끝나면) 정리돼서 사라져요. 그래서 다음 사람이 그 방에 들어와도 이전 물건은 남아 있지 않죠.
💡 오전에 Java로 메서드를 배웠다면 이미 겪어본 규칙이에요. 메서드 안에서 선언한 지역 변수가 메서드 밖에선 안 보이는 것과 똑같아요. "변수는 자기가 태어난 중괄호 안에서만 산다"는 건 언어가 달라도 통하는 기본기예요.
Step 2: 블록 스코프와 스코프 체인
함수 스코프를 봤으니, 한 단계 더 안쪽으로 들어가볼게요. if나 for의 중괄호 { } 안에서 만든 변수는 어떨까요? 이것도 그 중괄호 안에서만 보여요. 이걸 블록 스코프(block scope)라고 불러요. 우리가 쓰는 let과 const는 모두 블록 스코프를 따라요.
// instagram-clone-frontend/js/main.js
let totalLikes = 0;
if (totalLikes === 0) {
const message = "아직 좋아요가 없어요"; // if 블록 안에서만 보임
console.log(message);
}
// console.log(message); // ReferenceError! if 블록 밖에선 안 보여요
message는 if의 중괄호 안에서 태어났어요. 그래서 if 블록 안에서는 잘 보이지만, 블록을 벗어나면 사라져요. 주석 처리한 마지막 줄을 풀면 또 ReferenceError가 나죠.
for의 반복 변수 i도 마찬가지예요. 루프가 끝나면 i는 정리돼서 사라져요.
// for 의 i 도 블록 스코프 — 루프가 끝나면 사라져요
for (let i = 1; i <= 3; i++) {
console.log(i + "번째 게시물 확인");
}
// console.log(i); // ReferenceError! for 밖에선 i 가 없어요
콘솔 출력은 이래요.
1번째 게시물 확인
2번째 게시물 확인
3번째 게시물 확인
루프 안에서는 i가 1, 2, 3으로 잘 돌지만, 루프가 끝난 뒤 console.log(i)를 부르면 i가 없다고 에러가 나요. 반복용 변수가 일을 마치면 깔끔하게 정리되는 거죠.
스코프 체인 — 안쪽은 바깥을 찾아 올라간다
그럼 반대로, 안쪽 함수에서 바깥 변수를 쓰는 건 될까요? 이건 돼요. 함수가 자기 안에서 변수를 못 찾으면, 한 칸 바깥 스코프로 나가서 찾아봐요. 거기도 없으면 또 한 칸 더 바깥으로요. 이렇게 안쪽에서 바깥으로 변수를 찾아 올라가는 규칙을 스코프 체인(scope chain)이라고 불러요.
// 스코프 체인: 안쪽 함수는 자기에게 없는 변수를 바깥에서 찾아 올라가요
const outerTag = "#daily";
function printTag() {
console.log(outerTag); // 내 안에 없으면 바깥 스코프에서 찾음
}
printTag(); // #daily
printTag 함수 안에는 outerTag가 없어요. 그런데도 #daily가 잘 찍히죠. 함수가 자기 안에서 outerTag를 못 찾자, 바깥(전역)으로 나가서 찾아낸 거예요.
const outerTag = "#daily"; ← 전역 스코프
▲
│ 못 찾으면 바깥으로 올라가서 찾음 (스코프 체인)
│
function printTag() {
console.log(outerTag); ← 내 안엔 없네? → 바깥에서 찾자
}
정리하면 방향이 한쪽이에요. 안쪽은 바깥을 볼 수 있지만, 바깥은 안쪽을 볼 수 없어요. 이 "안쪽이 바깥을 기억한다"는 성질이, 바로 다음에 배울 클로저의 핵심 열쇠가 돼요.
Step 3: 클로저 — 함수가 자신의 변수를 기억한다
이제 오늘의 하이라이트, 클로저(closure)예요. 이름은 어렵게 들리지만 핵심은 간단해요. 함수가 자기가 태어난 곳의 변수를 계속 기억하는 능력이에요.
방금 스코프 체인에서 봤죠. 안쪽 함수는 바깥 변수를 찾아 쓸 수 있다고요. 그런데 여기서 한 발 더 나아가, 함수 안에서 함수를 만들어 바깥으로 돌려주면 어떻게 될까요? 돌려받은 안쪽 함수는 자기가 태어난 곳의 변수를 그대로 기억한 채로 밖에 나와요. 숫자를 하나씩 세는 카운터로 확인해볼게요.
// instagram-clone-frontend/js/main.js
function makeCounter() {
let count = 0; // 바깥 함수의 변수
return function () { // 안쪽 함수를 돌려줌
count = count + 1; // 바깥 count 를 계속 기억하고 더함
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3 ← count 가 사라지지 않고 기억돼요!
콘솔 출력은 이래요.
1
2
3
여기가 신기한 부분이에요. Step 1에서 "함수가 끝나면 안의 변수는 사라진다"고 했죠? 그런데 count는 안 사라졌어요. makeCounter()는 이미 끝났는데도, 돌려받은 함수(counter)를 부를 때마다 count가 1씩 늘어나며 기억되고 있어요.
makeCounter() 호출 → count = 0 인 방을 만들고,
그 방의 count 를 기억하는 함수를 돌려줌
│
const counter = ◀───────────────┘ (count 를 품은 함수)
counter() → count 0→1, 반환 1
counter() → count 1→2, 반환 2 ← 같은 count 가 계속 살아있음!
counter() → count 2→3, 반환 3
돌려받은 함수가 count를 손에 꼭 쥐고 나온 거예요. 그래서 makeCounter라는 방이 사라진 뒤에도 count는 그 함수와 함께 계속 살아 있어요. 이게 클로저예요. 함수가 자기가 만든 변수를 계속 기억하는 능력, 지난 시간 마무리에서 예고했던 바로 그 신기한 능력이죠.
🙋 학생 질문 — "튜터님, counter 를 두 개 만들면 count 를 같이 쓰나요?"
아니에요. makeCounter()를 부를 때마다 새로운 count 방이 만들어져요. 그래서 카운터마다 자기만의 count를 따로 기억해요.
const a = makeCounter();
const b = makeCounter();
console.log(a()); // 1
console.log(a()); // 2
console.log(b()); // 1 ← b 는 자기만의 count, a 와 안 섞여요
a가 2까지 셌어도 b는 자기 count를 0부터 새로 세요. 클로저가 카운터마다 독립된 기억을 만들어주는 거예요.
Step 4: 클로저 활용 — 좋아요 토글 상태 숨기기
클로저가 "변수를 기억한다"는 건 알겠는데, 이게 실제로 어디에 쓸모가 있을까요? 좋아요 버튼으로 바로 써먹어볼게요.
지난 시간 toggleLike를 만들었죠. 그때는 좋아요 상태(myLiked)를 함수 밖에 변수로 두고, 함수에 넘겼다가 돌려받기를 반복했어요. 그런데 이러면 상태 변수가 밖에 노출돼서, 실수로 다른 코드가 건드릴 수도 있어요. 클로저를 쓰면 그 상태를 함수 안에 숨겨둘 수 있어요.
// instagram-clone-frontend/js/main.js
function makeLikeToggle() {
let liked = false; // 이 상태는 함수 안에 숨어 있어요
return function () {
liked = !liked; // 누를 때마다 반대로 뒤집기
return liked;
};
}
function showLike(state) {
if (state) {
console.log("켜짐 ❤️");
} else {
console.log("꺼짐 🤍");
}
}
const toggle = makeLikeToggle();
showLike(toggle()); // 켜짐 ❤️
showLike(toggle()); // 꺼짐 🤍
showLike(toggle()); // 켜짐 ❤️
콘솔 출력은 이래요.
켜짐 ❤️
꺼짐 🤍
켜짐 ❤️
makeLikeToggle 안의 liked가 바로 클로저로 숨겨진 상태예요. toggle()을 부를 때마다 liked = !liked로 뒤집히는데(!는 지난 시간 배운 NOT 연산자, 참↔거짓을 뒤집죠), 이 liked는 makeCounter의 count처럼 계속 기억돼요. 그래서 누를 때마다 켜짐 → 꺼짐 → 켜짐으로 번갈아 바뀌죠.
핵심은 liked를 함수 밖에서 직접 못 건드린다는 거예요. Step 1에서 배운 대로, 함수 안 변수는 밖에서 안 보이니까요. 좋아요 상태가 함수 안에 안전하게 숨어서, 오직 toggle()을 통해서만 바뀔 수 있어요.
💡 지난 시간
toggleLike는 상태(myLiked)를 밖에서 들고 다녔지만, 오늘makeLikeToggle은 함수가 상태를 직접 기억해요. "데이터를 함수 안에 숨기고, 정해진 통로로만 바꾸게 한다" — 이게 클로저가 실무에서 사랑받는 이유예요. 나중에 더 큰 프로그램을 만들 때 이 패턴을 자주 만나게 될 거예요.
Step 5: 콜백 — 함수를 값으로 건네주기
세 번째 주제, 콜백(callback)이에요. 지금까지 우리는 함수에 숫자나 글자를 넘겼어요. announceLike(42)처럼요. 그런데 JavaScript에서는 함수 자체를 다른 함수에 넘길 수도 있어요. 지난 시간 "함수도 변수에 담을 수 있다"고 했던 것 기억나시죠? 변수에 담을 수 있으면, 인자로 넘길 수도 있어요.
이렇게 다른 함수에 인자로 넘겨져서, 그 안에서 불려 실행되는 함수를 콜백 함수라고 불러요.
// instagram-clone-frontend/js/main.js
function sayHi() {
console.log("안녕하세요!");
}
function runTwice(callback) { // callback 자리에 함수를 받음
callback(); // 받은 함수를 부름
callback(); // 한 번 더
}
runTwice(sayHi); // 안녕하세요! (두 번)
콘솔 출력은 이래요.
안녕하세요!
안녕하세요!
여기서 눈여겨볼 게 하나 있어요. runTwice(sayHi)에서 sayHi 뒤에 괄호가 없죠. sayHi()가 아니라 sayHi예요. 이 차이가 정말 중요해요.
sayHi() → 함수를 지금 실행해서, 그 결과를 넘김
sayHi → 함수 자체를 넘김 (실행은 나중에 runTwice 가)
괄호를 붙이면 "지금 실행해줘"가 되고, 괄호를 빼면 "이 함수 덩어리를 통째로 건네줄게"가 돼요. 콜백은 후자예요. runTwice에게 sayHi라는 함수를 통째로 건네주면, runTwice가 자기 안에서 callback()으로 두 번 불러 실행하죠.
이름 없는 화살표 함수를 그 자리에서 바로 만들어 넘길 수도 있어요.
// 이름 없는 화살표 함수를 그 자리에서 바로 넘겨도 돼요
runTwice(() => console.log("좋아요 눌렀어요"));
콘솔 출력은 이래요.
좋아요 눌렀어요
좋아요 눌렀어요
() => console.log("좋아요 눌렀어요")라는 화살표 함수를 따로 이름 붙이지 않고 runTwice에 바로 넘겼어요. 한 번 쓰고 말 짧은 함수라면 이렇게 그 자리에서 만들어 넘기는 게 깔끔하죠.
🙋 학생 질문 — "튜터님, 왜 함수를 굳이 넘기나요? runTwice 안에 그냥 적으면 안 되나요?"
좋은 질문이에요. runTwice 안에 console.log("안녕하세요!")를 직접 적어버리면, 이 함수는 "안녕하세요만 두 번 찍는 함수"로 굳어버려요. 다른 걸 두 번 하고 싶으면 새 함수를 또 만들어야 하죠.
콜백으로 받으면 runTwice는 "받은 걸 두 번 실행"이라는 뼈대만 갖고, 무엇을 할지는 부르는 쪽이 정해요. 인사를 두 번 할 수도, 좋아요를 두 번 누를 수도 있죠. "반복하는 틀"과 "실제 할 일"을 분리하는 거예요. 이 유연함이 콜백의 진짜 힘이에요.
Step 6: 콜백으로 배열을 돌며 실행하기
콜백의 쓸모가 가장 빛나는 곳이 배열을 돌 때예요. 배열의 각 요소마다 같은 동작을 해야 하는데, 그 "동작"을 콜백으로 받으면 하나의 순회 틀로 여러 가지 일을 할 수 있거든요.
지난 시간 배운 for 루프로, 배열을 돌면서 요소 하나하나를 콜백에 넘기는 함수를 직접 만들어볼게요.
// instagram-clone-frontend/js/main.js
function forEachItem(items, callback) {
for (let i = 0; i < items.length; i++) {
callback(items[i]); // 요소 하나를 콜백에 넘김
}
}
const tags = ["#여행", "#맛집", "#일상"];
forEachItem(tags, (tag) => {
console.log("태그: " + tag);
});
콘솔 출력은 이래요.
태그: #여행
태그: #맛집
태그: #일상
forEachItem은 배열(items)과 콜백(callback) 두 개를 받아요. 그리고 for 루프로 배열을 돌면서, 요소 하나(items[i])를 꺼내 콜백에 넘겨요. tags 배열을 넘기면 #여행, #맛집, #일상이 차례로 tag 자리에 들어가서 콜백이 세 번 실행되죠.
forEachItem(tags, 콜백)
│
├─ items[0] "#여행" ─▶ 콜백("#여행") → 태그: #여행
├─ items[1] "#맛집" ─▶ 콜백("#맛집") → 태그: #맛집
└─ items[2] "#일상" ─▶ 콜백("#일상") → 태그: #일상
진짜 매력은 콜백만 바꾸면 같은 순회로 다른 일을 할 수 있다는 거예요. 이번엔 각 태그의 글자 수를 세보죠.
// 콜백만 바꾸면 같은 순회로 다른 일을 할 수 있어요
forEachItem(tags, (tag) => {
console.log(tag + " 의 길이: " + tag.length);
});
콘솔 출력은 이래요.
#여행 의 길이: 3
#맛집 의 길이: 3
#일상 의 길이: 3
forEachItem은 한 글자도 안 바뀌었어요. 넘기는 콜백만 "출력하기"에서 "길이 세기"로 바꿨을 뿐인데 하는 일이 달라졌죠. 순회하는 틀은 그대로 두고, 각 요소에 무엇을 할지만 갈아끼우는 거예요.
💡 사실 JavaScript 배열에는 이런 순회를 미리 만들어둔 도구들이 있어요. 우리가 방금 직접 만든
forEachItem같은 걸 배열이 기본으로 갖고 있죠. 다음 시간에 그 도구들을 만나면, 오늘 직접 만들어본 이 원리 덕분에 "아, 안에서 콜백을 부르는 거구나" 하고 바로 이해될 거예요.
Step 7: 피드 필터링 함수 완성 — filterPosts
자, 이제 오늘 배운 스코프·클로저·콜백을 한자리에 모아 피드 필터링 함수를 완성할 차례예요. 지난 시간 마무리에서 약속했던, "여행 게시물만 보여줘" 같은 진짜 인스타그램스러운 기능이죠.
그런데 게시물 하나에는 정보가 여러 개 붙어 있어요. 글 내용도 있고, 카테고리도 있죠. 이렇게 여러 정보를 한 덩어리로 묶을 때 객체(object)를 써요. 객체는 다음 시간에 제대로 배울 건데, 오늘은 "이렇게 생겼다"만 미리 살짝 볼게요.
// instagram-clone-frontend/js/main.js
const posts = [
{ caption: "제주 여행 다녀왔어요", category: "travel" },
{ caption: "오늘의 맛집 발견", category: "food" },
{ caption: "평범한 일상", category: "daily" },
{ caption: "발리 서핑 도전", category: "travel" },
{ caption: "집밥 한 끼", category: "food" }
];
중괄호 { } 하나가 게시물 하나예요. 그 안에 caption(글 내용)과 category(카테고리)가 짝지어 들어 있죠. 이런 게시물 다섯 개가 배열로 묶여 있어요. 객체 안의 값을 꺼낼 때는 점(.)을 써요. posts[0].category라고 하면 첫 게시물의 카테고리인 "travel"이 나오죠. 지난 시간 postIds.length에서 점을 찍어 length를 꺼낸 것과 같은 방식이에요.
filterPosts — 카테고리로 걸러 콜백 실행
이제 이 게시물들 중에서 원하는 카테고리만 골라내는 함수를 만들어요. 골라낸 게시물을 어떻게 보여줄지는 콜백으로 받고요.
// instagram-clone-frontend/js/main.js
function filterPosts(items, category, onMatch) {
let matched = 0; // 함수 스코프 변수 (몇 개 찾았나)
for (let i = 0; i < items.length; i++) {
const post = items[i];
if (post.category === category) { // 카테고리가 같은가?
matched = matched + 1;
onMatch(post); // 일치한 게시물을 콜백에 넘김
}
}
return matched; // 찾은 개수를 돌려줌
}
한 줄씩 뜯어보면 오늘 배운 게 전부 들어 있어요.
let matched = 0— 몇 개를 찾았는지 세는 변수예요.filterPosts안에서만 사는 함수 스코프 변수죠(Step 1).for루프로 게시물을 하나씩 꺼내,post.category === category로 카테고리가 같은지 확인해요.- 일치하면
onMatch(post)로 그 게시물을 콜백에 넘겨요(Step 5·6). 보여주는 방식은 부르는 쪽이 정하죠. - 다 돌고 나면
return matched로 찾은 개수를 돌려줘요(지난 시간return).
이제 "여행 게시물만 보여줘"를 실행해볼게요.
// "여행 게시물만 보여줘" — 콜백으로 출력 방식을 정해요
const travelCount = filterPosts(posts, "travel", (post) => {
console.log("✈️ " + post.caption);
});
console.log("여행 게시물 " + travelCount + "개");
콘솔 출력은 이래요.
✈️ 제주 여행 다녀왔어요
✈️ 발리 서핑 도전
여행 게시물 2개
filterPosts에 게시물 배열, 찾을 카테고리("travel"), 그리고 "비행기 이모지와 함께 글 내용을 찍어라"는 콜백을 넘겼어요. 다섯 게시물 중 카테고리가 travel인 두 개만 골라져서 콜백이 두 번 실행됐고, 찾은 개수 2가 돌아왔죠.
같은 함수에 콜백만 바꾸면, 이번엔 맛집을 다른 모양으로 보여줄 수 있어요.
// 같은 함수, 콜백만 바꿔서 "맛집"을 다르게 출력해요
filterPosts(posts, "food", (post) => {
console.log("🍜 " + post.caption + " (맛집)");
});
콘솔 출력은 이래요.
🍜 오늘의 맛집 발견 (맛집)
🍜 집밥 한 끼 (맛집)
filterPosts는 그대로 두고, 카테고리를 "food"로, 콜백을 "라면 이모지와 함께 (맛집) 표시"로 바꿨을 뿐이에요. Step 6에서 본 "틀은 그대로, 콜백만 갈아끼우기"가 실제 기능에서 빛나는 모습이죠. 오늘 배운 세 개념이 이 함수 하나에 모두 녹아 있어요.
마무리
오늘 우리는 함수의 새로운 차원을 열었어요. 함수를 "그냥 부르는 것"에서, 함수가 변수를 기억하고(클로저), 다른 함수에 건네지는(콜백) 단계까지 나아갔죠. 짧게 되짚어볼게요.
- 스코프 — 변수는 자기가 태어난 범위 안에서만 보여요. 전역(어디서나) · 함수(함수 안) · 블록(
if/for안). - 스코프 체인 — 안쪽 함수는 자기에게 없는 변수를 바깥에서 찾아 올라가요. 안쪽은 바깥을 보지만, 바깥은 안쪽을 못 봐요.
- 클로저 — 함수가 자기가 만든 변수를 계속 기억하는 능력.
makeCounter의count,makeLikeToggle의liked처럼 함수 안에 상태를 숨겨둬요. - 콜백 — 함수를 값처럼 다른 함수에 넘기는 것. 넘길 땐 괄호를 빼고(
sayHi), 받은 쪽이 안에서 불러요(callback()). - filterPosts — 스코프·클로저·콜백을 모두 모아, 게시물을 카테고리별로 걸러 콜백으로 보여주는 함수를 완성.
스코프와 클로저는 처음엔 어렵게 느껴질 수 있어요. 괜찮아요. "함수 안 변수는 밖에서 안 보인다", "함수는 자기 변수를 기억한다", "함수도 값처럼 넘길 수 있다" 이 세 문장만 손에 익혀두면, 앞으로 코드를 읽다가 자연스럽게 깊어질 거예요.
다음 시간 예고
오늘 우리는 filterPosts를 for 루프로 직접 한 땀 한 땀 만들었어요. 그런데 사실 JavaScript 배열에는 이런 "걸러내기"를 한 줄로 해주는 도구가 이미 들어 있어요. 다음 시간에 배울 filter, map, reduce 같은 배열 메서드들이죠. 오늘 직접 만든 forEachItem과 filterPosts가, 다음 시간엔 단 한 줄로 줄어드는 마법을 보게 될 거예요.
그리고 오늘 살짝 맛만 본 **객체({ caption, category })**도 제대로 파헤칩니다. 게시물 데이터를 객체로 묶고, 배열에 담고, 점 표기법으로 자유롭게 꺼내 쓰는 법을 배워요. 거기에 한 번에 여러 값을 꺼내는 구조 분해, 배열을 펼쳐 합치는 **스프레드(...)**까지. 오늘 만든 filterPosts가 다음 시간엔 훨씬 세련되게 다시 태어날 거예요. 기대하세요!
과제
오늘 배운 스코프·클로저·콜백을 직접 손에 익혀볼 차례예요. 기초 → 응용 → 탐구 순서로 풀어보세요. 모든 과제는 콘솔(console.log)에서 확인하고, 오늘 배운 내용과 지난 시간 함수·return·for 루프만으로 충분히 풀 수 있어요. 배열 메서드(map/filter)는 다음 시간 거니까 아직 안 써도 돼요.
[구현] 클로저 카운터로 팔로워 수 세기 (기초)
오늘 만든 makeCounter를 응용해, 팔로워가 한 명씩 늘어나는 카운터를 만들어보세요.
function makeFollowerCounter() { ... }형태로, 안에let followers = 0;을 두고, 부를 때마다followers를 1 늘려 돌려주는 함수를 돌려주세요.const count = makeFollowerCounter();로 카운터를 하나 만든 뒤,console.log(count());를 세 번 불러1,2,3이 차례로 찍히는지 확인하세요.- 카운터를 하나 더 만들어(
const count2 = makeFollowerCounter();)count2()를 부르면1부터 새로 시작하는지 확인하고, "두 카운터가 서로 다른followers를 기억한다"를 콘솔로 눈으로 보세요.
[구현] 콜백으로 게시물 출력 방식 바꾸기 (응용)
오늘 만든 forEachItem을 활용해, 같은 배열을 콜백만 바꿔 두 가지로 출력해보세요.
const captions = ["제주 여행", "오늘의 맛집", "평범한 일상"];배열을 준비하세요.forEachItem(captions, 콜백)을 호출하되, 첫 번째 콜백은"📷 " + caption을 찍게 하세요.- 두 번째로 같은
forEachItem을 다시 호출하되, 이번엔 콜백을 바꿔caption + " (" + caption.length + "글자)"를 찍게 하세요. forEachItem은 한 글자도 안 고치고, 콜백만 바꿔 다른 결과가 나오는 걸 확인하세요.
[탐구] filterPosts로 일상 게시물 골라내기 (탐구)
오늘 만든 filterPosts와 posts 배열을 그대로 써서, 이번엔 daily(일상) 카테고리를 골라내보세요.
filterPosts(posts, "daily", 콜백)을 호출하고, 콜백에서"🏠 " + post.caption을 찍게 하세요.- 반환값(찾은 개수)을 변수에 받아
console.log("일상 게시물 " + 개수 + "개");로 출력하세요. 몇 개가 나오나요? - 한 발 더 나아가, 존재하지 않는 카테고리(
"sports")로 호출하면 콜백이 한 번도 안 불리고 개수가0이 나오는지 확인해보세요. 왜 그런지if (post.category === category)조건과 연결해 한 문장으로 정리해보세요.
생각해볼 주제
정답을 적는 문제가 아니에요. 오늘 배운 것의 "왜"를 곱씹어보는 질문들이에요. 스스로 답을 만들어본 뒤, 예시답안과 비교해보세요.
1. 왜 변수에 "보이는 범위"를 두었을까?
모든 변수가 어디서나 다 보이면 편하지 않을까요? 그런데 큰 프로그램에서 변수 수백 개가 전부 전역에 있다고 상상해보세요. 같은 이름을 실수로 두 번 쓰면 서로 덮어쓰고, 어디서 값이 바뀌었는지 추적하기도 어렵겠죠. 스코프가 변수를 함수·블록 안에 가둬주면 무엇이 좋아질까요? "변수를 좁은 범위에 가두는 게 오히려 안전하다"는 관점에서 생각해보세요.
2. 클로저로 상태를 숨기는 게 왜 좋을까?
makeLikeToggle에서 liked를 함수 안에 숨겼죠. 그냥 전역 변수 let liked = false;로 두고 써도 좋아요 토글은 동작해요. 그런데도 굳이 함수 안에 숨긴 이유가 뭘까요? 만약 다른 코드가 실수로 liked = true를 해버린다면, 전역 변수와 클로저로 숨긴 변수 중 어느 쪽이 더 안전할까요? "아무나 못 건드리게 막는 것"의 가치를 곱씹어보세요.
3. 함수를 값으로 넘길 수 있다는 게 왜 강력할까?
forEachItem은 "배열을 돈다"는 틀만 갖고, 실제 할 일은 콜백으로 받았어요. 만약 콜백 없이 forEachItem 안에 console.log를 직접 박아두었다면, 이 함수는 딱 한 가지 일밖에 못 하겠죠. 콜백으로 "할 일"을 밖에서 정해주면 같은 함수로 몇 가지 일을 할 수 있을까요? "틀과 할 일을 분리한다"는 관점에서, 함수를 값처럼 넘기는 것의 힘을 생각해보세요.
✅ 예시 답안정답 보기
과제와 생각해볼 주제의 예시답안이에요. 정답이 하나만 있는 건 아니에요. 함수 이름이나 변수 이름은 취향대로 골라도 좋아요. 중요한 건 스코프가 어디까지 보이는지 이해하고, 클로저로 변수를 기억하게 하고, 콜백으로 할 일을 함수에 넘겼는가 예요.
🎯 [과제 1 예시답안] 클로저 카운터로 팔로워 수 세기
핵심 접근
오늘 만든 makeCounter를 그대로 응용하는 과제예요. 핵심은 카운트 변수를 함수 안에 두고, 그 변수를 1씩 늘려 돌려주는 함수를 돌려주는 거예요. makeFollowerCounter를 부르면 followers = 0인 방이 만들어지고, 그 방의 followers를 기억하는 함수가 밖으로 나와요. 부를 때마다 같은 followers가 1씩 늘죠. 카운터를 또 만들면 새 followers 방이 따로 생겨서, 두 카운터가 서로 안 섞여요.
예시 구현
// instagram-clone-frontend/js/main.js 맨 아래에 이어서
function makeFollowerCounter() {
let followers = 0;
return function () {
followers = followers + 1;
return followers;
};
}
const count = makeFollowerCounter();
console.log(count()); // 1
console.log(count()); // 2
console.log(count()); // 3
const count2 = makeFollowerCounter();
console.log(count2()); // 1 ← 새 카운터는 followers 를 따로 기억
count를 세 번 부르면 followers가 사라지지 않고 1, 2, 3으로 기억돼요. 그게 클로저예요. 그리고 count2는 makeFollowerCounter를 다시 불러 만든 새 카운터라, 자기만의 followers를 0부터 새로 세요. count가 3까지 셌어도 count2는 1부터 시작하죠.
1
2
3
1
채점 포인트
| 항목 | 확인 내용 |
|---|---|
| 클로저 구조 | 함수 안에 let followers = 0;을 두고, 그걸 늘려 돌려주는 함수를 return 했는가 |
| 기억 확인 | 같은 카운터를 세 번 불러 1·2·3이 누적되는지 확인했는가 |
| 독립성 확인 | 카운터를 하나 더 만들어, followers가 서로 안 섞이고 1부터 새로 시작하는지 봤는가 |
흔한 실수
followers를 함수 밖 전역에 둠 —followers를makeFollowerCounter바깥에 두면, 모든 카운터가 같은 변수를 공유해서count2가 1이 아니라 4부터 시작해요. 카운터마다 독립적으로 세려면 변수를 반드시 함수 안에 둬야 클로저로 따로 기억돼요.- 돌려주는 함수에 괄호를 붙임 —
return function() {...}()처럼 끝에()를 붙이면 함수를 돌려주는 게 아니라 즉시 실행해버려요. 우리가 돌려줄 건 "나중에 부를 함수" 자체라서 괄호 없이return function() {...};로 돌려줘야 해요.
🎯 [과제 2 예시답안] 콜백으로 게시물 출력 방식 바꾸기
핵심 접근
오늘 만든 forEachItem을 그대로 쓰면서, 콜백만 바꿔 같은 배열을 두 가지로 출력하는 과제예요. 핵심은 forEachItem 자체는 한 글자도 안 고친다는 거예요. 순회하는 틀은 그대로 두고, "각 요소에 무엇을 할지"만 콜백으로 갈아끼우면 결과가 달라져요. 콜백을 "사진 이모지 붙이기"에서 "글자 수 세기"로 바꾸기만 하면 돼요.
예시 구현
// instagram-clone-frontend/js/main.js 맨 아래에 이어서
// (forEachItem 은 오늘 수업에서 이미 만들었어요)
const captions = ["제주 여행", "오늘의 맛집", "평범한 일상"];
// 첫 번째 콜백 — 사진 이모지 붙여 출력
forEachItem(captions, (caption) => {
console.log("📷 " + caption);
});
// 두 번째 콜백 — 글자 수 함께 출력 (forEachItem 은 그대로!)
forEachItem(captions, (caption) => {
console.log(caption + " (" + caption.length + "글자)");
});
같은 forEachItem에 같은 captions 배열을 넘겼는데, 콜백만 다르니 결과가 완전히 달라요. 첫 번째는 사진 이모지를 붙이고, 두 번째는 글자 수를 세죠. forEachItem은 "배열을 돈다"는 틀만 맡고, 실제 할 일은 콜백이 정하는 거예요.
📷 제주 여행
📷 오늘의 맛집
📷 평범한 일상
제주 여행 (5글자)
오늘의 맛집 (6글자)
평범한 일상 (6글자)
채점 포인트
| 항목 | 확인 내용 |
|---|---|
| forEachItem 재사용 | 오늘 만든 forEachItem을 고치지 않고 그대로 두 번 호출했는가 |
| 콜백 교체 | 두 호출에 서로 다른 콜백을 넘겨 결과가 달라지는지 확인했는가 |
| 길이 활용 | 두 번째 콜백에서 caption.length로 글자 수를 꺼냈는가 |
흔한 실수
forEachItem을 두 개로 복사함 — 출력이 다르다고forEachItem2를 새로 만들면 콜백을 배운 의미가 없어요. 콜백의 핵심은 하나의 함수에 다른 일을 시키는 거예요. 틀은 하나만 두고 콜백만 바꿔야 해요.- 콜백에 괄호를 붙여 넘김 —
forEachItem(captions, myCallback())처럼 콜백 자리에 괄호를 붙이면, 함수를 넘기는 게 아니라 지금 실행한 결과를 넘겨버려요. 콜백은 괄호 없이 함수 자체를 넘기거나, 그 자리에서 화살표 함수로 적어야 해요.
🎯 [과제 3 예시답안] filterPosts로 일상 게시물 골라내기
핵심 접근
오늘 만든 filterPosts와 posts를 그대로 써서, 카테고리만 "daily"로 바꿔 호출하는 과제예요. 핵심은 두 가지예요. 첫째, 반환값(찾은 개수)을 변수에 받아 출력하는 것. 둘째, 없는 카테고리("sports")로 부르면 if (post.category === category) 조건이 한 번도 참이 안 돼서 콜백이 아예 안 불리고 개수가 0이 되는 걸 확인하는 거예요.
예시 구현
// instagram-clone-frontend/js/main.js 맨 아래에 이어서
// (filterPosts 와 posts 는 오늘 수업에서 이미 만들었어요)
// "daily" 카테고리 골라내기
const dailyCount = filterPosts(posts, "daily", (post) => {
console.log("🏠 " + post.caption);
});
console.log("일상 게시물 " + dailyCount + "개");
// 없는 카테고리로 부르면? — 콜백이 한 번도 안 불려요
const sportsCount = filterPosts(posts, "sports", (post) => {
console.log("이 줄은 실행되지 않아요");
});
console.log("스포츠 게시물 " + sportsCount + "개");
posts 다섯 개 중 카테고리가 daily인 건 "평범한 일상" 하나뿐이라, 콜백이 한 번 불려 집 이모지와 함께 찍히고 개수 1이 돌아와요. 반면 "sports"는 어느 게시물의 카테고리와도 안 맞아서 if 조건이 계속 거짓이에요. 그래서 onMatch는 한 번도 안 불리고, matched가 0인 채로 돌아오죠.
🏠 평범한 일상
일상 게시물 1개
스포츠 게시물 0개
한 문장 정리 예시: "
if (post.category === category)가 참일 때만 콜백을 부르고 개수를 세는데,sports는 어느 게시물과도 안 맞아 조건이 한 번도 참이 안 돼서 콜백이 안 불리고 개수가 0이 된다."
채점 포인트
| 항목 | 확인 내용 |
|---|---|
| 반환값 받기 | 찾은 개수를 변수에 받아 console.log로 출력했는가 |
| daily 결과 | 일상 게시물 1개가 골라지는지 확인했는가 |
| 빈 결과 이해 | 없는 카테고리는 콜백이 안 불리고 개수가 0이 되는 이유를 조건과 연결했는가 |
흔한 실수
- 반환값을 안 받음 —
filterPosts(...)만 호출하고 반환값을 변수에 안 받으면, 골라진 게시물은 콜백으로 찍히지만 "몇 개"인지를 못 써요. 개수가 필요하면const dailyCount = filterPosts(...)처럼 돌려받은 값을 변수에 담아야 해요. - 없는 카테고리에서 에러가 날 거라 예상 —
"sports"로 불러도 에러는 안 나요. 그냥 조건이 한 번도 안 맞아서 콜백이 안 불릴 뿐이에요. "결과가 0개"와 "에러"는 다른 거예요. 빈 결과는 정상적인 동작이에요.
💭 [생각해볼 주제 예시답안]
1. 왜 변수에 "보이는 범위"를 두었을까?
문제 상황 요약
모든 변수가 어디서나 다 보이면 편하지 않을까요? 그런데 큰 프로그램에서 변수 수백 개가 전부 전역에 있다면요. 스코프가 변수를 함수·블록 안에 가둬주면 무엇이 좋아질까요?
튜터의 가이드 및 해설
스코프의 가치는 "가두는 게 오히려 안전하다" 에서 나와요.
변수가 전부 전역에 있다고 상상해볼게요. 처음엔 편해 보여요. 어디서든 다 보이니까요. 그런데 프로그램이 커지면 문제가 터져요. 예를 들어 내가 count라는 변수를 만들었는데, 다른 사람이 멀리 떨어진 코드에서 같은 이름 count를 또 만들면, 둘이 서로 덮어써요. 내 좋아요 카운트가 갑자기 엉뚱한 값으로 바뀌는데, 어디서 바뀌었는지 추적하기도 어렵죠. 변수 수백 개가 전부 전역이면, 이름 충돌과 의도치 않은 덮어쓰기가 끝없이 생겨요.
스코프는 변수를 자기가 일하는 좁은 범위에 가둬줘요. for 루프의 i는 그 루프 안에서만, 함수 안 변수는 그 함수 안에서만 살죠. 그래서 다른 곳에서 같은 이름을 써도 서로 안 부딪혀요. 각자 자기 방 안에서만 유효하니까요. 변수가 어디서 만들어지고 어디서 사라지는지도 명확해서, 값이 어떻게 바뀌는지 추적하기 쉬워져요.
정리하면 이렇게 갈려요.
- 전부 전역: 처음엔 편하지만, 프로그램이 커지면 이름 충돌·의도치 않은 덮어쓰기·추적 곤란이 생겨요.
- 스코프로 가두기: 변수가 좁은 범위에만 살아서, 같은 이름을 써도 안 부딪히고, 어디서 바뀌는지 명확해요.
- 그래서 보통은: 변수를 가능한 한 좁은 범위에 둬요. 꼭 필요한 곳에서만 보이게 가두는 게, 큰 프로그램에서 실수를 막는 길이거든요.
🎯 면접관을 홀리는 핵심 멘트
"변수가 다 전역이면 편할 것 같지만, 사실은 그 반대예요. 프로그램이 커지면 같은 이름이 멀리서 충돌해 서로 덮어쓰고, 어디서 값이 바뀌었는지 추적이 안 돼요. 그래서 저는 변수를 가능한 한 좁은 스코프에 가둬요. 필요한 곳에서만 보이게 하는 게 오히려 실수를 줄이는 안전장치라고 생각합니다."
2. 클로저로 상태를 숨기는 게 왜 좋을까?
문제 상황 요약
makeLikeToggle에서 liked를 함수 안에 숨겼죠. 그냥 전역 변수 let liked = false;로 두고 써도 좋아요 토글은 동작해요. 그런데도 굳이 함수 안에 숨긴 이유가 뭘까요?
튜터의 가이드 및 해설
클로저로 숨기는 이유는 "아무나 못 건드리게 막는 것" 의 가치예요.
liked를 전역 변수로 두면, 동작은 똑같이 해요. 문제는 그 변수가 누구에게나 열려 있다는 거예요. 프로그램 어디에서든 liked = true라고 한 줄만 적으면 좋아요 상태가 강제로 바뀌어요. 토글 버튼을 누르지도 않았는데 말이죠. 게다가 멀리 떨어진 코드가 실수로 liked라는 같은 이름을 쓰면, 우리 좋아요 상태와 충돌해버려요. 상태가 언제 어떻게 바뀌는지 통제할 수가 없어요.
클로저로 함수 안에 숨기면, liked는 오직 makeLikeToggle이 돌려준 함수를 통해서만 바뀔 수 있어요. 밖에서는 liked라는 변수가 존재하는지조차 몰라요(Step 1에서 배운 대로 함수 안 변수는 밖에서 안 보이니까요). 그래서 좋아요 상태를 바꾸는 유일한 통로가 toggle() 하나로 정해져요. 다른 코드가 실수로든 고의로든 함부로 건드릴 수 없죠. 상태가 안전하게 보호되는 거예요.
두 방식을 비교하면 이래요.
- 전역 변수로 두기: 만들기 간단하지만, 누구나 직접 바꿀 수 있어서 통제가 안 되고 이름 충돌 위험도 있어요.
- 클로저로 숨기기: 정해진 함수(
toggle)를 통해서만 바꿀 수 있어서, 상태가 보호되고 바뀌는 통로가 명확해요. - 그래서 보통은: 함부로 바뀌면 안 되는 중요한 상태는 클로저로 숨겨요. "데이터를 숨기고 정해진 통로로만 바꾸게 한다"는 건 프로그램이 커질수록 더 중요해지는 원칙이에요.
🎯 면접관을 홀리는 핵심 멘트
"전역 변수로 둬도 토글은 동작하지만, 문제는 그 상태를 아무나 직접 바꿀 수 있다는 거예요. 클로저로 함수 안에 숨기면 밖에서는 그 변수가 보이지도 않고, 정해진 함수를 통해서만 바꿀 수 있어요. 중요한 상태일수록 숨기고 통로를 하나로 정해 두는 게 안전하다고 생각합니다."
3. 함수를 값으로 넘길 수 있다는 게 왜 강력할까?
문제 상황 요약
forEachItem은 "배열을 돈다"는 틀만 갖고, 실제 할 일은 콜백으로 받았어요. 만약 콜백 없이 forEachItem 안에 console.log를 직접 박아두었다면, 이 함수는 딱 한 가지 일밖에 못 하겠죠. 콜백으로 "할 일"을 밖에서 정해주면 같은 함수로 몇 가지 일을 할 수 있을까요?
튜터의 가이드 및 해설
콜백의 힘은 "틀과 할 일을 분리한다" 에서 나와요.
forEachItem 안에 console.log("태그: " + tag)를 직접 박아두면, 이 함수는 영영 "태그를 출력하는 함수"로 굳어버려요. 글자 수를 세고 싶으면 새 함수를, 이모지를 붙이고 싶으면 또 새 함수를 만들어야 하죠. 할 일이 늘 때마다 비슷한 순회 함수가 끝없이 복제돼요. 지난 시간에 배운 "복사 붙여넣기의 함정"이 다시 찾아오는 거예요.
콜백으로 받으면 forEachItem은 "배열을 돈다"는 틀만 갖고, 무엇을 할지는 부르는 쪽이 정해요. 출력할 수도, 글자 수를 셀 수도, 이모지를 붙일 수도 있죠. 할 일이 백 가지로 늘어나도 forEachItem은 그대로예요. 콜백만 백 가지로 바꿔 넘기면 되니까요. 하나의 순회 틀로 무한히 많은 일을 할 수 있는 거예요. "어떻게 도는가(틀)"와 "각 요소에 무엇을 하는가(할 일)"를 깔끔하게 떼어낸 덕분이죠.
정리하면 이래요.
- 할 일을 함수 안에 박기: 그 함수는 딱 한 가지 일만 해요. 다른 일을 하려면 비슷한 함수를 또 만들어야 하죠.
- 할 일을 콜백으로 받기: 같은 틀로 무한히 많은 일을 할 수 있어요. 콜백만 바꾸면 되니까요.
- 그래서 보통은: "어떻게 도는가"와 "무엇을 하는가"가 나뉠 수 있으면 콜백으로 분리해요. 그러면 틀은 한 번만 잘 만들어두고, 할 일은 부르는 쪽이 자유롭게 정하거든요. 이 패턴은 다음 시간에 배울 배열 메서드의 바탕이기도 해요.
🎯 면접관을 홀리는 핵심 멘트
"할 일을 함수 안에 박아두면 그 함수는 한 가지 일밖에 못 해요. 콜백으로 받으면 '어떻게 도는가'라는 틀과 '무엇을 하는가'라는 할 일이 분리돼서, 같은 함수로 무한히 많은 일을 할 수 있어요. 틀은 한 번만 잘 만들고 할 일은 부르는 쪽이 정하게 하는 것 — 이게 함수를 값처럼 넘기는 진짜 힘이라고 생각합니다."