본문으로 건너뛰기

단원 연습 문제

자바스크립트 목차클로저와 클래스문제 12개

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
250
  • a()가 불릴 때 전역 칸의 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
1
  • c1.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
0
  • new 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 function TypeError가 납니다.

파이썬에서는 runTwice(card.add)라고 그대로 넘겨도 card가 묶여 넘어가 잘 돌아가지만 — 자바스크립트에서 메서드를 콜백으로 넘길 때는 () => card.add()처럼 화살표 함수로 감싸 점(.)을 살려 줍니다.

다시 볼 레슨: this를 잃을 때

문제 11 (만들어 보기)

주인 이름 name과 초기 포인트 pt를 저장하는 Card 클래스의 메서드 둘을 채워 봅니다.

  1. earn(n): this.pt를 n만큼 올리고 바뀐 this.pt를 돌려줍니다.
  2. 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로 멈춥니다.

다시 볼 레슨: 클래스와 new · 메서드와 this

문제 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 function TypeError가 납니다.

다시 볼 레슨: 클로저: 만들어진 칸을 기억하는 함수


틀린 문제는 그 레슨으로 돌아가 모션 그림과 '자주 틀리는 곳'을 보고, 하루쯤 지난 뒤 그 문제만 다시 풀어 보세요. 그날 바로 다시 풀면 방금 본 정답이 기억나 맞힌 것인지 알기 어렵습니다.

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

자바스크립트 목차로