Section 1~2 (변수·상수, 자료형과 연산자) 챕터
딥다이브 요약 GitHub
안녕하세요. 드디어 JavaScript를 시작합니다. 지금까지 HTML로 구조를, CSS로 스타일을 배웠다면, 이제 동작을 담당하는 언어를 배울 차례입니다. 자바, 파이썬, C++ 등 다른 언어를 경험하셨다면 문법은 금방 익힐 수 있습니다. 하지만 JS에는 다른 언어에서 보기 힘든 독특한 동작 방식이 있습니다. 이번 주는 그 차이를 이해하는 것이 목표입니다.
아래 10문제를 막힘없이 풀 수 있다면,
이번 주 학습 자료를 생략하거나, 모르는 부분만 선택적으로 읽으셔도 좋습니다.
바로 하단의 더 알면 좋을 것들을 확인해보세요.
Java나 C++에서는 변수를 선언할 때 타입을 명시합니다. 정적 타입 언어라고 부릅니다. 반면 JS는 동적 타입 언어로, 변수에 어떤 타입의 값이든 넣을 수 있고 런타임에 타입이 결정됩니다.
// Java였다면: int x = 1;
// JS에서는
let x = 1; // 지금은 숫자
x = "hello"; // 이제 문자열 — 에러 없음
x = true; // 이제 불리언 — 에러 없음
x = null; // 이제 null — 에러 없음
JS의 원시 타입은 7가지입니다:
number, string, boolean, undefined, null, symbol, bigint (
symbol과 bigint는 지금 주차에서는 몰라도 됩니다.
심화 주차에서 다룹니다). 이 외에 참조 타입으로 object(배열,
함수, 일반 객체 포함)가 있습니다.
대부분의 언어는 정수와 실수 타입이 분리되어 있습니다(int, float, double 등). JS는 number 하나로 모든 숫자를 표현합니다. 내부적으로 IEEE 754 64비트 부동소수점을 사용하는데, 쉽게 말해 컴퓨터가 소수를 2진수로 저장할 때 딱 맞아 떨어지지 않는 경우 가 생긴다는 뜻입니다. 여기서 유명한 버그가 나옵니다.
0.1 + 0.2 === 0.3; // false
0.1 + 0.2; // 0.30000000000000004
이는 JS만의 문제가 아니라 부동소수점 표현 방식 자체의 한계입니다. 실무에서 금액 계산 등 정밀도가 중요한 경우에는 정수로 변환하거나 라이브러리를 사용해야 합니다.
Infinity - // 양의 무한대
Infinity; // 음의 무한대
NaN; // Not a Number — 숫자가 아님을 나타내는 숫자(!)
1 / 0 - // Infinity
1 / 0; // -Infinity
"hello" * 2; // NaN
NaN === NaN; // false — NaN은 자기 자신과도 같지 않음
Number.isNaN(NaN); // true — NaN 확인은 이걸 써야 함
다른 언어에서는 보통 null 하나로 "값 없음"을 표현하는데, JS에는 undefined와 null이 둘 다 있습니다.
let x; // undefined — 엔진이 자동 부여
let y = null; // null — 개발자가 의도적으로 "비어있음" 명시
실무에서 권장하는 방식은 변수를 의도적으로 비울 때는 null을 사용하고, undefined는 직접 할당하지 않는 것입니다. undefined를 직접 할당하면 "아직 초기화 안 됨"인지 "의도적으로 비운 것"인지 구분이 안 되기 때문입니다. undefined가 "아직 초기화되지 않은 상태"를 어떻게 만들어내는지는 섹션 2의 var/let/const 호이스팅 파트에서 다시 만납니다.
다른 언어 경험자라면 변수 선언에서 const는 상수구나, 하고 이해할 수 있을겁니다. 그러나, var와 let은 왜 있는 걸까요? 그리고 var는 왜 쓰지 말라고 하는 걸까요?
// Java, Python이라면 블록 밖에서 x를 쓸 수 없음
// JS var는 다릅니다
if (true) {
var x = 10;
}
console.log(x); // 10 — 블록 밖에서도 접근 가능!
// let은 예상대로 동작
if (true) {
let y = 10;
}
console.log(y); // ReferenceError: y is not defined
JS 엔진은 코드를 실행하기 전에 var 선언을 스코프 최상단으로 끌어올립니다. 이를 호이스팅이라고 합니다. 이 때 발생하는 이슈가 선언은 올라가지만 값 할당은 올라가지 않는다 입니다.
console.log(x); // undefined — 에러가 아님!
var x = 5;
console.log(x); // 5
// JS 엔진이 실제로 실행하는 방식
var x; // 선언이 여기로 끌어올려짐
console.log(x); // undefined
x = 5; // 할당은 원래 위치에서 실행
console.log(x); // 5
왜 이런 일이 벌어지는지 한 단계 더 들어가 봅니다. 쉽게 말해 JS 엔진은 코드를 두 번 읽는다고 생각하면 됩니다. 첫 번째 읽기가 생성 단계로, 엔진이 스코프 안의 선언(변수와 함수)을 미리 수집해둡니다. 두 번째 읽기가 실행 단계이고, 이때 비로소 위에서 아래로 한 줄씩 코드가 흘러가며 할당·계산이 일어납니다. 호이스팅은 뭔가가 물리적으로 코드 위로 올라가는 것이 아니라,
첫 번째 읽기에서 이미 엔진이 변수의 존재를 알고 있기 때문에 나타나는 현상
입니다.
여기서 핵심은 "같은 호이스팅인데 왜 var만 undefined가 되는가"입니다. 생성 단계에서 var는 선언과 함께 undefined로 초기화까지 끝내버립니다. 그래서 선언문보다 앞에서 읽어도 undefined가 반환됩니다. 반면
let과 const는 생성 단계에서 선언만 이루어지고, 초기화는 실제 선언문에 도달한 순간에야 일어납니다
. 이 "선언은 끝났지만 아직 초기화되지 않은 구간"이 바로 뒤에서 볼 TDZ입니다.
말로만 들으면 잘 와닿지 않으니 코드로 단계를 쪼개서 봅니다. let으로 선언한 변수를 선언문보다 먼저 접근하면 어떻게 되는지부터 확인합니다.
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 1;
var였다면 undefined가 출력됐겠지만, let은 선언만 호이스팅되고 초기화는 아직 이뤄지지 않았기 때문에 접근 자체가 막힙니다. 이 "선언은 됐지만 초기화는 안 된" 구간이 TDZ(Temporal Dead Zone), 우리말로 옮기면 "일시적 사각지대"입니다.
함수 안에서 TDZ가 어디서 시작해서 어디서 끝나는지 주석으로 표시해보면 이렇게 됩니다.
function example() {
// ── 여기부터 TDZ 시작 (a가 선언은 됐지만 초기화 전)
console.log(a); // ReferenceError
let a = 1;
// ── 여기부터 TDZ 끝 (a 초기화 완료)
console.log(a); // 1
}
한 가지 주의할 점은 TDZ 안에서는 typeof 연산자조차 에러를 냅니다. "선언되지 않은 변수에는 typeof가 안전하다"고 기억하는 분들이 많은데, TDZ에 놓인 변수에는 이 규칙이 통하지 않습니다.
typeof a; // ReferenceError
let a;
TDZ 덕분에 let과 const는 var에서 흔하게 일어나던 "선언 전 참조 시 undefined 반환" 문제가 근본적으로 차단됩니다. 예상하지 못한 undefined가 코드 어딘가로 흘러들어가 버그를 만드는 대신, 에러가 즉시 터져 개발자가 바로 잡을 수 있도록 설계한 것입니다.
var x = 1;
var x = 2; // 에러 없음, 그냥 덮어씀
console.log(x); // 2
let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declared
| var | let | const | |
|---|---|---|---|
| 스코프 | 함수 | 블록 | 블록 |
| 재선언 | 가능 | 불가 | 불가 |
| 재할당 | 가능 |
실무에서는 기본적으로 const를 쓰고, 재할당이 필요한 경우에만 let을 씁니다. var는 쓰지 않습니다.
이번 주 본문에서는 얄코 강의 Section 1~2 범위(타입·변수·호이스팅)만 다뤘지만,
JS 입문에서 자주 같이 묶이는 주제가 두 가지 더 있습니다. 암묵적 타입 변환과
==/===는 이번 주 안에서 핵심만 짚고 마무리하고, 단축
평가와 ?? 연산자는 실무 패턴이 중요해서
week10(스프레드· 디스트럭처링 주차)로 미뤄둡니다.
JS는 타입이 맞지 않는 연산을 만나면 에러 대신 타입을 알아서 바꿔주는 동작이
있습니다. '5' - 1이 4가 되고 0 == false
가 true가 되는 게 여기서 옵니다. 먼저 콘솔에서 아래 몇 줄을 직접
돌려보며 "이상하다" 감각부터 잡아볼게요.
"5" - 1; // 4
0 == false; // true
null == undefined; // true
null == 0; // false — 이상하지 않나요?
+ 연산자에는 한 가지 꼭 알아둬야 할 규칙이 하나 더 있습니다.
피연산자 중 한쪽이라도 문자열이면, 나머지를 문자열로 변환해서 이어 붙인다
는 규칙입니다. 또 +는 왼쪽에서 오른쪽으로
순서대로 평가되기 때문에, 같은 수식이라도 항의 순서가 결과를 바꿉니다.
"3" + 1; // "31" — + 연산자는 한쪽이 문자열이면 나머지를 문자열로 변환
3 + 1 + "1"; // "41" — 왼쪽부터 평가: 3+1=4, 4+"1"="41"
비교 연산자에도 비슷한 "알아서 바꿔주는" 함정이 있습니다. ==는 두
값의 타입이 다르면 먼저 타입을 맞춘 뒤 비교합니다. 그래서
0 == false, '' == false,
null == undefined가 전부 true로 나옵니다.
===는 타입 변환 없이 값과 타입이 모두 같아야
true를 반환합니다. 실무에서는 예측 불가능한 버그를 피하려고
기본적으로 ===
를 씁니다. 한 줄 규칙:
비교는 ===, 문자열 결합만 +
라고 외워두세요.
0 == false; // true — false를 숫자 0으로 변환해서 비교
0 === false; // false — 타입이 달라서 false
null == undefined; // true — == 스펙의 특별 규칙
null === undefined; // false
||와 && 연산자는 boolean이 아닌 "값"을 반환합니다.
'hello' || 'default'가 'hello'를 돌려주는 동작(단축
평가)과, null/undefined일 때만 대체하는
?? 연산자는 React 컴포넌트에서 기본값을 다룰 때 정말 자주 씁니다.
이 주제는 week10(스프레드·디스트럭처링 주차)에서 실무 패턴과
함께 다룹니다. 지금은 "이런 게 있구나" 정도로 충분합니다.
const name = userInput || "익명"; // userInput이 falsy면 "익명"
const count = userCount ?? 0; // userCount가 null/undefined일 때만 0
아래 코드를 브라우저 콘솔에서 직접 실행해보고 결과를 입력해보세요. 예상과 다른 결과가 나왔다면 위 내용을 다시 읽어보세요.
console.log(x);
var x = 5;
console.log(x);| 가능 |
| 불가 |
| 호이스팅 시 동작 | undefined | ReferenceError (TDZ) | ReferenceError (TDZ) |