12단원 '웹 페이지 움직이기'의 아홉 레슨에서 아홉 문제를 내고, 1~11단원에서 세 문제를 섞었습니다. 마지막 두 문제는 코드를 직접 짜 봅니다.
한 번에 다 풀 필요는 없습니다. 문제 1~10은 폰으로도 풀 수 있고(약 30~40분), 끝의 만들어 보기 둘은 컴퓨터에서 Node로 직접 돌려 봅니다. 이 단원을 건너뛰어도 되는지 보려면 문제 2, 4, 6, 8, 10 다섯 문제만 먼저 풀어 보세요(약 15분). 건너뛸지 볼 때는 힌트를 펴지 않고 풉니다. 다섯 문제를 모두 맞히면 13단원으로 넘어가고, 하나라도 헷갈리면 정답의 '다시 볼 레슨'을 먼저 봅니다. 파이썬을 배웠다면 파이썬과 달라지는 곳이 이 다섯 문제에 들어 있습니다(정답의 '파이썬에서는' 줄). 끝의 만들어 보기 둘은 골라서 풉니다.
끝의 두 문제는 프로그램을 직접 만들어 봅니다. 글만 읽고 넘어가지 말고, 컴퓨터에서 작업 폴더의 main.js에 옮겨 적고 터미널에서 node main.js로 돌려 보세요. 실행 결과는 모두 Node 26으로 돌린 것입니다.
문제 1
할 일 번호 id와 필터 이름 f로 선택자 문자열 세 개를 만듭니다. 출력되는 세 줄을 쓰세요.
const id = 3;
const f = 'done';
console.log('#count');
console.log(`.todo.${f}`);
console.log(
`[data-id="${id}"]`,
);
힌트
id 선택자는 #, class 선택자는 ., 속성 선택자는 [속성="값"] 모양입니다.
정답
#count
.todo.done
[data-id="3"]- 3번 줄:
id="count"를 찾는'#count'가 찍힙니다. - 4번 줄:
todo와done두 클래스가 함께 붙은 요소를 찾는'.todo.done'이 찍힙니다. - 5~7번 줄:
data-id="3"속성을 찾는'[data-id="3"]'이 찍힙니다.
다시 볼 레슨: HTML을 처음 본다면
문제 2
선택자에서 #을 빼먹어 요소를 찾지 못하고 null을 받은 상황을 흉내 낸 코드입니다. 몇 번 줄에서 어떤 이름의 오류가 나는지 쓰세요.
function find(sel) {
if (sel === '#count') {
return {
textContent: '3개',
};
}
return null;
}
const el = find('count');
console.log(el);
el.textContent = '2개';
힌트
9번 줄의 find('count')는 오류 없이 null을 돌려주고, null에 점(.)을 찍어 속성을 고치는 줄에서 오류가 납니다.
정답
11번 줄에서 TypeError가 납니다.
nullmain.js:11el.textContent = '2개'; ^ TypeError: Cannot set properties of null (setting 'textContent') at main.js:11:16- 9~10번 줄:
'#count'대신'count'를 넘겨 요소를 찾지 못했으므로el에null이 들어가고 첫 줄에null이 찍힙니다. - 11번 줄:
null에.textContent를 대입하려다TypeError가 납니다. 오류는 11번 줄에서 나지만 고쳐야 할 곳은 9번 줄의 선택자('#count')입니다.
파이썬에서는 없는 키를 d['count']로 찾으면 찾는 그 줄에서 바로 KeyError가 나지만 — 브라우저의 querySelector는 못 찾으면 조용히 null을 돌려주고 나중에 null을 쓰는 아랫줄에서 TypeError가 납니다.
다시 볼 레슨: 요소 찾기: querySelector
문제 3
브라우저 전용 객체인 document를 Node(node main.js)에서 바로 읽으려고 합니다. 어떤 이름의 오류가 나는지 쓰세요.
console.log(document);
힌트
Node 환경에는 브라우저의 문서 나무 객체인 document가 정의되어 있지 않습니다.
정답
ReferenceError가 납니다.
main.js:1console.log(document); ^ ReferenceError: document is not defined at main.js:1:13document는 웹 브라우저가 만들어 주는 객체이므로 터미널에서node로 실행하면ReferenceError(document is not defined)가 납니다.
다시 볼 레슨: 웹 페이지에서 돌리기
문제 4
상태 변수 left를 바꾼 뒤 el.textContent를 찍어 봅니다. 출력되는 두 줄을 순서대로 쓰세요.
let left = 3;
const el = { textContent: '' };
el.textContent =
`남음 ${left}개`;
left = 1;
console.log(el.textContent);
el.textContent =
`남음 ${left}개`;
console.log(el.textContent);
힌트
3~4번 줄에서 대입한 문자열은 left = 1로 변수를 바꾼다고 해서 저절로 바뀌지 않습니다(규칙 2).
정답
남음 3개
남음 1개- 3~4번 줄: 그 시점의
left값인3으로 계산된'남음 3개'가el.textContent에 들어갑니다. - 5~6번 줄:
left를1로 바꿨지만el.textContent에 다시 대입하지 않았으므로 첫 줄에는'남음 3개'가 그대로 찍힙니다. - 7~9번 줄: 다시 대입한 뒤에는
'남음 1개'가 찍힙니다.
파이썬과 마찬가지로 — 자바스크립트에서도 화면 요소의 속성에 한 번 대입한 문자열은 나중에 변수를 바꾼다고 저절로 갱신되지 않으므로 직접 다시 대입해야 합니다.
다시 볼 레슨: 글자와 클래스 바꾸기
문제 5
목록을 비우지 않는 addAll()과 비우고 그리는 render()를 차례로 부릅니다. 출력되는 두 줄을 쓰세요.
const todos = ['우유', '산책'];
let dom = [];
function addAll() {
for (const t of todos) {
dom.push(t);
}
}
function render() {
dom = [];
for (const t of todos) {
dom.push(t);
}
}
이어서 두 함수를 부르고 길이를 찍습니다.
addAll();
addAll();
console.log(dom.length);
render();
console.log(dom.length);
힌트
addAll()을 두 번 부르면 비우지 않고 두 번 덧붙여지고, render()는 비운 뒤 다시 채웁니다.
정답
4
2- 비우는 단계가 없는
addAll()을 두 번 불러 2개 뒤에 2개가 더 붙어4가 찍힙니다. render()는 먼저 비운 뒤todos의 2개만 다시 넣으므로2가 찍힙니다.
다시 볼 레슨: 요소 만들어 붙이기 · for...of로 배열 돌기
문제 6
클릭할 때마다 '클릭!'이 찍히도록 onClick에 handle 함수를 등록하려 했는데, 아래 코드는 등록할 때만 한 번 찍히고 정작 click()을 부를 때는 TypeError가 납니다. 11번 줄을 어떻게 고쳐야 할까요? 한 줄만 고쳐, 고친 줄을 쓰세요.
let savedFn = null;
function onClick(fn) {
savedFn = fn;
}
function click() {
savedFn();
}
function handle() {
console.log('클릭!');
}
onClick(handle());
click();
힌트
11번 줄에서 handle() 뒤에 붙은 괄호 () 때문에 함수가 아니라 반환값 undefined가 넘어갔습니다.
정답
11번 줄을 onClick(handle);로 고칩니다.
let savedFn = null;
function onClick(fn) {
savedFn = fn;
}
function click() {
savedFn();
}
function handle() {
console.log('클릭!');
}
onClick(handle);
click();클릭!onClick(handle())처럼 괄호를 붙이면 등록하는 줄에서handle이 바로 실행되고 반환값undefined가savedFn에 저장됩니다.- 괄호를 뺀
onClick(handle);로 적어야 함수 자체가 전달되어click()을 부를 때 실행됩니다.
파이썬에서도 콜백을 넘길 때 괄호를 빼고 함수 이름을 넘기듯 — 브라우저의 addEventListener에서도 반드시 괄호 없이 함수 자체를 넘겨야 클릭할 때 실행됩니다.
다시 볼 레슨: 이벤트와 리스너
문제 7
수량 입력 칸(qtyIn)과 가격 입력 칸(priceIn)에서 읽은 .value를 다루는 코드입니다. 출력되는 세 줄을 쓰세요.
const qtyIn = { value: ' 2 ' };
const priceIn = {
value: '1500',
};
const qRaw = qtyIn.value.trim();
console.log(
qRaw + priceIn.value,
);
const total =
Number(qRaw) *
Number(priceIn.value);
console.log(total);
console.log(
' '.trim() === '',
);
힌트
input.value는 늘 문자열이므로 Number()로 바꾸기 전의 +는 문자열 이어 붙이기가 됩니다.
정답
21500
3000
true- 문자열
'2'와'1500'을+로 이어 붙여'21500'이 찍힙니다. Number()로 바꾼 숫자2 * 1500을 계산해3000이 찍힙니다.- 공백만 있는 문자열을
.trim()하면 빈 문자열''가 되어true가 찍힙니다.
다시 볼 레슨: 폼 제출과 입력 값 · 글자를 수로: Number와 NaN
문제 8
눌린 줄의 li.dataset.id가 '2'일 때 id가 2인 할 일을 지우려 했는데, 아래 코드는 아무것도 지우지 못해 남은 개수가 3으로 나옵니다. 9번 줄을 어떻게 고쳐야 2번 할 일이 지워져 2가 찍힐까요? 한 줄만 고쳐, 고친 줄을 쓰세요.
let todos = [
{ id: 1, text: '우유' },
{ id: 2, text: '복습' },
{ id: 3, text: '산책' },
];
const li = {
dataset: { id: '2' },
};
const id = li.dataset.id;
todos = todos.filter(
(t) => t.id !== id,
);
console.log(todos.length);
3힌트
li.dataset.id는 문자열 '2'이고 t.id는 숫자 2입니다. 9번 줄에서 문자열을 숫자로 바꿔 보세요.
정답
9번 줄을 Number(li.dataset.id)로 바꾸어 id에 대입합니다.
let todos = [
{ id: 1, text: '우유' },
{ id: 2, text: '복습' },
{ id: 3, text: '산책' },
];
const li = {
dataset: { id: '2' },
};
const id = Number(
li.dataset.id,
);
todos = todos.filter(
(t) => t.id !== id,
);
console.log(todos.length);2li.dataset.id는 늘 문자열'2'이므로 변환 없이 비교하면2 !== '2'가 항상true가 되어 아무 줄도 지워지지 않습니다.Number(li.dataset.id)로 숫자2로 바꿔야t.id !== id에서 2번 할 일이 걸러져 남은 개수2가 찍힙니다.
파이썬과 마찬가지로 자바스크립트의 !==도 숫자와 문자열을 서로 다른 값으로 보며 — 특히 HTML의 dataset 속성은 늘 문자열로 읽히므로 숫자 id와 비교하기 전에 반드시 Number()로 바꿔야 합니다.
다시 볼 레슨: 이벤트 위임과 data 속성 · map·filter·find
문제 9
localStorage에 처음 방문했을 때(null)와 JSON 문자열이 저장되어 있을 때를 처리하는 코드입니다. 출력되는 두 줄을 쓰세요.
function load(raw) {
const arr = JSON.parse(
raw ?? '[]',
);
return arr.length;
}
const saved = JSON.stringify([
{ id: 1, text: '우유' },
{ id: 2, text: '산책' },
]);
console.log(load(null));
console.log(load(saved));
힌트
raw가 null이면 raw ?? '[]'는 '[]'가 되어 빈 배열이 만들어집니다.
정답
0
2load(null)은JSON.parse('[]')를 실행해 빈 배열[]의 길이인0을 돌려줍니다.load(saved)는 항목 두 개가 든 배열을 복원해 길이2를 돌려줍니다.
다시 볼 레슨: 새로고침해도 남게: localStorage
문제 10
할 일 줄의 클래스 집합을 다루는 toggle(name, force) 함수를 실행합니다. 출력되는 두 줄을 쓰세요.
const cls = new Set(['todo']);
function toggle(name, force) {
if (force) cls.add(name);
else cls.delete(name);
}
toggle('done', true);
console.log([...cls].join(' '));
toggle('done', false);
console.log([...cls].join(' '));
힌트
첫 번째 호출은 'done'을 추가하고, 두 번째 호출은 'done'을 똅니다.
정답
todo done
todo- 6~7번 줄:
'done'클래스가 추가되어'todo done'이 찍힙니다. - 8~9번 줄:
'done'클래스가 제거되어'todo'만 찍힙니다.
파이썬에 없는 브라우저 DOM의 classList.toggle('done', 불리언)은 — 두 번째 인자가 참이면 클래스를 붙이고 거짓이면 떼어 CSS 스타일(.done .text)을 켜고 끕니다.
다시 볼 레슨: 글자와 클래스 바꾸기
문제 11 (만들어 보기)
할 일 배열 todos와 눌린 줄 요소 li({ dataset: { id: '...' } })를 받아, 그 줄과 같은 숫자 id를 가진 항목을 지운 새 배열을 돌려주는 함수 dropRow(todos, li)의 몸통을 채우세요. 이렇게 나와야 합니다.
2
우유, 산책
시험 값의 li.dataset.id를 '2'에서 '1'로 바꾸면 이렇게 나와야 합니다.
2
복습, 산책
아래 뼈대에서 주석 둘 아래를 채우세요.
function dropRow(todos, li) {
// 1. dataset.id를 숫자로
// 2. 다른 id만 filter로
}
이어서 시험 배열과 li 객체를 넣어 확인합니다.
const list = [
{ id: 1, text: '우유' },
{ id: 2, text: '복습' },
{ id: 3, text: '산책' },
];
const row = {
dataset: { id: '2' },
};
const res = dropRow(list, row);
console.log(res.length);
console.log(
res
.map((t) => t.text)
.join(', '),
);
힌트
1번 주석 아래에 Number(li.dataset.id)를 상수 id에 담고, 2번 주석 아래에 todos.filter로 t.id !== id인 항목만 돌려줍니다.
정답
모범 답입니다.
function dropRow(todos, li) {
const id = Number(
li.dataset.id,
);
return todos.filter(
(t) => t.id !== id,
);
}이어서 시험 배열과 li 객체를 넣어 확인합니다.
const list = [
{ id: 1, text: '우유' },
{ id: 2, text: '복습' },
{ id: 3, text: '산책' },
];
const row = {
dataset: { id: '2' },
};
const res =
dropRow(list, row);
console.log(res.length);console.log(
res
.map((t) => t.text)
.join(', '),
);2
우유, 산책다시 볼 레슨: 이벤트 위임과 data 속성 · map·filter·find
문제 12 (만들어 보기)
이번에는 빈 데서 함수를 직접 씁니다. localStorage.getItem이 돌려준 값 raw(null이거나 JSON 문자열, 또는 깨진 문자열)를 받아 안전하게 배열로 복원해 돌려주는 함수 safeLoad(raw)를 작성하세요(null이거나 JSON 문법 오류가 나면 빈 배열 []을 돌려줍니다). 이렇게 나와야 합니다.
0
1
0
두 번째 시험 값을 '[{"id":1},{"id":2}]'로 바꾸면 이렇게 나와야 합니다.
0
2
0
아래 칸의 위쪽에 safeLoad 함수를 쓰세요.
// safeLoad 함수 쓰기
console.log(
safeLoad(null).length,
);
console.log(
safeLoad('[1]').length,
);
console.log(
safeLoad('{bad').length,
);
힌트
try 안에서 JSON.parse(raw ?? '[]')를 돌려주고 catch 안에서 return [];를 돌려줍니다.
정답
모범 답입니다.
function safeLoad(raw) {
try {
return JSON.parse(
raw ?? '[]',
);
} catch {
return [];
}
}이어서 세 가지 입력을 넣어 확인합니다.
console.log(
safeLoad(null).length,
);
console.log(
safeLoad('[1]').length,
);
console.log(
safeLoad('{bad').length,
);0
1
0다시 볼 레슨: 새로고침해도 남게: localStorage · JSON: 객체와 글 사이
틀린 문제는 그 레슨으로 돌아가 모션 그림과 '자주 틀리는 곳'을 보고, 하루쯤 지난 뒤 그 문제만 다시 풀어 보세요. 그날 바로 다시 풀면 방금 본 정답이 기억나 맞힌 것인지 알기 어렵습니다.