9단원 '클로저와 클래스'의 다섯 레슨에서 일곱 문제를 내고, 1~8단원에서 세 문제를 섞었습니다. 마지막 두 문제는 코드를 직접 짜 봅니다.
한 번에 다 풀 필요는 없습니다. 문제 1~10은 폰으로도 풀 수 있고(약 30~40분), 끝의 만들어 보기 둘은 컴퓨터에서 Node로 직접 돌려 봅니다. 이 단원을 건너뛰어도 되는지 보려면 문제 2, 4, 7, 8, 10 다섯 문제만 먼저 풀어 보세요(약 15분). 건너뛸지 볼 때는 힌트를 펴지 않고 풉니다. 다섯 문제를 모두 맞히면 10단원으로 넘어가고, 하나라도 헷갈리면 정답의 '다시 볼 레슨'을 먼저 봅니다. 파이썬을 배웠다면 파이썬과 달라지는 곳이 이 다섯 문제에 들어 있습니다(정답의 '파이썬에서는' 줄). 끝의 만들어 보기 둘은 골라서 풉니다.
끝의 두 문제는 프로그램을 직접 만들어 봅니다. 글만 읽고 넘어가지 말고, 컴퓨터에서 작업 폴더의 main.js에 옮겨 적고 터미널에서 node main.js로 돌려 보세요. 실행 결과는 모두 Node 26으로 돌린 것입니다.
문제 1
대기 번호표를 뽑는 함수를 만들어 두 번 부릅니다. 출력되는 두 줄을 쓰세요.
function makeTicket() {
let num = 0;
return () => {
num += 1;
return num;
};
}
const next = makeTicket();
console.log(next());
console.log(next());
힌트
makeTicket()이 끝난 뒤에도 돌려준 안쪽 함수는 자신이 만들어진 칸의 num 이름표를 기억합니다(규칙 5).
정답
1
2- 8번 줄:
makeTicket()을 부르면 그 호출 칸에num이0으로 만들어지고, 안쪽 함수를 돌려줍니다. - 9번 줄: 첫 번째
next()가 기억해 둔num을0에서1로 바꾸고1을 돌려줍니다. - 10번 줄: 두 번째
next()가 같은 칸의num을1에서2로 바꾸고2를 돌려줍니다.
흔한 오답은 두 줄 모두 1을 쓰는 것입니다. next()를 부를 때마다 let num = 0;이 다시 실행된다고 본 것입니다. let num = 0;은 8번 줄에서 makeTicket()을 부를 때 딱 한 번만 실행됩니다.
다시 볼 레슨: 클로저: 만들어진 칸을 기억하는 함수
문제 2
점수 계산기를 두 개 만들고 bonus를 바꾼 뒤 차례로 부릅니다. 출력되는 세 줄을 쓰세요.
let bonus = 100;
function makeCalc(start) {
let score = start;
return () => {
score += bonus;
return score;
};
}
이어서 bonus를 바꾸고 함수를 부릅니다.
const a = makeCalc(0);
const b = makeCalc(50);
bonus = 200;
console.log(a());
console.log(a());
console.log(b());
힌트
안쪽 함수는 bonus의 옛 값 100을 복사해 두지 않고 전역 칸의 bonus 이름표를 기억합니다. 또한 makeCalc를 두 번 불렀으므로 a와 b가 기억하는 score 칸은 서로 다릅니다.
정답
200
400
250a()가 불릴 때 전역 칸의bonus는 이미200으로 바뀌어 있습니다. 클로저는 함수가 만들어질 때의 값을 복사해 두지 않고 그 칸의 이름을 기억합니다(규칙 5).- 첫 번째
a()는 첫 호출 칸의score를0 + 200으로 바꿔200을 찍고, 두 번째a()는 같은 첫 칸의score를200 + 200으로 바꿔400을 찍습니다. b()는 두 번째makeCalc(50)호출 칸의score인50에200을 더해250을 찍습니다.
흔한 오답은 100, 200, 150을 쓰는 것입니다. makeCalc를 부를 때 bonus가 100이었으니 100이 복사되어 박힌다고 본 것입니다(클로저가 값을 복사해 둔다는 생각).
다시 볼 레슨: 클로저: 만들어진 칸을 기억하는 함수
문제 3
주문 객체를 펼쳐 새 객체를 만드는 함수와 화면에 찍기만 하는 함수를 부릅니다. 출력되는 세 줄을 쓰세요.
function show(order) {
console.log(order.menu);
}
function withCup(order) {
return {
...order,
cup: '머그',
};
}
const base = { menu: '라테' };
const a = show(base);
const b = withCup(base);
console.log(a);
console.log(b.cup, base.cup);
힌트
show에는 return이 없으므로 console.log로 찍기만 하고 undefined를 돌려줍니다. withCup은 ...order로 새 객체를 만들어 돌려주므로 원래 base 객체는 바뀌지 않습니다.
정답
라테
undefined
머그 undefined- 11번 줄:
show(base)가 실행되며base.menu인라테를 먼저 찍습니다.show안에return이 없으므로a에는undefined가 붙습니다. - 12번 줄:
withCup(base)는base의 속성을 펼친 새 객체에cup: '머그'를 더해 돌려줍니다.base자체에는cup속성이 생기지 않습니다. - 13~14번 줄:
a는undefined이고,b.cup은'머그', 없는 속성인base.cup은undefined입니다.
다시 볼 레슨: 돌려받은 값 쓰기 · 단축 속성과 나머지 모으기
문제 4
Box 클래스로 상자를 만들고 값을 바꿉니다. 출력되는 한 줄을 쓰세요.
class Box {
constructor(qty) {
this.qty = qty;
}
}
const a = new Box(2);
const b = new Box(2);
const same = a;
same.qty = 9;
console.log(a.qty, b.qty);
힌트
new Box(2)를 두 번 불렀으므로 a와 b는 서로 다른 두 객체를 가리킵니다. 반면 const same = a;는 a가 가리키는 객체에 이름표만 하나 더 붙입니다(규칙 3).
정답
9 2- 6~7번 줄:
new Box(2)를 부를 때마다 빈 객체가 새로 만들어지므로a와b는 서로 다른 두Box객체를 가리킵니다. - 8~9번 줄:
const same = a;는 객체를 복사하지 않고a와 같은 첫 번째Box객체를 가리킵니다.same.qty = 9;로 바꾸면a.qty도9가 됩니다. - 10번 줄:
b는 따로 만든 두 번째 객체이므로b.qty는2그대로입니다.
파이썬에서는 생성자를 def __init__(self, qty):로 쓰고 Box(2)처럼 new 없이 부르지만 — 자바스크립트는 생성자 이름이 constructor이고 반드시 new Box(2)처럼 new를 붙여 부릅니다.
다시 볼 레슨: 클래스와 new · 객체를 넘기고 복사하기
문제 5
도장 카드 두 장을 만들고 add 메서드를 차례로 부릅니다. 출력되는 두 줄을 쓰세요.
class StampCard {
constructor(owner) {
this.owner = owner;
this.stamps = 0;
}
add(n) {
this.stamps += n;
return this.stamps;
}
}
이어서 카드 두 장을 만들고 add를 부릅니다.
const c1 = new StampCard('민지');
const c2 = new StampCard('준호');
c1.add(3);
c2.add(1);
console.log(c1.add(2));
console.log(c2.stamps);
힌트
메서드 안의 this는 그 메서드를 부를 때 점(.) 앞에 적힌 객체를 가리킵니다. c1.add(...)는 c1의 도장을, c2.add(1)은 c2의 도장을 바꿉니다.
정답
5
1c1.add(3)을 부르면this가c1을 가리켜c1.stamps가3이 됩니다.c2.add(1)을 부르면this가c2를 가리켜c2.stamps가1이 됩니다.c1.add(2)는c1.stamps를3 + 2인5로 바꾸고5를 돌려주어 첫 줄에5가 찍히고, 둘째 줄에는c2.stamps인1이 찍힙니다.
다시 볼 레슨: 메서드와 this
문제 6
할인 금액이 0원인 쿠폰 객체에서 ||와 ??로 기본값을 줍니다. 출력되는 두 줄을 쓰세요.
const c = { cut: 0 };
console.log(c.cut || 500);
console.log(c.cut ?? 500);
console.log(c.tag ?? '기본');
힌트
||는 왼쪽 값이 거짓 같은 값(0, '', false, null, undefined)이면 오른쪽 값을 쓰고, ??는 왼쪽 값이 null이나 undefined일 때만 오른쪽 값을 씁니다.
정답
500
0
기본- 2번 줄:
c.cut은 숫자0입니다. 자바스크립트에서0은 거짓 같은 값이라0 || 500은 오른쪽 값500을 돌려줍니다. - 3번 줄:
??는null과undefined만 비었다고 보므로 숫자0을 그대로 살려0을 찍습니다. - 4번 줄: 없는 속성
c.tag는undefined이므로??가 오른쪽 값'기본'을 돌려줍니다.
다시 볼 레슨: 참 같은 값과 ??
문제 7
다음 코드는 끝까지 도나요, 중간에 멈추나요? 끝까지 돌면 출력되는 줄을, 멈추면 멈추기 전까지 찍힌 줄과 오류 이름(TypeError 같은 이름)을 쓰세요.
class StampCard {
constructor() {
this.stamps = 0;
}
add() {
this.stamps += 1;
}
}
const card = new StampCard();
card.add();
console.log(card.stamps);
const fn = card.add;
fn();
console.log(card.stamps);
힌트
10번 줄의 card.add()는 점 앞에 card가 있지만, 12번 줄에서 함수만 꺼내 담은 fn을 13번 줄에서 fn()처럼 점 없이 부르면 add 안의 this가 무엇이 되는지 보세요.
정답
1을 한 줄 찍은 뒤 13번 줄(add 안 6번 줄)에서 TypeError로 멈춥니다.
1main.js:6 this.stamps += 1; ^ TypeError: Cannot read properties of undefined (reading 'stamps') at add (main.js:6:5) at main.js:13:1- 10~11번 줄:
card.add()는 점 앞에card가 있어this가card에 붙고,1이 찍힙니다. - 12~13번 줄:
const fn = card.add;는 메서드 함수만 꺼내fn에 붙입니다.fn()처럼 점 없이 부르면add호출 칸의this는undefined가 되어undefined.stamps를 읽으려다TypeError로 멈춥니다. 14번 줄은 실행되지 않습니다.
흔한 오답은 1 다음에 2가 찍힌다고 쓰는 것입니다. card.add가 card에서 나왔으니 fn으로 옮겨 담아도 card를 기억한다고 본 것입니다(this가 붙어 있다는 생각).
파이썬에서는 fn = card.add처럼 메서드를 변수에 담아 fn()으로 불러도 self가 card에 묶여 있어 2가 찍히지만 — 자바스크립트의 this는 함수를 부를 때 점 앞에 무엇이 적혔는지로 그때그때 정해져, 점 없이 fn()으로 부르면 this가 undefined가 됩니다.
다시 볼 레슨: this를 잃을 때
문제 8
Counter를 물려받은 StepCounter를 만들고 메서드를 차례로 부릅니다. 출력되는 두 줄을 쓰세요.
class Counter {
constructor(start) {
this.count = start;
}
up() {
this.count += 1;
}
reset() {
this.count = 0;
}
}
이어서 자식 클래스 StepCounter를 만듭니다.
class StepCounter
extends Counter {
constructor(start, step) {
super(start);
this.step = step;
}
up() {
this.count += this.step;
}
}
이어서 인스턴스를 만들고 메서드를 차례로 부릅니다.
const s =
new StepCounter(10, 4);
s.up();
console.log(s.count);
s.reset();
console.log(s.count);
힌트
s.up()은 자식 클래스 StepCounter에서 먼저 찾고, StepCounter에 없는 s.reset()은 부모 클래스 Counter로 올라가 찾습니다.
정답
14
0new StepCounter(10, 4)는super(start)로 부모 생성자를 먼저 실행해this.count를10으로 만들고, 이어서this.step을4로 만듭니다.s.up()은 자식 클래스StepCounter에 다시 정의된up()을 먼저 찾아10 + 4인14를 찍습니다.s.reset()은StepCounter에 없으므로 부모 클래스Counter의reset()을 찾아this.count를0으로 바꿉니다.
파이썬에서는 class StepCounter(Counter):와 super().__init__(start)로 쓰지만 — 자바스크립트는 extends Counter를 붙이고 자식 생성자 안에서 this를 쓰기 전에 반드시 super(start)를 먼저 불러야 합니다.
다시 볼 레슨: 상속: extends와 super
문제 9
도장 카드 객체가 담긴 배열에서 도장이 3개 이상인 손님 이름만 골라 새 배열을 만듭니다. 출력되는 한 줄을 쓰세요.
class Card {
constructor(owner, stamps) {
this.owner = owner;
this.stamps = stamps;
}
}
const list = [
new Card('민지', 4),
new Card('준호', 2),
new Card('서연', 3),
];
const names = list
.filter((c) => c.stamps >= 3)
.map((c) => c.owner);
console.log(names);
힌트
filter로 stamps >= 3인 Card 객체 두 개를 먼저 거른 뒤, map으로 각 객체의 owner 문자열만 꺼내 새 배열로 바꿉니다.
정답
[ '민지', '서연' ]filter((c) => c.stamps >= 3)이 민지(4)와 서연(3)의Card객체만 남긴 새 배열을 돌려줍니다.- 이어서
.map((c) => c.owner)가 각 카드 객체에서 이름만 꺼내[ '민지', '서연' ]배열을 돌려줍니다.
다시 볼 레슨: 클래스와 new · map·filter·find
문제 10
전달받은 함수를 두 번 실행하는 runTwice에 card.add를 넘겨 도장을 두 번 찍으려 합니다. 2 한 줄이 찍혀야 하는데, 아래처럼 6번 줄에서 멈췄습니다. 몇 번 줄의 무엇을 고쳐야 할까요? 한 줄만 고쳐, 고친 줄을 쓰세요.
class StampCard {
constructor() {
this.stamps = 0;
}
add() {
this.stamps += 1;
}
}
이어서 runTwice에 메서드를 넘깁니다.
function runTwice(fn) {
fn();
fn();
}
const card = new StampCard();
runTwice(card.add);
console.log(card.stamps);
main.js:6 this.stamps += 1; ^ TypeError: Cannot read properties of undefined (reading 'stamps') at add (main.js:6:5) at runTwice (main.js:10:3) at main.js:15:1힌트
멈춘 자리는 6번 줄이지만, this가 왜 undefined가 되었는지는 at 줄을 따라 10번 줄(fn())과 15번 줄(runTwice(card.add))을 함께 봐야 합니다. 15번 줄에서 함수를 넘길 때 점(.)을 살려 부르도록 화살표 함수로 감싸 보세요.
정답
15번 줄을 runTwice(() => card.add());로 고칩니다.
class StampCard {
constructor() {
this.stamps = 0;
}
add() {
this.stamps += 1;
}
}이어서 고친 15번 줄을 넣어 실행합니다.
function runTwice(fn) {
fn();
fn();
}
const card = new StampCard();
runTwice(() => card.add());
console.log(card.stamps);2오류는 6번 줄에서 났지만 고칠 곳은 메서드를 떼어 넘긴 15번 줄입니다. runTwice(card.add)처럼 메서드만 떼어 넘기면 10번 줄에서 점 없이 fn()으로 불려 add 안의 this가 undefined가 됩니다. () => card.add()처럼 화살표 함수로 감싸 넘기면 fn()이 불릴 때마다 그 안에서 점과 함께 card.add()를 실행하므로 this가 card에 붙습니다.
- 흔한 오답은 15번 줄을
runTwice(card.add());로 고치는 것입니다. 이렇게 쓰면card.add()를 그 자리에서 한 번 실행해 돌려준 값(undefined)을runTwice에 넘기게 되어, 10번 줄에서fn is not a functionTypeError가 납니다.
파이썬에서는 runTwice(card.add)라고 그대로 넘겨도 card가 묶여 넘어가 잘 돌아가지만 — 자바스크립트에서 메서드를 콜백으로 넘길 때는 () => card.add()처럼 화살표 함수로 감싸 점(.)을 살려 줍니다.
다시 볼 레슨: this를 잃을 때
문제 11 (만들어 보기)
주인 이름 name과 초기 포인트 pt를 저장하는 Card 클래스의 메서드 둘을 채워 봅니다.
earn(n):this.pt를n만큼 올리고 바뀐this.pt를 돌려줍니다.label():'민지: 150'꼴의 문자열을 돌려줍니다.
뼈대의 마지막 두 줄이 이렇게 찍어야 합니다.
150
민지: 150
뼈대 끝의 시험 값을 new Card('준호', 80)과 c.earn(0)으로 바꾸면 이렇게 나와야 합니다.
80
준호: 80
아래 뼈대에서 주석 둘 아래를 채우세요.
class Card {
constructor(name, pt) {
this.name = name;
this.pt = pt;
}
// 1. earn(n) 메서드 쓰기
// 2. label() 메서드 쓰기
}
const c = new Card('민지', 100);
console.log(c.earn(50));
console.log(c.label());
힌트
메서드 안에서 객체의 속성을 읽거나 바꿀 때는 반드시 앞에 this.를 붙여 this.pt, this.name으로 씁니다.
정답
모범 답입니다.
class Card {
constructor(name, pt) {
this.name = name;
this.pt = pt;
}
earn(n) {
this.pt += n;
return this.pt;
}
label() {
return this.name +
': ' + this.pt;
}
}이어서 인스턴스를 만들어 확인합니다.
const c =
new Card('민지', 100);
console.log(c.earn(50));
console.log(c.label());150
민지: 150- 다르게 써도 되는 곳:
label()안에서+연산자로this.name과this.pt를 이어 붙여도 됩니다. - 흔한 오답:
earn(n)에서this.pt += n;만 쓰고return this.pt;를 빠뜨리면 첫 줄에undefined가 찍힙니다.this.pt대신this.없이pt라고만 쓰면 생성자 매개변수pt는 이미 사라진 뒤라ReferenceError로 멈춥니다.
문제 12 (만들어 보기)
이번에는 빈 데서 클로저 함수를 직접 씁니다. 최대 한도 max를 받아 안쪽 함수를 돌려주는 makeLimiter(max)를 만드세요. 돌려받은 함수에 숫자 n을 넣어 부를 때마다 지금까지 누적된 값 used에 n을 더하되, used가 max를 넘으면 max로 맞춘 뒤 used를 돌려줍니다. 이렇게 나와야 합니다.
3
5
5
makeLimiter(5)를 makeLimiter(10)으로 바꾸면 3, 7, 9가 나와야 합니다.
3
7
9
아래 칸의 마지막 네 줄 위쪽에 makeLimiter 함수를 쓰세요.
// 여기에 makeLimiter 함수를 쓰세요
const add = makeLimiter(5);
console.log(add(3));
console.log(add(4));
console.log(add(2));
힌트
makeLimiter(max) 안에 let used = 0;을 만들고, (n) => { ... } 화살표 함수를 돌려줍니다. 안쪽 함수에서 used += n; 뒤에 if (used > max)로 한도를 넘었는지 확인해 used = max;로 맞춘 뒤 return used;를 씁니다.
정답
모범 답입니다.
function makeLimiter(max) {
let used = 0;
return (n) => {
used += n;
if (used > max) {
used = max;
}
return used;
};
}
const add = makeLimiter(5);
console.log(add(3));
console.log(add(4));
console.log(add(2));3
5
5- 다르게 써도 되는 곳:
if (used > max)대신 삼항 연산자를 써도 되고, 누적 변수 이름은total처럼 달라도 됩니다. - 흔한 오답:
let used = 0;을 안쪽 화살표 함수 안에 두면 부를 때마다0으로 다시 시작해3,4,2가 찍힙니다.makeLimiter가 함수를 돌려주지 않고 숫자를 바로 돌려주면add(3)에서add is not a functionTypeError가 납니다.
다시 볼 레슨: 클로저: 만들어진 칸을 기억하는 함수
틀린 문제는 그 레슨으로 돌아가 모션 그림과 '자주 틀리는 곳'을 보고, 하루쯤 지난 뒤 그 문제만 다시 풀어 보세요. 그날 바로 다시 풀면 방금 본 정답이 기억나 맞힌 것인지 알기 어렵습니다.