Step 72. JavaScript 기초 — 웹페이지를 살아 있게 하는 언어
Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간
전제: Step 71을 마쳤다. HTML 태그로 페이지를 만들 수 있고, 개발자 도구(F12)의 Elements 탭을 열 수 있다. 파이썬으로 변수와 함수를 다뤄 봤다.
- 준비물: Step 71에서 만든 page.html, 브라우저. 그리고 오늘의 실험대인 Node.js(JS를 브라우저 밖에서 실행해 주는 도구) —
node --version을 쳐서 버전이 나오면 준비된 것입니다 (이 책의 실측은 v24에서 했습니다). 없어도 브라우저의 Console 탭으로 같은 실험을 할 수 있습니다. - 주의: 오늘 실습은 100% 안전합니다. 모든 스크립트는 여러분이 만든 파일과 여러분의 터미널에서만 실행됩니다.
지난 시간에 만든 페이지는 예쁘지만 조용합니다. 버튼을 눌러도, 글자를 쳐도 반응이 없습니다. HTML과 CSS만으로는 "정적인 문서"가 한계이기 때문입니다. 여기에 생명을 불어넣는 것이 JavaScript(자바스크립트, JS)입니다. 보안 학습자에게 JS가 필수인 이유를 미리 말하면 — 웹 해킹의 절반이 "피해자의 브라우저에서 내가 쓴 JS를 실행시키는 기술"이기 때문입니다. 그 기술의 이름이 XSS입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 파이썬과 비교해 JS 기초 문법(let/const, function, console.log)을 쓴다
- HTML 문서 안에
<script>를 심어 페이지를 "살아 있게" 만든다 document.getElementById로 화면 요소를 찾아 읽고 바꾸는 DOM 조작을 한다addEventListener로 클릭 같은 사건에 반응하는 코드를 예약한다alert("hi")한 줄이 왜 XSS 검증의 출발점인지 설명한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | JavaScript — 브라우저(Console 탭)와 Node.js 두 곳에서 실행 |
| 오늘의 도구 | 브라우저 개발자 도구의 Console 탭 (JS 대화형 창), node 파일명.js (터미널 실행) |
| 오늘의 문법 | let/const, function, console.log, 백틱 문자열(`값은 ${x}`), JSON.stringify/parse |
| 오늘의 DOM 도구 | document.getElementById, .textContent, .value, addEventListener |
| 오늘의 산출물 | js_dom.html — DOM 조작 페이지, js_event.html — 클릭 카운터 |
2-1. 파이썬과 비교하는 JS 기초 문법
파이썬을 아는 여러분을 위해 나란히 비교합니다.
| 개념 | 파이썬 | JavaScript |
|---|---|---|
| 변수 | x = 5 |
let x = 5; |
| 상수 | (관습적 대문자) | const PI = 3.14; |
| 함수 | def add(a, b): |
function add(a, b) { return a+b; } |
| 출력 | print(x) |
console.log(x); |
| 문자열 이어붙이기 | f"값은 {x}" |
`값은 ${x}` |
차이는 세 가지입니다. 문장 끝에 세미콜론(;)을 찍고, 코드 덩어리를 들여쓰기 대신 중괄호({ })로 감싸고, 변수 앞에 let이나 const를 붙입니다. let은 바뀔 수 있는 변수, const는 한 번 정하면 못 바꾸는 상수입니다.
2-2. script 태그 — 페이지 안의 엔진실
JS는 <script> 태그 안에 씁니다. 이 태그는 어디에 둬도 되지만, 초보 시절에는 </body> 바로 앞이 정석입니다. 이유는 실행 순서입니다. 브라우저는 문서를 위에서 아래로 읽는데, script가 요소보다 먼저 실행되면 "아직 만들어지지 않은 요소"를 찾으려다 실패합니다. 몸통이 다 만들어진 뒤에 엔진을 켜는 것, 그것이 script를 아래에 두는 이유입니다.
2-3. DOM 조작과 이벤트
JS의 핵심 동작은 두 가지입니다.
- DOM 조작:
document.getElementById("msg")는 "id가 msg인 요소를 찾아와"라는 명령입니다. 찾아온 요소의.textContent(글자 내용)를 바꾸면 화면이 바뀝니다. 지난 시간 개발자 도구에서 손으로 하던 일을 코드로 하는 것입니다. - 이벤트(event, 사건): 클릭, 키 입력 같은 "사건"이 일어났을 때 실행할 함수를 미리 등록해 둡니다. "버튼이 클릭되면 이 함수를 실행해 줘"라는 예약입니다. 이 덕분에 페이지가 사용자의 행동에 반응합니다.
2-4. JSON — 데이터의 공용어
JS를 하다 보면 곧 JSON(JavaScript Object Notation, 제이슨)이라는 형식을 만납니다. 데이터를 주고받을 때 쓰는 문자열 형식으로, 생김새가 JS의 객체(이름표 붙은 값들의 묶음)와 같습니다. 서버가 돌려주는 데이터의 대부분이 이 형식이라, 오늘 익숙해 두면 통신을 배울 때 곧바로 써먹습니다.
3. 따라 하기
3-1. Node로 JS 맛보기 — 브라우저 없는 실험대
브라우저 없이 터미널에서 JS를 실행해 봅니다. hello.js를 만듭니다.
let x = 5;
function add(a, b) { return a + b; }
console.log(add(x, 10));
let n = 1;
console.log(`클릭 횟수: ${n}`);
입력
node hello.js
출력 (2026-09-09 실측, Node v24):
15
클릭 횟수: 1
읽는 법: 변수를 만들고 함수를 정의해 호출했더니 15가 돌아왔고, 백틱 문자열 안의 ${n}이 1로 치환됐습니다. 파이썬에서 하던 것과 같은 흐름입니다. console.log는 파이썬의 print에 해당합니다.
왜: 문법 실험은 브라우저보다 터미널이 빠릅니다. 브라우저가 없어도 되는 이유는, JS라는 언어 자체와 "브라우저가 제공하는 창구(document 등)"가 분리되어 있기 때문입니다.
예측:
console.log("1" + 1)은 무엇일까요? 2일까요, 11일까요? (실측 답:11— 문자열과 숫자의 +는 이어붙이기입니다.Number("1") + 1은2입니다. 2026-09-09 실측)
3-2. JSON — 포장하고 풀기
이어서 json_test.js를 만듭니다.
let user = { name: "해커지망생", level: 1 };
console.log(user.name);
let text = JSON.stringify(user);
console.log(text);
let back = JSON.parse(text);
console.log(back.level);
출력 (2026-09-09 실측):
해커지망생
{"name":"해커지망생","level":1}
1
읽는 법: JSON.stringify는 객체를 문자열로 포장하고, JSON.parse는 문자열을 다시 객체로 풀어헤칩니다. 포장 → 전송 → 풀기, 이것이 웹에서 데이터가 오가는 표준 절차입니다.
왜: 브라우저와 서버가 대화할 때 데이터는 "문자열"로만 오갈 수 있습니다. 그래서 구조를 문자열로 포장하는 이 형식이 필요합니다.
3-3. document는 어디에 사는가 — 영역 확인 실험
실험입니다. Node에서 document를 불러 보세요.
입력
node -e "console.log(document)"
출력 (2026-09-09 실측):
ReferenceError: document is not defined
읽는 법: "그런 이름 없음" 오류입니다. document는 JS 언어의 일부가 아니라 브라우저가 제공하는 창구입니다. 그래서 DOM 조작 코드는 브라우저(또는 브라우저 Console 탭)에서만 동작합니다. 반대로 지금까지의 문법 실험은 어디서든 됩니다.
왜: "언어의 기능"과 "브라우저가 빌려주는 기능"의 경계를 아는 것이 JS 오류를 읽는 첫걸음입니다. 이 오류가 뜨면 "브라우저 밖에서 브라우저 물건을 쓰려 했다"로 해석하세요.
3-4. 첫 스크립트 — alert 한 줄의 위력
이제 브라우저로 갑니다. Step 71의 page.html을 복사해 js_alert.html을 만들고, </body> 바로 앞에 한 줄을 넣습니다.
<script>
alert("hi");
</script>
입력: 저장 → 브라우저로 열기.
화면 예시: 페이지가 열리며 "hi"라는 경고 상자가 툭 뜹니다. 확인을 눌러야 페이지를 만질 수 있습니다.
읽는 법: alert는 브라우저에 경고 상자를 띄우는 함수입니다. 별거 아닌 한 줄이지만, 이것이 "웹페이지 안에서 JS가 실행된다"는 증거이자, XSS 취약점을 검증할 때 전 세계 해커들이 쓰는 바로 그 한 줄입니다.
왜: "게시판에 이 한 줄을 심었는데 다른 사람 화면에서 경고 상자가 떴다"면 그 게시판은 XSS에 뚫린 것입니다. 오늘은 자기 파일에만 심었지만, 이 한 줄의 의미를 기억하세요.
3-5. DOM 조작 — 코드로 화면 바꾸기
js_dom.html을 만듭니다.
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>DOM 조작</title></head>
<body>
<p id="msg">원래 글자</p>
<script>
let box = document.getElementById("msg");
console.log("찾은 내용:", box.textContent);
box.textContent = "바뀐 글자!";
box.style.color = "red";
</script>
</body>
</html>
입력: 저장 → 열기 → F12의 Console 탭도 함께 봅니다.
화면 예시: 화면에는 빨간색 "바뀐 글자!"가 보이고, Console에는 찾은 내용: 원래 글자가 찍힙니다. 브라우저에서 console.log의 출력은 화면이 아니라 이 Console 탭으로 갑니다.
읽는 법: ① getElementById로 id="msg"인 요소를 찾아 box에 담았고 ② 내용을 읽어 Console에 찍었고 ③ textContent를 바꿔 화면 글자를 교체했고 ④ style.color로 색까지 바꿨습니다. 지난 시간 마우스로 하던 수정을 코드 네 줄로 자동화한 것입니다.
왜: "요소 찾기 → 읽기 → 바꾸기" 이 삼단이 DOM 조작의 전부입니다. 모든 화려한 웹 앱도 결국 이 삼단의 반복입니다.
3-6. 이벤트 — 클릭에 반응하는 버튼
js_event.html을 만듭니다.
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>클릭 카운터</title></head>
<body>
<button id="btn">눌러 보세요</button>
<p id="count">클릭 횟수: 0</p>
<script>
let n = 0;
let btn = document.getElementById("btn");
btn.addEventListener("click", function() {
n = n + 1;
document.getElementById("count").textContent = `클릭 횟수: ${n}`;
});
</script>
</body>
</html>
입력: 저장 → 열기 → 버튼을 여러 번 클릭.
화면 예시: 클릭할 때마다 숫자가 1씩 올라갑니다.
읽는 법: addEventListener("click", 함수)는 "클릭 사건이 일어나면 이 함수를 실행해 줘"라는 예약입니다. 이렇게 사건이 올 때까지 기다렸다가 실행되는 함수를 콜백(callback, 나중에 불러 달라는 함수)이라 부릅니다.
왜: 정적 문서가 "상태(변수 n)를 기억하고 행동에 반응하는 프로그램"이 되는 순간입니다. 여러분은 방금 첫 인터랙티브 앱을 완성했습니다.
3-7. 예측해 보기 — script를 위에 두면
실험 전 예측입니다. 방금 코드에서 <script> 덩어리를 잘라내어 버튼보다 먼저 나오게 옮기면 어떻게 될까요? ① 정상 동작 ② 버튼이 안 눌림 ③ 페이지가 안 열림.
직접 확인: script를 위로 옮기고 저장 → 열면 페이지는 열리는데 버튼이 안 눌립니다. Console 탭에는 빨간 오류가 떠 있습니다:
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
이 메시지의 구조는 Node에서도 동일하게 재현됩니다 (2026-09-09 실측):
node -e "let btn = null; btn.addEventListener('click', function(){});"
TypeError: Cannot read properties of null (reading 'addEventListener')
읽는 법: script가 실행된 시점에 button 요소가 아직 만들어지지 않아서, getElementById가 "없음(null)"을 돌려줬고, null에 addEventListener를 부르려다 쓰러진 것입니다. "null의 무엇을 읽을 수 없다"는 이 오류 문장은 JS에서 가장 자주 만나는 얼굴입니다.
왜: 실행 순서 하나가 프로그램 전체를 좌우한다는 것, 그리고 JS 오류는 반드시 Console 탭에 남는다는 것을 배우는 실험입니다. JS가 이상할 때 제일 먼저 볼 곳이 Console이라는 습관을 들이세요.
4. 미션과 연습문제
미션 — 인사 기계 미니 앱
greet.html을 만들어 다음 요구사항을 채우세요.
- 이름을 입력하는
<input id="name">과 "인사" 버튼, 결과를 보여 줄<p id="out">을 배치합니다 - 버튼을 누르면 입력창의 값을 읽어(
입력창변수.value) "○○님, 반갑습니다!"를 out에 표시합니다 - 입력창이 비어 있으면 "이름을 입력해 주세요"를 빨간색으로 표시합니다 (
if (조건) { ... } else { ... }— 파이썬과 비슷합니다) - 인사할 때마다 Console에 "인사함: ○○"을 기록합니다
- 마지막으로, 자신의 페이지에서 입력값이 "화면에 그대로 반영"된다는 사실이 왜 보안상 민감한지 한 문장으로 노트에 적습니다 (힌트: 만약 입력에 HTML 태그를 넣을 수 있다면?)
연습문제
문제 1. let과 const의 차이를 말하고, const x = 1; x = 2;를 실행하면 어떤 일이 생기는지 말해 보세요.
문제 2. 입력창의 값을 읽을 때 .value를 쓰고 문단의 글자를 읽을 때 .textContent를 씁니다. 이 둘의 차이는 무엇인가요?
문제 3. 3-1에서 "1" + 1이 11이 된 이유와, 입력창에서 읽은 숫자 두 개를 더하려면 왜 Number()가 필요한지 설명해 보세요.
문제 4. Uncaught TypeError: Cannot read properties of null (reading 'addEventListener') 오류가 떴습니다. 어떤 상황이며, 고치는 방법 두 가지를 말해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>인사 기계</title></head>
<body>
<input id="name" placeholder="이름을 입력하세요">
<button id="greet">인사</button>
<p id="out"></p>
<script>
let btn = document.getElementById("greet");
btn.addEventListener("click", function() {
let name = document.getElementById("name").value;
let out = document.getElementById("out");
if (name === "") {
out.textContent = "이름을 입력해 주세요";
out.style.color = "red";
} else {
out.textContent = `${name}님, 반갑습니다!`;
out.style.color = "black";
console.log(`인사함: ${name}`);
}
});
</script>
</body>
</html>
검증하는 법: ① 빈 입력으로 인사를 누르면 빨간 안내가 뜨는가. ② 이름을 넣으면 검은 인사말로 바뀌는가. ③ Console에 기록이 남는가. 셋이 전부 ‘예’이면 완성입니다.
5번 문장의 모범 답 예시: "입력값이 화면에 그대로 반영된다면, 사용자가 태그나 스크립트를 입력했을 때 그것도 반영되어 내 페이지에서 남의 코드가 실행될 수 있다." — 이 문장이 쓰였다면 여러분은 벌써 XSS의 서문에 도착한 것입니다.
연습문제 해답
문제 1 해답. let은 다시 값을 넣을 수 있는 변수, const는 한 번 정하면 바꿀 수 없는 상수입니다. const에 다시 대입하면 TypeError: Assignment to constant variable. 오류가 납니다 (2026-09-09 Node에서 실측).
문제 2 해답. textContent는 "태그와 태그 사이에 끼인 글자"를 읽고, value는 "입력창 안에 사용자가 친 값"을 읽습니다. input처럼 열고 닫는 사이가 없는 태그에는 textContent가 없으므로 value를 씁니다.
문제 3 해답. JS에서 문자열과 숫자의 +는 더하기가 아니라 이어붙이기이기 때문입니다. 입력창의 .value는 숫자를 쳐도 항상 문자열로 돌아오므로, Number(값)으로 변환해야 진짜 덧셈이 됩니다. 파이썬의 int()와 같은 발상입니다.
문제 4 해답. getElementById가 요소를 못 찾아 null을 돌려줬는데, 그 null에 addEventListener를 불렀다는 뜻입니다. 고치는 법: ① script를 </body> 직전으로 옮겨 요소가 먼저 만들어지게 하거나, ② getElementById에 적은 id와 실제 태그의 id가 (대소문자까지) 같은지 확인합니다.
완료 기준 체크리스트
- [ ] let과 const로 변수를 만들고 function으로 함수를 정의할 수 있다
- [ ] node 또는 Console 탭에서 JS를 실행하고 console.log를 읽을 수 있다
- [ ] getElementById로 요소를 찾아 textContent를 읽고 바꿀 수 있다
- [ ] input의 value와 textContent의 차이를 안다
- [ ] addEventListener로 클릭 이벤트에 반응하게 만들 수 있다
- [ ] JSON.stringify/parse로 객체를 문자열과 오가게 할 수 있다
- [ ] 미션: 인사 기계를 완성했다
6. 흔한 실수와 해결
벽 1. 버튼을 눌러도 아무 반응이 없다
증상 (2026-09-09 Node에서 재현 실측):
TypeError: Cannot read properties of null (reading 'addEventListener')
원인: 열에 아홉은 script가 요소보다 먼저 실행됐거나, getElementById에 적은 id와 실제 id가 다릅니다 (대소문자 주의).
해결: F12 → Console 탭에 빨간 오류가 있는지 보세요. 오류가 어느 줄에서 실패했는지 알려 줍니다. script는 </body> 직전에 두세요.
벽 2. 입력창의 값이 undefined로 나온다
증상: 읽어 온 값이 입력한 글자 대신 undefined(정의되지 않음)라는 글자입니다.
원인: 요소는 찾았지만 .value를 빼먹었거나, input이 아닌 다른 요소를 찾았습니다. JS에서 "값이 정해지지 않은 변수"는 오류 없이 undefined라는 값이 됩니다 (2026-09-09 실측: let nothing;을 찍으면 undefined).
해결: input의 값은 요소.value로 읽습니다. 태그 사이 글자는 textContent, 입력창 속 값은 value입니다.
벽 3. 더하기가 이상하다 (1+1이 11)
증상 (2026-09-09 실측): console.log("1" + 1) → 11. Number("1") + 1 → 2.
원인: 입력창에서 읽은 값은 숫자가 아니라 문자열입니다. 문자열끼리의 +는 이어붙이기입니다.
해결: Number(값)으로 숫자로 변환한 뒤 더하세요.
벽 4. 백틱 문자열이 안 먹힌다
증상: ${n}이 변환되지 않고 글자 그대로 출력됩니다.
원인: 백틱()이 아니라 작은따옴표(')로 문자열을 감쌌습니다. ${} 치환은 백틱 문자열에서만 동작합니다. 해결: 키보드 숫자 1 왼쪽의 백틱 키로 감싸세요. 세 종류의 따옴표(', ", ) 중 ${}를 쓸 수 있는 것은 백틱뿐입니다.
벽 5. const를 바꾸려다 멈춘다
증상 (2026-09-09 실측):
TypeError: Assignment to constant variable.
원인: const로 만든 상자에 새 값을 넣으려 했습니다.
해결: 바뀌는 값이면 처음부터 let으로 선언하세요. 헷갈리면 "기본은 const, 바꿀 필요가 생기면 let"이라는 현대 JS 관습을 따르면 됩니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| JavaScript | 브라우저 안에서 DOM을 만지는 프로그래밍 언어 |
| Node.js | JS를 브라우저 밖(터미널)에서 실행해 주는 도구 — document 같은 브라우저 창구는 없음 |
| script 태그 | 페이지 안의 엔진실 — </body> 직전에 둔다 |
| 이벤트/콜백 | 사건(클릭 등)이 오면 실행되도록 미리 예약해 둔 함수 |
| JSON | 데이터를 주고받는 문자열 형식 — stringify로 포장, parse로 풀기 |
| null / undefined | "찾았지만 없음"과 "정해진 적 없음" — JS 오류의 단골 주인공 |
오늘의 문법·명령
| 문법·명령 | 하는 일 |
|---|---|
let x = 5; / const PI = 3.14; |
변수 / 상수 만들기 |
function f(a) { ... } |
함수 정의 |
console.log(x) |
출력 (브라우저에서는 Console 탭으로) |
`값은 ${x}` |
백틱 문자열 — f-string에 해당 |
document.getElementById("msg") |
id로 요소 찾아오기 (브라우저 전용) |
요소.textContent / 요소.value |
태그 속 글자 / 입력창 속 값 |
요소.addEventListener("click", 함수) |
클릭 사건에 함수 예약 |
JSON.stringify / JSON.parse |
객체 → 문자열 / 문자열 → 객체 |
node 파일.js |
터미널에서 JS 실행 |
명령어보다 중요한 감각
오늘의 핵심 감각은 "누가 이 코드를 실행하는가"를 항상 묻는 것입니다. 같은 JS라도 문법 실험은 어디서든 되지만, document는 브라우저에서만 살아 있습니다. 그리고 오늘 배운 것을 공격자 시선으로 뒤집어 보세요. ① alert가 실행된다 = 코드 주입 성공의 증거. ② getElementById로 읽을 수 있다 = 쿠키나 입력값도 JS로 읽을 수 있다. ③ 입력값이 화면에 반영된다 = 태그가 반영되면 스크립트도 반영될 수 있다. 이 셋이 모이면 XSS입니다. 참고로 요소의 내용을 바꾸는 방법에는 textContent 말고 innerHTML도 있는데, 이것은 글자를 HTML로 해석해서 넣습니다. 사용자 입력을 innerHTML로 반영하는 코드가 XSS의 단골 소재입니다 — "사용자 입력은 textContent로"를 오늘부터 습관으로 삼으세요.
전부 체크되면 Step 72 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.